用户空间 I/O HOWTO
作者 Hans-Jürgen Koch Linux 开发者,Linutronix
日期 2006-12-11
关于本文档
翻译
如果您知道本文档的任何翻译,或者有兴趣翻译它,请发送电子邮件至 hjk@hansjkoch.de。
前言
对于许多类型的设备,创建一个 Linux 内核驱动程序是不切实际的。真正需要的只是一种处理中断并提供对设备内存空间访问的方法。控制设备的逻辑不一定非得在内核内部,因为设备不需要利用内核提供的任何其他资源。这类设备中常见的一种是工业 I/O 卡。
为了解决这种情况,设计了用户空间 I/O 系统(UIO)。对于典型的工业 I/O 卡,只需要一个非常小的内核模块。驱动程序的主要部分将在用户空间运行。这简化了开发并减少了内核模块中严重错误的风险。
请注意,UIO 不是通用的驱动程序接口。已经由其他内核子系统(如网络、串行或 USB)很好处理的设备不适合使用 UIO 驱动程序。适合使用 UIO 驱动程序的硬件必须满足以下所有条件:
- 设备具有可映射的内存。可以通过写入该内存完全控制设备。
- 设备通常会生成中断。
- 设备不适合于标准内核子系统。
致谢
我要感谢 Linutronix 的 Thomas Gleixner 和 Benedikt Spranger,他们不仅编写了大部分 UIO 代码,还通过提供各种背景信息极大地帮助了我编写本 HOWTO。
反馈
在本文档中发现了错误吗?(或者也许发现了一些正确的内容?)我很乐意听取您的意见。请发送电子邮件至 hjk@hansjkoch.de。
关于 UIO
如果您为您的卡的驱动程序使用 UIO,您将获得以下好处:
- 只需编写和维护一个小型内核模块。
- 在用户空间开发驱动程序的主要部分,使用您习惯的所有工具和库。
- 驱动程序中的错误不会导致内核崩溃。
- 可以在无需重新编译内核的情况下更新驱动程序。
UIO 的工作原理
每个 UIO 设备都通过一个设备文件和几个 sysfs 属性文件进行访问。第一个设备的设备文件将被称为 /dev/uio0,而后续设备将被称为 /dev/uio1、/dev/uio2 等等。
/dev/uioX 用于访问卡的地址空间。只需使用 mmap() 来访问您的卡的寄存器或 RAM 位置。
中断通过从 /dev/uioX 读取来处理。从 /dev/uioX 进行阻塞读取() 将在中断发生时立即返回。您还可以在 /dev/uioX 上使用 select() 来等待中断。从 /dev/uioX 读取的整数值表示总中断计数。您可以使用此数字来确定是否错过了一些中断。
对于一些具有多个内部中断源但没有单独的 IRQ 屏蔽和状态寄存器的硬件,可能存在用户空间无法确定中断源的情况,如果内核处理程序通过写入芯片的 IRQ 寄存器来禁用它们。在这种情况下,内核必须完全禁用 IRQ 以保持芯片的寄存器不受影响。现在用户空间部分可以确定中断的原因,但它无法重新启用中断。另一个特例是对于需要将中断重新启用的芯片,这是一种读-修改-写操作,用于组合的 IRQ 状态/确认寄存器。如果同时发生新的中断,这将是有竞争条件的。
为了解决这些问题,UIO 还实现了一个 write() 函数。通常不使用它,对于只有单个中断源或具有单独的 IRQ 屏蔽和状态寄存器的硬件,可以忽略它。但如果需要,对 /dev/uioX 的写入将调用驱动程序实现的 irqcontrol() 函数。您必须写入一个通常为 0 或 1 的 32 位值,以禁用或启用中断。如果驱动程序没有实现 irqcontrol(),write() 将返回 -ENOSYS。
为了正确处理中断,您的自定义内核模块可以提供自己的中断处理程序。它将自动被内置处理程序调用。
对于不生成中断但需要轮询的卡,可以设置一个定时器,以在可配置的时间间隔触发中断处理程序。这种中断模拟是通过从定时器的事件处理程序调用 uio_event_notify() 来完成的。
每个驱动程序都提供用于读取或写入变量的属性。这些属性可以通过 sysfs 文件进行访问。自定义内核驱动程序模块可以向 UIO 驱动程序拥有的设备添加自己的属性,但目前尚未添加到 UIO 设备本身。如果发现有用的话,这可能会在将来发生变化。
UIO 框架提供以下标准属性:
- name:您的设备的名称。建议使用您的内核模块的名称。
- version:由您的驱动程序定义的版本字符串。这允许您的驱动程序的用户空间部分处理内核模块的不同版本。
- event:自上次读取设备节点以来驱动程序处理的中断总数。
这些属性出现在 /sys/class/uio/uioX 目录下。请注意,此目录可能是一个符号链接,而不是一个真实的目录。任何访问它的用户空间代码必须能够处理这一点。
每个 UIO 设备可以使一个或多个内存区域可用于内存映射。这是必要的,因为一些工业 I/O 卡在驱动程序中需要访问多个 PCI 内存区域。
每个映射在 sysfs 中都有自己的目录,第一个映射出现为 /sys/class/uio/uioX/maps/map0/。后续的映射创建目录 map1/、map2/ 等等。只有当映射的大小不为 0 时,这些目录才会出现。
每个 mapX/ 目录包含四个只读文件,显示内存的属性:
- name:用于标识此映射的字符串。这是可选的,字符串可以为空。驱动程序可以设置它以使用户空间更容易找到正确的映射。
- addr:可以映射的内存地址。
- size:addr 指向的内存的大小(以字节为单位)。
- offset:必须添加到由 mmap() 返回的指针以到达实际设备内存的偏移量(以字节为单位)。如果设备的内存不是页面对齐的,这一点很重要。请记住,mmap() 返回的指针始终是页面对齐的,因此最好始终添加此偏移量。
从用户空间,可以通过调整 mmap() 调用的 offset 参数来区分不同的映射:
offset = N * getpagesize();
有时,有类似内存的区域,无法使用这里描述的技术进行映射,但仍然有办法从用户空间访问它们。最常见的例子是 x86 ioport。在 x86 系统上,用户空间可以使用 ioperm()、iopl()、inb()、outb() 等函数访问这些 ioport。
由于这些 ioport 区域无法映射,它们不会像上面描述的普通内存一样出现在 /sys/class/uio/uioX/maps/ 下。没有关于硬件提供的端口区域的信息,对于驱动程序的用户空间部分来说,找出哪些端口属于哪个 UIO 设备变得困难。
为了解决这种情况,添加了新目录 /sys/class/uio/uioX/portio/。只有在驱动程序希望向用户空间传递有关一个或多个端口区域的信息时,它才存在。如果是这种情况,将在 /sys/class/uio/uioX/portio/ 下方出现名为 port0、port1 等的子目录。
每个 portX/ 目录包含四个只读文件,显示端口区域的名称、起始、大小和类型:
- name:用于标识此端口区域的字符串。这是可选的,字符串可以为空。驱动程序可以设置它以使用户空间更容易找到某个端口区域。
- start:此区域的第一个端口。
- size:此区域中的端口数。
- porttype:描述端口类型的字符串。
撰写您自己的内核模块
请参考 uio_cif.c 作为示例。以下段落解释了该文件的不同部分。
struct uio_info
此结构告诉框架有关您的驱动程序的详细信息,其中一些成员是必需的,其他是可选的。
- const char *name:必需。您的驱动程序的名称,它将出现在 sysfs 中。我建议使用您的模块的名称。
- const char *version:必需。此字符串出现在 /sys/class/uio/uioX/version 中。
- struct uio_mem mem[MAX_UIO_MAPS]:如果有可以使用 mmap() 进行映射的内存,则需要此项。对于每个映射,您需要填写一个 uio_mem 结构。有关详细信息,请参见下面的描述。
- struct uio_port port[MAX_UIO_PORTS_REGIONS]:如果要向用户空间传递有关 ioport 的信息,则需要此项。对于每个端口区域,您需要填写一个 uio_port 结构。有关详细信息,请参见下面的描述。
- long irq:必需。如果您的硬件生成中断,则在初始化期间确定中断号是您模块的任务。如果您没有硬件生成的中断但希望以其他方式触发中断处理程序,则将 irq 设置为 UIO_IRQ_CUSTOM。如果根本没有中断,您可以将 irq 设置为 UIO_IRQ_NONE,尽管这很少有意义。
- unsigned long irq_flags:如果您将 irq 设置为硬件中断号,则需要此项。这里给定的标志将在调用 request_irq() 时使用。
- int (*mmap)(struct uio_info *info, struct vm_area_struct *vma):可选。如果需要特殊的 mmap() 函数,可以在此处设置它。如果此指针不为 NULL,则将调用您的 mmap() 而不是内置的 mmap()。
- int (*open)(struct uio_info *info, struct inode *inode):可选。您可能希望有自己的 open(),例如仅在实际使用设备时才启用中断。
- int (*release)(struct uio_info *info, struct inode *inode):可选。如果定义了自己的 open(),您可能还需要一个自定义的 release() 函数。
- int (*irqcontrol)(struct uio_info *info, s32 irq_on):可选。如果需要能够通过写入 /dev/uioX 从用户空间启用或禁用中断,可以实现此函数。参数 irq_on 将为 0 以禁用中断,为 1 以启用中断。
通常,您的设备将具有一个或多个可以映射到用户空间的内存区域。对于每个区域,您必须在 mem[] 数组中设置一个 struct uio_mem。以下是 struct uio_mem 的字段描述:
- const char *name:可选。设置此项以帮助识别内存区域,它将出现在相应的 sysfs 节点中。
- int memtype:如果使用映射,则需要此项。如果您的卡上有物理内存需要映射,则设置为 UIO_MEM_PHYS。对于逻辑内存(例如使用 __get_free_pages() 分配但不是 kmalloc() 分配的内存),请使用 UIO_MEM_LOGICAL。还有 UIO_MEM_VIRTUAL 用于虚拟内存。
- phys_addr_t addr:如果使用映射,则需要此项。填写您的内存块的地址。此地址是出现在 sysfs 中的地址。
- resource_size_t size:填写 addr 指向的内存块的大小。如果 size 为零,则认为该映射未使用。请注意,对于所有未使用的映射,必须使用零初始化 size。
- void *internal_addr:如果您需要从内核模块内部访问此内存区域,您将希望通过使用类似 ioremap() 的函数来内部映射它。此函数返回的地址不能映射到用户空间,因此您不应将其存储在 addr 中。请改用 internal_addr 来记住这样的地址。
请不要触摸 struct uio_mem 的 map 元素!它由 UIO 框架用于为此映射设置 sysfs 文件。只需让它保持不变。
有时,您的设备可能具有一个或多个无法映射到用户空间的端口区域。但如果有其他可能让用户空间访问这些端口的方法,将有关端口的信息可用于 sysfs 是有意义的。对于每个区域,您必须在 port[] 数组中设置一个 struct uio_port。以下是 struct uio_port 的字段描述:
- char *porttype:必需。将其设置为预定义常量之一。在 x86 架构中找到的 ioport,请使用 UIO_PORT_X86。
- unsigned long start:如果使用端口区域,则需要此项。填写此区域的第一个端口的编号。
- unsigned long size:填写此区域中的端口数。如果 size 为零,则认为该区域未使用。请注意,对于所有未使用的区域,必须使用零初始化 size。
请不要触摸 struct uio_port 的 portio 元素!它由 UIO 框架内部用于为此区域设置 sysfs 文件。只需让它保持不变。
添加中断处理程序
在中断处理程序中需要做什么取决于你的硬件和你想要如何处理它。你应该尽量保持内核中断处理程序中的代码量较少。如果你的硬件在每次中断后不需要执行任何操作,那么你的处理程序可以为空。
另一方面,如果你的硬件需要在每次中断后执行一些操作,那么你必须在你的内核模块中执行这些操作。请注意,你不能依赖于驱动的用户空间部分。你的用户空间程序随时可能会终止,可能会导致你的硬件仍然需要适当的中断处理。
也许还有一些应用场景,你希望在每次中断时从硬件中读取数据,并将其缓冲在为此目的分配的一块内核内存中。使用这种技术,如果你的用户空间程序错过了一个中断,你就可以避免数据丢失。
关于共享中断的说明:你的驱动程序应该在可能的情况下支持中断共享。只有当你的驱动程序能够检测到你的硬件是否触发了中断时,中断共享才是可能的。这通常是通过查看中断状态寄存器来完成的。如果你的驱动程序发现 IRQ 位实际上被设置了,它将执行相应的操作,并且处理程序返回 IRQ_HANDLED。如果驱动程序检测到不是你的硬件引起的中断,它将不执行任何操作并返回 IRQ_NONE,允许内核调用下一个可能的中断处理程序。
如果你决定不支持共享中断,你的卡在没有空闲中断的计算机上将无法工作。由于这在 PC 平台上经常发生,通过支持中断共享,你可以避免很多麻烦。
使用 uio_pdrv 处理平台设备
在许多情况下,平台设备的 UIO 驱动程序可以以一种通用的方式处理。在定义 struct platform_device 的地方,你只需实现你的中断处理程序并填充你的 struct uio_info。然后将指向这个 struct uio_info 的指针作为你的平台设备的 platform_data。
你还需要设置一个包含内存映射地址和大小的 struct resource 数组。这些信息通过 struct platform_device 的 .resource 和 .num_resources 元素传递给驱动程序。
现在,你需要将 struct platform_device 的 .name 元素设置为 "uio_pdrv",以使用通用的 UIO 平台设备驱动程序。该驱动程序将根据给定的资源填充 mem[] 数组,并注册设备。
这种方法的优点是你只需要编辑一个你必须编辑的文件。你不需要创建额外的驱动程序。
使用 uio_pdrv_genirq 处理平台设备
特别是在嵌入式设备中,你经常会发现芯片的中断引脚连接到自己的专用中断线上。在这种情况下,我们可以进一步使用通用中断处理程序,这就是 uio_pdrv_genirq 所做的事情。
这个驱动程序的设置与上面描述的 uio_pdrv 相同,只是你不需要实现中断处理程序。struct uio_info 的 .handler 元素必须保持为 NULL。.irq_flags 元素不能包含 IRQF_SHARED。
你将把 struct platform_device 的 .name 元素设置为 "uio_pdrv_genirq" 以使用这个驱动程序。
uio_pdrv_genirq 的通用中断处理程序将简单地使用 disable_irq_nosync() 来禁用中断线。在完成工作后,用户空间可以通过向 UIO 设备文件写入 0x00000001 来重新启用中断。驱动程序已经实现了 irq_control() 使这成为可能,你不需要实现自己的。
使用 uio_pdrv_genirq 不仅可以节省一些中断处理程序代码行数,你也不需要了解有关芯片内部寄存器的任何信息来创建驱动程序的内核部分。你只需要知道芯片连接到的中断引脚的中断号。
在设备树启用的系统中,驱动程序需要使用 "of_id" 模块参数来探测与其处理的节点的 "compatible" 字符串。默认情况下,节点的名称(不包括单元地址)会暴露为用户空间的 UIO 设备的名称。要设置自定义名称,可以在 DT 节点中指定一个名为 "linux,uio-name" 的属性。
使用 uio_dmem_genirq 处理平台设备
除了静态分配的内存范围外,可能还希望在用户空间驱动程序中使用动态分配的区域。特别是,能够访问通过 dma-mapping API 提供的内存可能非常有用。uio_dmem_genirq 驱动程序提供了一种实现这一点的方法。
这个驱动程序的使用方式与 "uio_pdrv_genirq" 驱动程序在中断配置和处理方面类似。
将 struct platform_device 的 .name 元素设置为 "uio_dmem_genirq" 以使用这个驱动程序。
在使用这个驱动程序时,填写 struct platform_device 的 .platform_data 元素,它是 struct uio_dmem_genirq_pdata 类型,包含以下元素:
- struct uio_info uioinfo:与 uio_pdrv_genirq 平台数据相同的结构
- unsigned int *dynamic_region_sizes:指向要映射到用户空间的动态内存区域大小列表的指针
- unsigned int num_dynamic_regions:dynamic_region_sizes 数组中的元素数
在平台数据中定义的动态区域将被附加到平台设备资源之后的 mem[] 数组中,这意味着静态和动态内存区域的总数不能超过 MAX_UIO_MAPS。
当打开 UIO 设备文件 /dev/uioX 时,将分配动态内存区域。类似于静态内存资源,动态区域的内存区域信息随后可以通过 sysfs 在 /sys/class/uio/uioX/maps/mapY/* 中查看。当关闭 UIO 设备文件时,动态内存区域将被释放。当没有进程持有设备文件时,返回给用户空间的地址是 ~0。
在用户空间编写驱动程序
一旦你有了硬件的工作内核模块,你就可以编写驱动程序的用户空间部分。你不需要任何特殊的库,你的驱动程序可以用任何合理的语言编写,你可以使用浮点数等。简而言之,你可以使用编写用户空间应用程序的所有工具和库。
获取有关 UIO 设备的信息
有关所有 UIO 设备的信息都可以在 sysfs 中找到。在你的驱动程序中,你应该首先检查名称和版本,以确保你正在与正确的设备交互,并且它的内核驱动程序具有你期望的版本。
你还应该确保你需要的内存映射存在并且大小符合你的预期。
有一个名为 lsuio 的工具,它列出了 UIO 设备及其属性。你可以在这里找到它:http://www.osadl.org/projects/downloads/UIO/user/。使用 lsuio,你可以快速检查你的内核模块是否已加载以及它导出了哪些属性。详细信息请参阅 man 手册。
lsuio 的源代码可以作为获取有关 UIO 设备信息的示例。文件 uio_helper.c 包含了许多函数,你可以在你的用户空间驱动程序代码中使用。
mmap() 设备内存
在确保你拥有所需的内存映射和大小的正确设备后,你只需要调用 mmap() 将设备的内存映射到用户空间。
mmap() 调用的偏移量对于 UIO 设备具有特殊意义:它用于选择你想要映射的设备的哪个映射。要映射第 N 个映射的内存,你必须使用 N 倍的页面大小作为你的偏移量:
offset = N * getpagesize();
N 从零开始,所以如果你只有一个要映射的内存范围,设置 offset = 0。这种技术的一个缺点是内存总是从其起始地址开始映射。
等待中断
在成功映射设备内存后,你可以像访问普通数组一样访问它。通常,你会执行一些初始化。之后,你的硬件开始工作,并且一旦完成、有可用数据或需要你的注意,就会生成中断。
/dev/uioX 是一个只读文件。read() 将一直阻塞,直到发生中断。read() 的 count 参数只有一个合法值,即有符号 32 位整数的大小(4)。count 的任何其他值都会导致 read() 失败。有符号 32 位整数的读取是你的设备的中断计数。如果该值比你上次读取的值多一,那么一切正常。如果差值大于一,你错过了中断。
你也可以在 /dev/uioX 上使用 select()。
通用 PCI UIO 驱动
通用驱动是一个名为 uio_pci_generic 的内核模块。它可以与任何符合 PCI 2.3(大约在 2002 年发布)和任何符合 PCI Express 设备的设备一起工作。使用这个驱动,你只需要编写用户空间驱动程序,而不需要编写特定硬件的内核模块。
使驱动程序识别设备
由于该驱动程序没有声明任何设备 ID,它不会自动加载,也不会自动绑定到任何设备,你必须自己加载它并为驱动程序分配 ID。例如:
modprobe uio_pci_generic echo "8086 10f5" > /sys/bus/pci/drivers/uio_pci_generic/new_id
如果你的设备已经有一个特定的硬件驱动程序,通用驱动程序仍然不会绑定到它。在这种情况下,如果你想使用通用驱动程序(为什么要这样做?),你将不得不手动解绑硬件特定的驱动程序并绑定通用驱动程序,像这样:
echo -n 0000:00:19.0 > /sys/bus/pci/drivers/e1000e/unbind echo -n 0000:00:19.0 > /sys/bus/pci/drivers/uio_pci_generic/bind
你可以通过在 sysfs 中查找设备来验证设备是否已绑定到驱动程序,例如像下面这样:
ls -l /sys/bus/pci/devices/0000:00:19.0/driver
如果成功的话,应该会打印:
.../0000:00:19.0/driver -> ../../../bus/pci/drivers/uio_pci_generic
请注意,通用驱动程序不会绑定到旧的 PCI 2.2 设备。如果绑定设备失败,请运行以下命令:
dmesg
并查看输出以获取失败原因。
关于 uio_pci_generic 的一些要点
中断是使用 PCI 命令寄存器中的中断禁用位和 PCI 状态寄存器中的中断状态位来处理的。所有符合 PCI 2.3(大约在 2002 年发布)和所有符合 PCI Express 设备都应该支持这些位。uio_pci_generic 检测到这种支持,并不会绑定到不支持命令寄存器中的中断禁用位的设备。
在每次中断时,uio_pci_generic 设置中断禁用位。这可以防止设备在清除该位之前生成更多的中断。用户空间驱动程序在阻塞和等待更多中断之前应该清除这个位。
使用 uio_pci_generic 编写用户空间驱动程序
用户空间驱动程序可以使用 PCI sysfs 接口,或者包装它的 libpci 库来与设备通信,并通过写入命令寄存器来重新启用中断。
使用 uio_pci_generic 的示例代码
以下是使用 uio_pci_generic 的一些示例用户空间驱动程序代码:
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> int main() { int uiofd; int configfd; int err; int i; unsigned icount; unsigned char command_high; uiofd = open("/dev/uio0", O_RDONLY); if (uiofd < 0) { perror("uio open:"); return errno; } configfd = open("/sys/class/uio/uio0/device/config", O_RDWR); if (configfd < 0) { perror("config open:"); return errno; } /* 读取并缓存命令值 */ err = pread(configfd, &command_high, 1, 5); if (err != 1) { perror("command config read:"); return errno; } command_high &= ~0x4; for(i = 0;; ++i) { /* 打印一条消息,用于调试。 */ if (i == 0) fprintf(stderr, "Started uio test driver.\n"); else fprintf(stderr, "Interrupts: %d\n", icount); /****************************************/ /* 在这里我们从设备得到了一个中断。 对它进行一些操作。 */ /****************************************/ /* 重新启用中断。 */ err = pwrite(configfd, &command_high, 1, 5); if (err != 1) { perror("config write:"); break; } /* 等待下一个中断。 */ err = read(uiofd, &icount, 4); if (err != 4) { perror("uio read:"); break; } } return errno; }
通用 Hyper-V UIO 驱动
通用驱动是一个名为 uio_hv_generic 的内核模块。它支持 Hyper-V VMBus 上的设备,类似于 PCI 总线上的 uio_pci_generic。
使驱动程序识别设备
由于该驱动程序没有声明任何设备 GUID,它不会自动加载,也不会自动绑定到任何设备,你必须自己加载它并为驱动程序分配 ID。例如,要使用网络设备类 GUID:
modprobe uio_hv_generic echo "f8615163-df3e-46c5-913f-f2d2f965ed0e" > /sys/bus/vmbus/drivers/uio_hv_generic/new_id
如果你的设备已经有一个特定的硬件驱动程序,通用驱动程序仍然不会绑定到它。在这种情况下,如果你想为用户空间库使用通用驱动程序,你将不得不手动解绑硬件特定的驱动程序并绑定通用驱动程序,使用设备特定的 GUID,像这样:
echo -n ed963694-e847-4b2a-85af-bc9cfc11d6f3 > /sys/bus/vmbus/drivers/hv_netvsc/unbind echo -n ed963694-e847-4b2a-85af-bc9cfc11d6f3 > /sys/bus/vmbus/drivers/uio_hv_generic/bind
你可以通过在 sysfs 中查找设备来验证设备是否已绑定到驱动程序,例如像下面这样:
ls -l /sys/bus/vmbus/devices/ed963694-e847-4b2a-85af-bc9cfc11d6f3/driver
如果成功的话,应该会打印:
.../ed963694-e847-4b2a-85af-bc9cfc11d6f3/driver -> ../../../bus/vmbus/drivers/uio_hv_generic
关于 uio_hv_generic 的一些要点
在每次中断时,uio_hv_generic 设置中断禁用位。这可以防止设备在清除该位之前生成更多的中断。用户空间驱动程序在阻塞和等待更多中断之前应该清除这个位。
当主机撤销设备时,中断文件描述符会被标记为关闭,对中断文件描述符的任何读取将返回 -EIO。类似于关闭的套接字或断开的串行设备。
VMBus 设备区域被映射到 uio 设备资源中:
- 通道环形缓冲区:从客户机到主机和从主机到客户机
- 从客户机到主机的中断信令页
- 从客户机到主机的监视页
- 网络接收缓冲区
- 网络发送缓冲区
如果通过对主机的请求创建了一个子通道,那么 uio_hv_generic 设备驱动程序将为每个通道环形缓冲区创建一个 sysfs 二进制文件。例如:
/sys/bus/vmbus/devices/3811fe4d-0fa0-4b62-981a-74fc1084c757/channels/21/ring
更多信息