SLES PCI BAR mmap失败?原因与UIO/VFIO解决方案
2025-04-01 10:14:58
SLES 下 PCI BAR mmap 失败?别慌,原因和解法都在这儿
搞底层硬件开发,特别是跟 PCI 设备打交道的时候,内存映射(mmap
)是个家常便饭的操作。通过 mmap
把设备的 BAR (Base Address Register) 空间映射到用户态,就能像读写内存一样直接跟硬件寄存器交互,效率高,用起来也方便。
可有时候,同一段在别的 Linux 发行版上跑得好好的代码,挪到 SUSE Linux Enterprise Server (SLES) 上就翻车了。就像下面这位朋友遇到的情况:
问题来了:SLES 上的 mmap 为啥报错 EINVAL?
来看下这段在 SLES 15 上碰壁的代码片段:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
int main() {
char csr_bar_path[256];
int csr_fd;
struct stat sb;
unsigned char *csr_bar;
// 假设目标设备是 0000:16:00.0,我们要映射 resource2 (通常是 CSR BAR)
snprintf(csr_bar_path, sizeof(csr_bar_path), "/sys/bus/pci/devices/0000:16:00.0/resource2");
csr_fd = open(csr_bar_path, O_RDWR | O_SYNC);
if (csr_fd < 0) {
perror("Cannot open CSR bar file");
return 1;
}
if (fstat(csr_fd, &sb) == -1) {
perror("fstat failed");
close(csr_fd);
return 1;
}
printf("CSR file size = %ld bytes\n", sb.st_size); // 输出获取到的文件大小
// 关键的 mmap 调用
csr_bar = (unsigned char *)mmap(NULL, // 地址,NULL 表示让内核选
sb.st_size, // 映射大小,从 fstat 获取
PROT_READ | PROT_WRITE, // 权限:可读可写
MAP_SHARED, // 共享映射,对内存的修改会写回文件(这里是设备)
csr_fd, // 文件符
0); // 文件内偏移量
if (csr_bar == MAP_FAILED) { // 注意:mmap 失败返回 MAP_FAILED,不是 -1
printf("mmap failed for CSR bar: %s (errno %d)\n", strerror(errno), errno);
close(csr_fd);
return 1;
}
printf("mmap successful!\n");
// 在这里可以访问 csr_bar 指针来读写设备寄存器...
// 比如: csr_bar[0x10] = 0x5A;
// 清理
munmap(csr_bar, sb.st_size);
close(csr_fd);
return 0;
}
代码逻辑挺清晰:打开 /sys/bus/pci/devices/.../resourceX
这个特殊文件,获取它的大小(代表 BAR 空间的大小,这里是 65536 字节,也就是 64KB),然后调用 mmap
进行映射。
问题是,在 SLES 15 上,mmap
调用失败了,返回 MAP_FAILED
,并且 errno
被设置为 EINVAL
(Invalid argument,无效参数)。怪事儿是,这段代码在 RHEL 7.2 上跑得欢呢。fstat
能正确读出大小(64KB),说明文件路径和权限看起来没啥问题。那这个 EINVAL
到底是哪儿来的?
刨根问底:为啥 SLES 不让你 mmap PCI BAR 了?
mmap
返回 EINVAL
通常暗示传入的参数有问题。我们来捋一捋 mmap
的参数:addr
(NULL,没问题),length
(sb.st_size,看起来也合理),prot
(读写权限,标准用法),flags
(MAP_SHARED,也标准),fd
(成功打开的文件符),offset
(0,从头开始映射,也合理)。
参数本身好像挑不出毛病。那问题可能出在更深层次的地方,尤其是在 SLES 15 和 RHEL 7.2 这两个系统之间存在差异的地方:
-
内核版本的差异和安全策略收紧: SLES 15 通常搭载比 RHEL 7.2 新得多的 Linux 内核。新内核往往会引入更严格的安全检查和资源访问控制机制。直接通过
/sysfs
下的resourceX
文件来mmap
PCI BAR 空间,可能在新内核里被认为是一种不安全或者不推荐的做法,干脆就被禁用了。/sysfs
主要是用来展示设备信息和调整某些参数的,把它直接当成内存设备文件来mmap
并不是它的设计初衷,尤其是对于 MMIO (Memory-Mapped I/O) 区域。内核开发者可能觉得,有更安全的接口(比如 UIO 或 VFIO)可以用,就没必要开放这条“捷径”了。 -
resourceX
文件的特殊性: 这些/sysfs
下的resourceX
文件并非普通文件。虽然fstat
能报告一个大小,但这并不意味着内核驱动就一定实现了对它的mmap
操作支持。驱动程序的mmap
文件操作接口 (file_operations->mmap
) 可能没有实现,或者在 SLES 的特定内核配置下,该实现直接返回了-EINVAL
。 -
内核配置选项: SLES 15 的内核编译时可能启用了某些特定的安全配置(比如
CONFIG_STRICT_DEVMEM
的变种或相关的 PCI 子系统约束),限制了对物理内存或设备内存区域的直接用户态映射。虽然CONFIG_STRICT_DEVMEM
主要影响/dev/mem
,但类似的哲学也可能影响到对/sysfs
资源文件的处理。
简单说,SLES 15 很可能觉得你这种直接 mmap /sysfs/../resourceX
的搞法“不够规矩”,不让你玩儿了。它希望你用更现代、更安全的方式去访问硬件。
动手解决:几种绕过或修复 mmap 失败的方法
既然此路不通,我们就换条道走。有几种常用的方法可以在用户态访问 PCI 设备 BAR 空间,尤其是在新版内核上:
方法一:拥抱 UIO (Userspace I/O)
UIO 是 Linux 内核提供的一个轻量级框架,专门用来帮助在用户空间编写驱动程序。它允许用户态程序处理设备中断、映射设备的内存空间。用 UIO 是目前比较推荐的标准做法之一。
原理是啥?
UIO 会为你的 PCI 设备创建一个对应的字符设备文件,通常是 /dev/uioX
(X 是个数字)。这个 uioX
设备文件就代表了你的硬件。UIO 驱动负责处理底层的设备发现、资源分配等,然后把设备的操作接口(比如内存映射、中断等待)暴露给用户态。你 mmap
的目标不再是 /sysfs
下的 resourceX
文件,而是这个 /dev/uioX
文件。UIO 框架确保了这种映射是受控且相对安全的。
怎么操作?
-
确认 UIO 模块加载:
lsmod | grep uio
你可能需要看到
uio
和一个具体的 UIO 驱动,比如uio_pci_generic
。如果没有,需要加载它:sudo modprobe uio_pci_generic
-
解绑设备原有驱动(如果需要):
你的 PCI 设备可能已经被内核自带的驱动占用了(比如网卡驱动ixgbe
,或者通用pci-stub
)。你需要先把它解绑,才能让uio_pci_generic
接管。- 找到设备的总线地址(比如
0000:16:00.0
)。 - 找到它当前绑定的驱动:
lspci -k -s 0000:16:00.0
会显示Kernel driver in use: xxx
。 - 解绑:
# 注意替换成你的设备地址和驱动名 echo "0000:16:00.0" | sudo tee /sys/bus/pci/drivers/xxx/unbind
- 找到设备的总线地址(比如
-
绑定到
uio_pci_generic
:
你需要告诉uio_pci_generic
驱动去管理你的设备。这通常通过写入设备的 Vendor ID 和 Device ID 到驱动的new_id
文件来完成。- 先用
lspci -n -s 0000:16:00.0
查到设备的 Vendor ID 和 Device ID(比如8086 1533
)。 - 绑定设备:
# 注意替换成你的 Vendor ID 和 Device ID echo "8086 1533" | sudo tee /sys/bus/pci/drivers/uio_pci_generic/new_id
如果成功,系统里应该会出现一个新的
/dev/uioX
设备。同时,检查/sys/bus/pci/devices/0000:16:00.0/uio/
目录,确认 UIO 信息。 - 先用
-
找到
/dev/uioX
和 BAR 映射信息:
绑定成功后,可以通过/sys/class/uio/
目录下找到对应的uioX
。里面会有maps/mapY
子目录(Y 通常从 0 开始),每个mapY
代表一个 BAR 空间。- 查看
cat /sys/class/uio/uioX/maps/mapY/name
来确认是哪个 BAR。 - 查看
cat /sys/class/uio/uioX/maps/mapY/addr
(物理地址,可能用不到)。 - 查看
cat /sys/class/uio/uioX/maps/mapY/size
(映射大小,这个很重要 )。 - 查看
cat /sys/class/uio/uioX/maps/mapY/offset
(在设备文件中的偏移,通常 mmap 时直接用这个值,但也经常是 0 ,要看具体实现)。
- 查看
-
修改代码,使用
/dev/uioX
进行 mmap:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <errno.h> // 可能需要读取 /sys 下的文件来获取 size 和 offset #include <sys/stat.h> // 为了 fstat 替代方案 // 辅助函数:从 /sys 文件读取数字 long get_sysfs_long(const char *path) { FILE *fp = fopen(path, "r"); if (!fp) return -1; long val; int ret = fscanf(fp, "%li", &val); fclose(fp); return (ret == 1) ? val : -1; } int main() { // 假设设备绑定后是 /dev/uio0,我们要映射 map0 (对应 resource2) const char *uio_dev_path = "/dev/uio0"; const char *uio_map_size_path = "/sys/class/uio/uio0/maps/map0/size"; const char *uio_map_offset_path = "/sys/class/uio/uio0/maps/map0/offset"; // 有些驱动 offset 不一定是 0 int uio_fd; unsigned char *bar_ptr; long map_size; long map_offset; // mmap 的 offset 参数 map_size = get_sysfs_long(uio_map_size_path); if (map_size <= 0) { fprintf(stderr, "Failed to read map size from %s\n", uio_map_size_path); return 1; } // 注意:UIO mmap offset 通常指的是第几个 page,不是字节偏移! // UIO 标准做法是 offset = map_index * PAGE_SIZE // 如果 /sys/class/uio/uioX/maps/mapY/offset 文件存在且非0, mmap offset 可能就是那个值 (需要验证具体驱动) // 对于 uio_pci_generic,通常一个 map 对应一个 BAR,映射时 offset 参数设为 0 即可映射整个 BAR. // 更有保障的做法是确认你要映射的是哪个map (比如map0), 然后 offset = 0 * PAGE_SIZE (或者就是0) // 如果一个 UIO 设备暴露了多个 map,要映射 map1,则 offset = 1 * sysconf(_SC_PAGE_SIZE) map_offset = 0; // 假设我们要映射的是 map0 printf("UIO device: %s, Map index: %ld, Size: %ld, Offset for mmap: %ld\n", uio_dev_path, map_offset / sysconf(_SC_PAGE_SIZE), map_size, map_offset); uio_fd = open(uio_dev_path, O_RDWR | O_SYNC); if (uio_fd < 0) { perror("Cannot open UIO device file"); return 1; } bar_ptr = (unsigned char *)mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, uio_fd, map_offset); // 使用从 UIO 信息获取的偏移 if (bar_ptr == MAP_FAILED) { printf("mmap failed for UIO device: %s (errno %d)\n", strerror(errno), errno); close(uio_fd); return 1; } printf("UIO mmap successful!\n"); // ... 访问 bar_ptr ... munmap(bar_ptr, map_size); close(uio_fd); return 0; }
安全建议:
- 使用 UIO 通常需要 root 权限来加载模块、绑定/解绑驱动。
- 可以通过配置
udev
规则来调整/dev/uioX
文件的权限和属主,允许特定用户或用户组访问,避免一直用 root。 uio_pci_generic
比较简单,如果你的设备需要复杂的初始化序列或中断处理逻辑,可能需要编写自己的 UIO 驱动。
进阶使用:
- UIO 不仅能映射内存,还能处理中断。用户态程序可以
read()
/dev/uioX
文件来等待中断信号。 - 为了系统启动时自动绑定,可以编写
udev
规则或使用内核模块参数 (uio_pci_generic.ids=vendor:device,...
)。
方法二:探索 /dev/mem
(谨慎使用!)
这是一种比较古老且“粗暴”的方法,直接打开 /dev/mem
这个特殊设备文件来访问物理内存。PCI BAR 空间本质上也是映射到物理地址空间的一段区域。
原理是啥?
/dev/mem
允许拥有足够权限的进程(通常是 root)直接读写物理内存地址。只要你知道 PCI BAR 对应的物理起始地址和大小,就可以通过 mmap
/dev/mem
文件,并指定那个物理地址作为偏移量,来间接访问设备内存。
怎么操作?
-
获取 BAR 的物理地址和大小:
/sys/bus/pci/devices/.../resourceX
文件本身的内容就是 BAR 的物理起始地址、结束地址和标志位。你可以读这个文件来获取信息。或者更简单地,看lspci -v -s 0000:16:00.0
的输出,里面会直接列出每个 BAR 的Memory at ... (physical)
地址和size=...
。假设
lspci -v
显示resource2
(BAR2) 的信息是:
Region 2: Memory at f7c00000 (64-bit, non-prefetchable) [size=64K]
那么物理起始地址是0xf7c00000
,大小是64 * 1024 = 65536
字节。 -
打开
/dev/mem
:mem_fd = open("/dev/mem", O_RDWR | O_SYNC); if (mem_fd < 0) { perror("Cannot open /dev/mem"); // 很可能是权限不够,或者 CONFIG_STRICT_DEVMEM 生效了 return 1; }
-
计算 mmap 参数并执行映射:
mmap
的offset
参数必须是系统页面大小(PAGE_SIZE
)的整数倍。物理地址0xf7c00000
可能正好是页对齐的,但如果不是,你需要:- 找到物理地址所在的页的起始地址:
page_base_addr = physical_addr & ~(PAGE_SIZE - 1)
- 计算页内偏移:
page_offset = physical_addr & (PAGE_SIZE - 1)
- 调整映射大小,确保覆盖整个 BAR 区域,并且映射的总长度是原大小加上页内偏移:
map_len = size + page_offset
(可能需要向上舍入到页面大小边界,不过对于设备内存,精确长度通常可以工作,只要基地址和偏移对就行)。
- 找到物理地址所在的页的起始地址:
- 在
mmap
调用中:fd
是/dev/mem
的文件描述符。offset
参数传入计算得到的page_base_addr
。
- 映射成功后,得到的虚拟地址指针
virt_addr
加上page_offset
才是 BAR 的实际起始虚拟地址:bar_ptr = virt_addr + page_offset
。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <errno.h> int main() { long physical_addr = 0xf7c00000; // 从 lspci 获取 long size = 65536; // 从 lspci 或 fstat 获取 int mem_fd; unsigned char *bar_ptr_raw; // mmap 直接返回的指针 unsigned char *bar_ptr; // 调整页偏移后的指针 long page_size = sysconf(_SC_PAGE_SIZE); long page_base_addr, page_offset; long map_len; // 计算页对齐基地址和页内偏移 page_base_addr = physical_addr & ~(page_size - 1); page_offset = physical_addr - page_base_addr; // 等价于 physical_addr & (page_size - 1) // 映射长度需要包含页内偏移, 覆盖到目标区域末尾 // 通常需要向上取整到页边界, 但对 /dev/mem MMIO, 精确长度+偏移 可能也行 // 简单起见,直接映射 size + page_offset,确保覆盖 map_len = size + page_offset; // 如果需要严格页对齐的长度(有些架构需要),可以向上取整 // map_len = ((size + page_offset + page_size - 1) / page_size) * page_size; printf("Physical Addr: 0x%lx, Size: %ld\n", physical_addr, size); printf("Page Size: %ld\n", page_size); printf("Page Base Addr: 0x%lx, Page Offset: %ld\n", page_base_addr, page_offset); printf("Mapping Length: %ld\n", map_len); mem_fd = open("/dev/mem", O_RDWR | O_SYNC); if (mem_fd < 0) { perror("Cannot open /dev/mem"); // 检查 CONFIG_STRICT_DEVMEM 是否启用 return 1; } bar_ptr_raw = (unsigned char *)mmap(NULL, map_len, // 调整后的映射长度 PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, page_base_addr); // 使用页对齐的物理地址作为偏移 if (bar_ptr_raw == MAP_FAILED) { printf("mmap failed for /dev/mem: %s (errno %d)\n", strerror(errno), errno); close(mem_fd); return 1; } // 最终的 BAR 访问指针 bar_ptr = bar_ptr_raw + page_offset; printf("/dev/mem mmap successful! Raw ptr: %p, Adjusted BAR ptr: %p\n", bar_ptr_raw, bar_ptr); // ... 使用 bar_ptr 访问设备寄存器,注意访问范围不要超过 size ... // 例如: bar_ptr[0x10] = 0x5A; munmap(bar_ptr_raw, map_len); close(mem_fd); return 0; }
安全建议(极其重要):
- 这是最不推荐的方法!
/dev/mem
太危险了。一点小小的计算错误或代码 bug,就可能往物理内存的任意位置写入数据,直接导致系统崩溃、数据损坏、硬件损坏! - 内核安全限制: 很多现代 Linux 发行版(包括 SLES)默认启用了
CONFIG_STRICT_DEVMEM
内核选项。这会严格限制对/dev/mem
的访问,通常只允许映射 BIOS、VGA 等少量“已知安全”的区域。尝试映射 PCI BAR 的物理地址很可能被拒绝(返回EPERM
或类似错误),即使你是 root。要绕过这个限制,你需要重新编译内核并禁用CONFIG_STRICT_DEVMEM
,这本身就是个不小的安全风险,而且维护麻烦。 - 绕过内存保护: 使用
/dev/mem
意味着你的程序完全绕过了操作系统的内存管理和保护机制。任何错误都可能造成灾难性后果。
除非你非常清楚自己在做什么,并且没有其他选择,否则尽量避免使用 /dev/mem
。
进阶使用:
- 访问 MMIO 内存时,CPU 缓存可能带来问题。直接读写可能操作的是 Cache 而不是设备寄存器。使用
/dev/mem
映射时,需要考虑如何让映射的内存区域变成非缓存的(uncached)。这可以通过mmap
时传递特殊 flag(但标准 mmap 没有直接控制 cache 的 flag),或者更底层地使用pgprot_noncached()
配合remap_pfn_range
(内核态)或者某些架构特定的mmap
技巧(用户态很难做到)。这也是 UIO/VFIO 框架帮你处理好的事情之一。
方法三:VFIO (Virtual Function I/O) 框架
VFIO 是比 UIO 更强大、更安全的框架。它最初是为 KVM 虚拟机提供安全的设备直通(pass-through)设计的,但也完全可以在宿主机上用于用户态驱动开发。
原理是啥?
VFIO 利用 IOMMU(Input/Output Memory Management Unit,也叫 VT-d 或 AMD-Vi)硬件来提供内存隔离和保护。当设备被绑定到 VFIO 驱动(vfio-pci
)时,VFIO 会为这个设备创建一个隔离的 IOMMU 域。用户态程序通过一系列 ioctl
调用与 VFIO 交互,获取设备信息、BAR 信息、设置中断等。内存映射也是通过 VFIO 的接口完成的,并且映射操作会受到 IOMMU 的监管。这意味着,即使用户态程序有 bug,也无法访问到它不该访问的内存区域,安全性大大提高。
怎么操作?
VFIO 的使用流程比 UIO 复杂不少:
-
确认 IOMMU 启用:
- 在 BIOS/UEFI 中启用 Intel VT-d 或 AMD IOMMU。
- 内核启动参数需要包含
intel_iommu=on
或amd_iommu=on
。检查dmesg | grep -e DMAR -e IOMMU
是否有相关信息。
-
加载
vfio-pci
模块:sudo modprobe vfio-pci
-
解绑并绑定设备到
vfio-pci
:
和 UIO 类似,先解绑原驱动,然后绑定到vfio-pci
:# 先找到设备所属的 IOMMU Group GROUP=$(readlink /sys/bus/pci/devices/0000\:16\:00.0/iommu_group | sed 's|.*/||') echo "Found IOMMU Group: $GROUP" # 检查 group 下设备是否已被其他驱动使用,如果被用需要先unbind lspci -k -s 0000:16:00.0 # 假设驱动是 'current_driver' echo "0000:16:00.0" | sudo tee /sys/bus/pci/devices/0000\:16\:00.0/driver/unbind # 绑定到 vfio-pci echo "vfio-pci" | sudo tee /sys/bus/pci/devices/0000\:16\:00.0/driver_override # 通过 vendor id, device id 绑定 (假设 Vendor 8086, Device 1533) # echo "8086 1533" | sudo tee /sys/bus/pci/drivers/vfio-pci/new_id # 或者用这种方式 echo "0000:16:00.0" | sudo tee /sys/bus/pci/drivers/vfio-pci/bind # 确认绑定成功, driver in use 应该是 vfio-pci lspci -k -s 0000:16:00.0
注意:绑定时可能会提示“device is part of an non-viable group”,这可能需要将 IOMMU Group 里的所有设备都解绑并绑定到
vfio-pci
,或者通过内核参数pcie_acs_override=downstream,multifunction
来放松 ACS (Access Control Services) 检查(有安全风险!)。 -
通过 VFIO API 访问设备:
- 打开 VFIO 容器文件:
container_fd = open("/dev/vfio/vfio", O_RDWR)
- 将 IOMMU Group 添加到容器:
ioctl(container_fd, VFIO_GROUP_SET_CONTAINER, &group_fd)
(先要打开/dev/vfio/GROUP
文件得到group_fd
) - 设置 IOMMU 类型(通常是 TYPE1):
ioctl(container_fd, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU)
- 获取设备文件描述符:
device_fd = ioctl(group_fd, VFIO_GROUP_GET_DEVICE_FD, "0000:16:00.0")
- 获取设备信息(区域数量等):
ioctl(device_fd, VFIO_DEVICE_GET_INFO, &device_info)
- 获取特定区域(BAR)的信息(大小、偏移量、flags):
ioctl(device_fd, VFIO_DEVICE_GET_REGION_INFO, ®ion_info)
(需要指定region_info.index
,例如VFIO_PCI_BAR0_REGION_INDEX
) - 执行 mmap :关键在于
mmap
的offset
参数。在 VFIO 里,这个 offset 通常用来编码你要映射的区域索引 (region index) ,而不是物理地址。region_info
里面会告诉你该区域的 offset 和 size。mmap
时fd
是device_fd
。
// 伪代码示意,VFIO ioctl 调用比较繁琐 struct vfio_region_info reg_info = { .argsz = sizeof(reg_info) }; reg_info.index = VFIO_PCI_BAR2_REGION_INDEX; // 假设 resource2 是 BAR2 if (ioctl(device_fd, VFIO_DEVICE_GET_REGION_INFO, ®_info) < 0) { perror("Failed to get region info"); // ... error handling ... } // 检查 reg_info.flags & VFIO_REGION_INFO_FLAG_MMAP 是否为真,表示可以 mmap if (!(reg_info.flags & VFIO_REGION_INFO_FLAG_MMAP)) { fprintf(stderr, "Region %d is not mmapable\n", reg_info.index); // ... error handling ... } printf("VFIO Region index: %d, Offset: 0x%llx, Size: 0x%llx\n", reg_info.index, reg_info.offset, reg_info.size); // mmap 时,fd 是 device_fd, offset 通常是 region_info.offset // (VFIO API 文档确认 offset 用法: The region's mappable offset will be stored in the offset field.) bar_ptr = (unsigned char *)mmap(NULL, reg_info.size, PROT_READ | PROT_WRITE, MAP_SHARED, device_fd, reg_info.offset); // 使用 VFIO 提供的 offset if (bar_ptr == MAP_FAILED) { printf("mmap failed for VFIO device: %s (errno %d)\n", strerror(errno), errno); // ... error handling ... } printf("VFIO mmap successful!\n"); // ... 使用 bar_ptr ... munmap(bar_ptr, reg_info.size); // ... 关闭各种 fd ...
- 打开 VFIO 容器文件:
安全建议:
- VFIO 是目前公认最安全的用户态设备访问方式,得益于 IOMMU 的硬件隔离。
- 同样需要 root 权限来操作驱动绑定和 VFIO 设备文件。可以通过
udev
规则管理/dev/vfio/
下文件的权限。 - 正确配置 IOMMU 和处理 IOMMU Group 非常重要。
进阶使用:
- VFIO 支持复杂的设备模型,包括中断处理(通过 eventfd)、DMA 内存管理(用户态分配内存,通过 VFIO 映射给设备使用)、模拟 PCI 配置空间访问等。
- 是实现高性能用户态网络驱动(如 DPDK)或将硬件直通给虚拟机的基石。
如何选择?
碰到 SLES 上 mmap /sysfs/.../resourceX
失效的情况:
-
首选 UIO 或 VFIO。 这两个是 Linux 内核推荐的、更现代、更健壮的用户态 I/O 框架。
- 如果你的需求相对简单,主要是内存映射和可能的中断处理,UIO(特别是
uio_pci_generic
)是个不错的起点 ,相对容易上手。 - 如果追求最高的安全性 (IOMMU 隔离),或者需要处理复杂的设备交互、DMA 操作,或者你本身就在做虚拟化相关的工作,那么 VFIO 是更好的选择 ,虽然学习曲线陡峭一些。
- 如果你的需求相对简单,主要是内存映射和可能的中断处理,UIO(特别是
-
/dev/mem
应该作为最后的手段,并且尽可能避免。 它的风险太高,而且很可能在新系统上被内核配置所限制。如果不得不考虑它,务必万分小心,并充分了解CONFIG_STRICT_DEVMEM
的影响。
所以,遇到 SLES 15 (或其他新内核系统) 上 mmap
PCI BAR 报 EINVAL
的问题,别死磕 /sysfs/../resourceX
了,试试 UIO 或者 VFIO 吧,大概率能解决问题,而且写出的代码也会更规范、更安全。