虚设备详解:从软件模拟到硬件辅助的虚拟化实践
2026/9/9 9:07:26 网站建设 项目流程

1. 先搞清楚:什么是虚设备

很多人第一次听到虚设备,会把它和“虚拟机”搞混。我最初接触这个词,是在一块板子上调SPI:项目里同时挂了触摸屏、Flash和一颗温湿度传感器,三路都走SPI,可MCU只有一个硬件SPI外设。硬件上加不了,只能让软件想办法。后来我用软件把一个物理SPI控制器拆成三个逻辑SPI设备,每个设备独立控制片选,问题才解决。所以在我看来,虚设备(Virtual Device)的定义不用死记,直接用一句话理解就行:通过软件模拟的方式,使一个物理设备在逻辑上表现为多个设备。

这篇内容不是说概念就完了,我会把这个定义掰开来讲,再给出一套可以照着做的实现思路。不管你是做嵌入式、搞Linux驱动,还是在云平台上做设备虚拟化,只要遇到过“硬件外设不够用、但又不方便加芯片”的处境,这篇内容应该对你有用。就算你只是听说过这个词,也能从里面找到几个能直接动手的小例子。

1.1 从物理设备到逻辑设备

要理解虚设备,得先把“物理设备”和“逻辑设备”这两个词理清。物理设备很好理解,就是板上那颗实实在在的芯片,比如I2C控制器、UART控制器、网卡、声卡。操作系统和应用进程通常不会直接操作寄存器,而是通过设备驱动和文件系统暴露出来的接口去访问它。这个暴露出来的接口,就是逻辑设备。

举个例子,一个USB转串口芯片插入电脑后,驱动会创建出/dev/ttyUSB0。这个节点的本质是一个文件,但应用层读写它,就是在跟底层物理串口通信。这个阶段我们看到的还是一个物理设备对应一个逻辑设备,并没有体现“拆分”。

真正的虚设备,发生在同一个物理设备被逻辑拆分的场景。芯片只有一个UART控制器,但通过软件把波特率、数据位、收发缓冲区分成两组,一组给GPS模块用,一组给调试终端用,上层看起来就是/dev/ttyS0和/dev/ttyS1两个互不干扰的串口。上层的应用根本不需要知道自己正在和同一个物理UART通信。这就是“一个物理设备,多个逻辑设备”最直观的形态。

1.2 虚设备到底“虚”在哪里

虚设备的“虚”,不是指硬件不存在,而是指上层的观察视图被软件改写了。这个“软件”可以出现在好几层:可以在驱动里,可以在Hypervisor里,也可以在纯用户态程序里。

从实现方式上看,虚设备大概可以分成三类:第一类是把一个物理设备切分成多个逻辑实例,这是最贴合标题定义的做法,比如一个物理SPI控制器的多个片选引脚,分别对应多个虚拟SPI从设备;第二类是把多个物理设备聚合成一个逻辑设备,比如多块硬盘组成一个RAID卷,上层只看到一个块设备;第三类是干脆用纯软件“无中生有”,连物理硬件都不依赖,比如Linux里的loop回环设备,它就是一个文件模拟成块设备。

我一直觉得,理解虚设备的关键,不在于记住这三类分类,而在于意识到“设备”这个概念本来就是分层的。硬件是基础,驱动是桥梁,逻辑设备是入口。虚设备只不过是把桥底下真实硬件的位置挪了挪,或者把入口多开几个。上层的业务逻辑只要接口人设不变,就感觉不到底层变化。

1.3 虚设备和模拟器、仿真器的边界

很多人会把虚设备和模拟器、仿真器混在一起说,虽然它们有交集,但不能完全画等号。模拟器通常连CPU指令集都一起模拟,比如QEMU模拟一块ARM开发板时,连CPU都是翻译执行的;仿真器更多强调二进制兼容,目标是在另一个平台上原样运行;而虚设备关注的是设备接口层面的行为,它的重点是“让上层以为自己在访问真实设备”,并不一定需要模拟出全套CPU环境。

实际工程里,虚设备经常作为模拟器的一部分出现。QEMU里给虚拟机提供的一块virtio磁盘,就是一个典型的虚设备:它由QEMU进程在宿主机上用软件实现,虚拟机里看到的是一块PCI磁盘控制器,对它发请求,后端其实是宿主机上的一个普通文件。反过来,不是所有模拟器都用了虚设备。你在电路模拟软件Digital里搭一个SPI主控,这个主控不是给操作系统用的,它只是一个抽象模型,但设计思想是一脉相通的——用软件行为替代真实硬件行为。

搞清楚这个边界有个实际好处:当你需要自己造一个虚设备时,不需要去碰指令集模拟那一堆重装备,只需要把设备对外接口做出来,把数据通路打通,就够用了。很多人被“虚拟化”三个字吓住,其实做的只是虚设备这一小层。

2. 虚设备的核心原理:软件到底在哪一层做手脚

先看一个反直觉的事实:操作系统并不在乎你访问的设备是不是真实存在的。它只在乎三样东西——中断、地址空间、数据通道。linux内核里的设备模型,其实就是在管理这三样东西的抽象。虚设备要实现“一个物理设备变多个逻辑设备”,本质是在这三样东西上做文章。

2.1 设备模型:操作系统如何“认设备”

Linux的设备模型里,最核心的关系是“总线-设备-驱动”。驱动挂在总线上,设备也在总线上,当两者的ID匹配成功,驱动就进入到探针流程。对于真实PCI设备,这种匹配靠PCI配置空间里的Vendor ID和Device ID;对于虚拟设备,这套机制同样可以复用。

早期做虚拟设备,最简单的方式是注册一个miscdevice。miscdevice是内核里一类杂项设备,它使用主设备号10,每个设备用不同的次设备号区分。驱动里只要调用misc_register注册一次,系统里就会多出一个/dev/xxx节点。注册两次,就有两个节点。每个节点对应同一份底层物理资源,但通过file_operations里的read、write、ioctl函数,你可以在软件上把它们导向不同的逻辑通路。这就是一个最小可行虚设备的雏形。

现代的设备模型比miscdevice更复杂,比如platform_device用于描述板级设备,virtio设备用于虚拟化场景。但无论哪一套,本质都是让驱动通过“设备节点、驱动核心、数据回调”这条链对上用户空间。虚设备要做的就是在这条链的不同位置插入软件逻辑,让同一个物理资源被多份回调共用。

2.2 中断、地址空间和DMA资源怎么分

当一个物理设备被拆成多个逻辑设备后,最大麻烦是中断共享。硬件只有一个中断号,但逻辑上可能挂了三四个设备。一旦发生中断,驱动不知道该把这次事件交给哪个逻辑设备处理。

解决思路一般是先读状态寄存器,再分发。比如一个物理UART对应两个虚拟串口,中断进来后,驱动去读中断状态寄存器,如果状态位显示“接收FIFO有数据”,再判断数据属于哪一个逻辑通道,然后调用对应的tty层函数。这个过程叫中断分发,是虚设备驱动里最常见的一段代码。

地址空间的处理也不能忽略。多个逻辑设备可能映射到同一个物理基地址,但通过不同的偏移区分寄存器。Linux里的ioremap可以让你把同一段物理地址映射到多个虚拟地址空间,每个逻辑设备各管理各的偏移。DMA通道更要小心,如果物理外设只有一个DMA缓冲区,两个逻辑设备同时申请DMA,就必须加锁或者用软件缓存轮转,否则数据一交错,上层拿到的就是乱码。

2.3 数据通路里的仲裁和调度

除了中断和地址,虚设备还得考虑数据请求的并发。同一个物理设备就算被拆成十个逻辑设备,底层执行能力还是那一份。如果十个逻辑设备同时发起读写,你不能让它们直接都冲到物理控制器上,得先在软件里做仲裁。

SPI总线上有一个很形象的例子:一个硬件SPI控制器,拖了四个从设备,片选引脚决定当前跟谁通信。在软件模拟SPI时,片选就是一把锁。A设备要发数据时先把片选拉低,发完再拉高;B设备想发数据就得等。这个过程看似简单,但如果中间没有做好并发保护,A发了一半,B插进来把片选拉走了,两边数据就全乱了。

块设备虚拟化里的调度策略会更复杂。一个物理NVMe盘虚拟成多个逻辑卷后,每个逻辑卷都希望自己独占带宽,可底层的队列深度是有限的。这时候就要在软件层做IO调度,比如按权重轮转,保证优先级高的逻辑卷不会一直被饿死。虚设备做得好不好,一半看接口,另一半就看这个调度层够不够精细。

3. 我实际做过的几种虚设备实现

空讲原理没意思,我把自己做过的几种实现方式按复杂度排了个序。从最朴素的GPIO模拟SPI,到Linux字符设备,再到虚拟化里的virtio和SR-IOV,你可以看到虚设备在不同层级上的样子。

3.1 最接地气的软件模拟SPI:GPIO硬扛时序

软件模拟SPI可能是大多数嵌入式工程师接触到的第一个“虚设备”。它的本质很粗暴:MCU片内没有硬件SPI,或者硬件SPI被别的设备占用了,于是用几个普通GPIO引脚,按SPI协议的时序手动拉电平。

下面是一段非常简化的主机发送代码:

#define SCLK_PIN (1 << 5) #define MOSI_PIN (1 << 6) #define CS_PIN (1 << 7) void spi_delay(void) { // 空转或调用udelay,按需要的速率调整 } void spi_cs_low(void) { GPIO_OUTPUT_CLR(CS_PIN); } void spi_cs_high(void) { GPIO_OUTPUT_SET(CS_PIN); } void spi_send_byte(uint8_t data) { for (int i = 7; i >= 0; i--) { GPIO_OUTPUT_CLR(SCLK_PIN); // SCLK拉低,准备数据 if (data & (1 << i)) GPIO_OUTPUT_SET(MOSI_PIN); // 发送位为1 else GPIO_OUTPUT_CLR(MOSI_PIN); // 发送位为0 spi_delay(); // 等信号稳定 GPIO_OUTPUT_SET(SCLK_PIN); // SCLK拉高,从设备采样 spi_delay(); } }

这段代码会把一个物理SPI控制器占用的SCLK引脚让出来,用普通GPIO去模拟另外一路SPI,于是MCU上就同时存在“硬件SPI”和“软件SPI”两套逻辑主机。再配合不同的片选引脚,一个物理GPIO组就能虚拟出多个SPI设备。

我用这个方案在项目里同时驱动了一颗Flash和一颗温湿度传感器,吞吐量虽然比硬件SPI低,但胜在灵活。注意,软件模拟SPI最怕时序不对,尤其是CPOL和CPHA必须和从设备匹配。有些传感器对时钟极性很敏感,SCLK空闲高还是低都会导致读出来的数据全是0xFF。我建议在任何一段模拟SPI代码里,都把时钟极性和相位定义成宏,方便调试。

3.2 在Linux里注册一个虚拟字符设备

如果你想在Linux环境里做虚设备,不用一上来就写全套PCI驱动,从miscdevice开始最容易。下面是一个最小可运行的内核模块骨架,它会创建一个/dev/vdev0节点,应用层可以像读写文件一样访问它。

#include <linux/module.h> #include <linux/miscdevice.h> #include <linux/fs.h> static ssize_t vdev_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { // 这里返回自定义数据,可以指向某个物理设备寄存器模拟值 return 0; } static ssize_t vdev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) { // 这里接收应用层请求,分发给对应的物理设备 return count; } static const struct file_operations vdev_fops = { .owner = THIS_MODULE, .read = vdev_read, .write = vdev_write, }; static struct miscdevice vdev_device = { .minor = MISC_DYNAMIC_MINOR, .name = "vdev0", .fops = &vdev_fops, }; static int __init vdev_init(void) { return misc_register(&vdev_device); } static void __exit vdev_exit(void) { misc_deregister(&vdev_device); } module_init(vdev_init); module_exit(vdev_exit); MODULE_LICENSE("GPL");

编译装载后,系统里就会多出/dev/vdev0。如果你想让它表现为“多个设备”,也很简单:再写一个vdev1的miscdevice结构体,用同一个物理资源作为后端,分别挂不同的文件操作处理函数。上层开两个终端,一个写vdev0,一个写vdev1,它们各自的数据通路由你控制,完全可以做到互不干扰。

这个例子的价值不在于功能,而在于让你理解逻辑设备的创建方式。内核只在乎你注册了多少个miscdevice,每个设备号对应哪个fops,它并不关心底层是不是同一颗芯片。换句话说,软件模拟层给了你充分的自由度。

3.3 virtio前后端:虚拟化场景的标准答案

如果说字符设备虚设备还是“小打小闹”,那虚拟化场景里的virtio就是工业级的做法。virtio的典型架构是前端驱动在虚拟机里,后端设备在宿主机上。前后端通过共享内存里的virtqueue环形队列通信,虚拟机把IO请求放进队列,宿主机上的后端进程取出请求,执行完再把结果放回去。

我最早看virtio代码时,觉得它特别绕,后来才明白虚设备在这种场景下的核心难点是“少拷贝”。如果每次读写都要在虚拟机、宿主机、物理设备之间搬几遍数据,性能就直接崩了。virtio通过共享内存和内存屏障,让前后端在同一个缓冲区内操作,尽量做到零拷贝。所以常见的虚拟网卡、虚拟磁盘都用virtio,而不是纯软件把每个字节翻译一遍。

如果你要做这一类虚设备,我的建议是先不要急着读整个QEMU源码,重点看两个部分:一是virtqueue的分配和通知机制,二是前后端如何协商feature位。把这俩搞明白,virtio就算入门了。真正实现时,你还可以选择直接复用内核里的vhost框架,把后端放到内核态,进一步减少上下文切换。

3.4 SR-IOV:硬件辅助下的“一拆多”

还有一种虚设备不是纯软件,而是硬件辅助的,比如PCIe的SR-IOV。一张物理网卡支持SR-IOV时,它的PCIe配置空间里会有一个物理功能PF和若干个虚拟功能VF。通过软件打开SR-IOV开关,一个PF可以创建出多个VF,每个VF对虚拟机来说都像一张独立的物理网卡。

这种方式的“虚”,体现在VF的配置空间和队列资源是从PF里虚拟分配出来的,但真正收发数据时,硬件会通过内部地址映射直接把包分发到对应VF的队列,不需要Hypervisor软件一层层转发。所以SR-IOV的性能非常接近物理直通,同时又有一定的隔离性。

如果想用SR-IOV做实验,可以找一张支持SR-IOV的网卡,比如常见的Intel 82599或Mellanox系列,然后在宿主机上打开IOMMU,使用ip link set eth0 vf 0 mac ...之类的命令创建VF,再把它分配给虚拟机。需要注意,SR-IOV本身依赖硬件支持,不是所有芯片都能用软件模拟出来。如果你的网卡不支持VF,再折腾也没用。

4. 虚设备在行业里的几个典型应用

虚设备不是实验室里的概念,在很多行业里已经是标配。我挑了四个比较有代表性的场景,分别是嵌入式测试、电路仿真、金融业务测试和云环境设备调度。它们表面上差距很大,但底层用的都是同一套逻辑拆分思想。

4.1 嵌入式开发和硬件在环测试

嵌入式产品开发最痛苦的事,是硬件还没回来但软件必须提前开写。没有真外设怎么办?用虚设备模拟。比如Linux内核里的dummy_hcd就是一个虚拟USB主机控制器驱动,它不需要任何物理USB芯片,就可以模拟出完整的USB主机功能。你可以把U盘驱动挂上去,做读写测试,等真硬件回来后再切换过去。

我在一个项目里就干过类似的事:当时要调试一个通过I2C挂载的传感器驱动,但传感器样片比开发板晚到两周。我用内核里的i2c-dev模拟一个虚拟I2C客户端,在驱动层面先把数据解析逻辑调通,样片到了之后只改一个地址就够了,整个联调时间压缩了一半。这种做法的核心价值是,把硬件依赖从开发链路里剥离,让软件不再等硬件。

4.2 电路模拟软件Digital里的虚拟器件

如果你平时做数字电路设计,一定听说过Digital这款开源电路模拟软件。它不需要真实的FPGA开发板,也不需要逻辑分析仪,直接在软件里把你画的电路跑起来。门电路、触发器、计数器、存储器,每个元器件都是一个虚拟器件,但它们的逻辑行为和时序波形跟真实芯片基本一致。

我在学习SPI时序时就用过Digital:先在软件里搭一个SPI主机和一个SPI从设备,然后用软件自带的逻辑分析仪看波形,验证CPOL和CPHA设得对不对。相比在真实板子上抓波形,Digital最大的好处是可以反复回放、断点调试。你甚至可以把一个复杂外设拆成多个虚拟模块,每个模块独立仿真,这就是虚设备思想在EDA领域的具体落地。

4.3 银行模拟软件:业务系统的“假外围”

再说一个跟硬件不太沾边的场景。金融软件在联调测试时,不可能每次都连真实的银行核心系统,更不能随便发起真实交易。这时候就需要一个银行模拟软件,用软件模拟卡中心、账务系统、支付通道的对外接口,让被测系统以为自己在跟真银行通信。

这类模拟软件跟虚设备的关系在于,它同样是在逻辑上把一个“物理真实系统”替换成多个可控的模拟对象。一个被测系统可能同时对接短信、支付、账务三个外部渠道,测试环境里就可以用一个银行模拟软件把这三个渠道都虚拟出来,每个渠道返回不同的预设报文。这样既不影响生产环境,又能覆盖各种异常分支。它的本质,是用软件模拟外部设备的接口行为,让被测方获得真实的联调体验。

4.4 容器和云环境中的设备虚拟化

到了云平台这一层,虚设备的“多对一”就更明显了。一个GPU物理卡,通过驱动和调度器可以拆成多个容器可见的虚拟GPU资源;一块RDMA网卡,也可以通过虚拟化技术拆成多个实例,分给不同容器使用。

容器的设备插件机制就是一个很好的例子。管理节点上的Device Plugin负责“发现-上报-分配”物理设备,它可以把一张物理GPU上报成多个资源单位。容器运行时在创建容器时,通过环境变量或挂载方式把虚拟后的资源放进去。应用看到的可能是/dev/dri/renderD128,实际上后端指向的可能是同一个物理GPU。这种模式让有限的物理设备资源利用率大幅提升,也是当前多云基础设施里常见的做法。

5. 实操避坑:我踩过的坑和排查思路

虚设备听着不错,真正做的时候坑不少。这里整理几个典型问题,都是我实际遇到过、花了不少时间排查的。按出现频率排,大概有中断风暴、时序错位、设备号冲突和性能取舍这几类。

5.1 中断风暴和CPU占用过高

软件模拟的中断不像硬件中断那样“天然收敛”。如果虚拟设备处理逻辑写得不好,尤其是中断服务程序里没有做状态清除或合并,很容易出现一个中断触发一次处理、处理完又立刻触发一次,CPU占用直接飙到接近100%。

我一次排查一个虚拟串口设备,发现只要往它写一个字节,宿主机的CPU占用就会跳高。后来用perf采样才发现,中断处理里每次读写都调用了udelay,导致中断上下文长时间占用CPU。解决办法很简单:把需要延时的操作移到工作队列里,中断服务程序只负责接收数据并通知内核线程,真正的读写放在进程上下文执行。这之后CPU占用立刻降下来了。

另外,很多虚拟设备会频繁产生msi中断,尤其是在高吞吐的虚拟网卡场景。你可以参考内核NAPI的思路,把多次数据到达聚合成一次软中断,而不是每一次包都触发一次硬中断,这样能显著降低中断次数。

5.2 时序偏差导致的数据错位

软件模拟SPI的坑比想象中多。表面看只是把SCLK来回拉几下,但真实从设备对时序的容限可能很小。常见的现象是:写入寄存器一切正常,读数据时总是错一个bit,或者读回的数据整体是乱码。

我排查过一个温湿度传感器,软件模拟SPI读回来的数值老是跳变。后来用逻辑分析仪抓波形才发现,SCLK上升沿之后MOSI上的数据没有稳定足够时间,从设备采样时刚好采到了电平跳变点。解决办法是在SCLK拉高之前增加一个极短的延时,让MOSI彻底稳定。另一个问题是连续读多个字节时,片选信号没有严格按照命令字格式拉高拉低,导致数据边界错位。这类问题最好的排查工具是逻辑分析仪或Digital这类仿真软件,光靠眼睛看代码很难发现。

5.3 设备号冲突和权限怪异

Linux下虚设备一旦多了,最容易出问题的就是设备号冲突和应用层没有访问权限。miscdevice如果用MISC_DYNAMIC_MINOR,主设备号统一是10,次设备号由内核动态分配,冲突概率不大。但如果你手动指定次设备号,又跟另一个注册的设备撞了,misc_register会直接返回失败,而且dmesg里不一定有明确提示。

权限问题更常见。很多新手加载内核模块后,发现/dev/vdev0存在,但普通用户打开时报Permission denied。这是因为默认创建的设备节点属主是root。建议在模块加载后写一条udev规则,把设备节点权限改成0666,或者把用户加进对应组。容器里访问设备则要额外配置cgroup的device allowlist,否则就算宿主机权限对了,容器里依然看不到设备。

5.4 性能到底差多少,心里要有数

不是所有虚设备都能做到性能无损。软件模拟GPIO SPI的吞吐量一般只有几十kbps到几Mbps,和硬件SPI动辄几十Mbps差了一个数量级;virtio-net经过优化后可以达到接近物理网卡的吞吐;而SR-IOV因为有硬件辅助,性能衰减可以控制在很小范围。

所以做虚设备之前,先问自己一句:业务对性能的底线是多少?如果只需要发几个配置命令,软件模拟完全够用;如果要跑大数据量传输,还硬上纯软件模拟,那就是跟自己过不去。我在项目里就吃过亏:一个虚拟USB控制器模拟串口,用来传输固件升级包,结果升级一次要十几分钟,后来还是改回真实的物理串口才解决。选型这件事,一定要在动手前想清楚。

6. 想自己动手做虚设备,从哪里开始

如果你想完整地体验一次虚设备的开发,我建议按照下面的顺序,从最小程序开始,慢慢增加复杂度。

6.1 从最小可运行的虚拟字符设备入手

不要上来就写PCI驱动或者virtio,先做一个最简单能跑的内核模块。我前面给过的miscdevice例子就是一个很好的起点。你只要把它编译加载,然后写一段用户态程序打开/dev/vdev0,读写几个字节,基本就算入坑了。

接下来的扩展方向有四个:一是让read/write返回或接收真实数据,二是增加ioctl命令,让应用层能控制虚拟设备的工作模式,三是把设备数量从一个变成多个,四是加入中断或者等待队列,模拟真实设备的异步事件。每完成一个,你对设备模型的理解就会深一层。整个过程不需要真实硬件,用一台普通Linux虚拟机就够了。

6.2 平时调试用到的工具和学习路线

做虚设备调试,有几个工具建议掌握。dmesg是看内核驱动的第一道窗口;strace可以追踪用户态应用访问设备的过程;perf用来分析中断和CPU占用;lspci -vvv查看PCI配置空间;virsh管理虚拟机设备。电路验证阶段,Digital这类模拟软件能帮你快速确认时序。

学习路线上,我建议按“Linux设备模型 -> miscdevice/字符设备 -> platform设备 -> virtio基础 -> SR-IOV概念”的顺序走。前面两级可以在自己的虚拟机上完成,后面两级需要虚拟机管理和一定的硬件支持,可以慢慢来。关键是不要跳过第一步,很多人一上来就想啃完整套PCI驱动源码,结果被中断、DMA、BAR空间一堆概念劝退。

6.3 别为了“虚拟”而虚拟

最后说一点个人体会。虚设备是个好工具,但它不是银弹。判断一个场景要不要用虚设备,看三个条件:第一,物理资源是否真的被独占且不够用;第二,是否允许微小的性能损耗;第三,你有没有能力维护好驱动的并发和调度。三个条件都满足,再动手做。

我见过一些项目,物理USB口明明够用,却非要写一个虚拟USB设备叠加一层,结果驱动出了问题,反而耽误了进度。虚设备的正确用法,是解决“多个逻辑使用者争抢一个物理资源”的问题,而不是为了简历上多一行“虚拟化经验”。把这一点想明白,你的技术方向就不会跑偏。真到动手那天,记得从最小模块开始,一点一点加功能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询