尧图精选

虚设备详解:从软件模拟到硬件辅助的虚拟化实践

🕒 发布时间:2026/9/9 9:07:49 📁 来源:尧图网络
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 最接地气的软件模拟SPIGPIO硬扛时序软件模拟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设备叠加一层结果驱动出了问题反而耽误了进度。虚设备的正确用法是解决“多个逻辑使用者争抢一个物理资源”的问题而不是为了简历上多一行“虚拟化经验”。把这一点想明白你的技术方向就不会跑偏。真到动手那天记得从最小模块开始一点一点加功能。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →