返回

SLES PCI BAR mmap失败?原因与UIO/VFIO解决方案

Linux

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 这两个系统之间存在差异的地方:

  1. 内核版本的差异和安全策略收紧: SLES 15 通常搭载比 RHEL 7.2 新得多的 Linux 内核。新内核往往会引入更严格的安全检查和资源访问控制机制。直接通过 /sysfs 下的 resourceX 文件来 mmap PCI BAR 空间,可能在新内核里被认为是一种不安全或者不推荐的做法,干脆就被禁用了。/sysfs 主要是用来展示设备信息和调整某些参数的,把它直接当成内存设备文件来 mmap 并不是它的设计初衷,尤其是对于 MMIO (Memory-Mapped I/O) 区域。内核开发者可能觉得,有更安全的接口(比如 UIO 或 VFIO)可以用,就没必要开放这条“捷径”了。

  2. resourceX 文件的特殊性: 这些 /sysfs 下的 resourceX 文件并非普通文件。虽然 fstat 能报告一个大小,但这并不意味着内核驱动就一定实现了对它的 mmap 操作支持。驱动程序的 mmap 文件操作接口 (file_operations->mmap) 可能没有实现,或者在 SLES 的特定内核配置下,该实现直接返回了 -EINVAL

  3. 内核配置选项: 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 框架确保了这种映射是受控且相对安全的。

怎么操作?

  1. 确认 UIO 模块加载:

    lsmod | grep uio
    

    你可能需要看到 uio 和一个具体的 UIO 驱动,比如 uio_pci_generic。如果没有,需要加载它:

    sudo modprobe uio_pci_generic
    
  2. 解绑设备原有驱动(如果需要):
    你的 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
      
  3. 绑定到 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 信息。

  4. 找到 /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 ,要看具体实现)。
  5. 修改代码,使用 /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 文件,并指定那个物理地址作为偏移量,来间接访问设备内存。

怎么操作?

  1. 获取 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 字节。

  2. 打开 /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;
    }
    
  3. 计算 mmap 参数并执行映射:

    • mmapoffset 参数必须是系统页面大小(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 复杂不少:

  1. 确认 IOMMU 启用:

    • 在 BIOS/UEFI 中启用 Intel VT-d 或 AMD IOMMU。
    • 内核启动参数需要包含 intel_iommu=onamd_iommu=on。检查 dmesg | grep -e DMAR -e IOMMU 是否有相关信息。
  2. 加载 vfio-pci 模块:

    sudo modprobe vfio-pci
    
  3. 解绑并绑定设备到 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) 检查(有安全风险!)。

  4. 通过 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, &region_info) (需要指定 region_info.index,例如 VFIO_PCI_BAR0_REGION_INDEX
    • 执行 mmap :关键在于 mmapoffset 参数。在 VFIO 里,这个 offset 通常用来编码你要映射的区域索引 (region index) ,而不是物理地址。region_info 里面会告诉你该区域的 offset 和 size。mmapfddevice_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, &reg_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 是目前公认最安全的用户态设备访问方式,得益于 IOMMU 的硬件隔离。
  • 同样需要 root 权限来操作驱动绑定和 VFIO 设备文件。可以通过 udev 规则管理 /dev/vfio/ 下文件的权限。
  • 正确配置 IOMMU 和处理 IOMMU Group 非常重要。

进阶使用:

  • VFIO 支持复杂的设备模型,包括中断处理(通过 eventfd)、DMA 内存管理(用户态分配内存,通过 VFIO 映射给设备使用)、模拟 PCI 配置空间访问等。
  • 是实现高性能用户态网络驱动(如 DPDK)或将硬件直通给虚拟机的基石。

如何选择?

碰到 SLES 上 mmap /sysfs/.../resourceX 失效的情况:

  1. 首选 UIO 或 VFIO。 这两个是 Linux 内核推荐的、更现代、更健壮的用户态 I/O 框架。

    • 如果你的需求相对简单,主要是内存映射和可能的中断处理,UIO(特别是 uio_pci_generic)是个不错的起点 ,相对容易上手。
    • 如果追求最高的安全性 (IOMMU 隔离),或者需要处理复杂的设备交互、DMA 操作,或者你本身就在做虚拟化相关的工作,那么 VFIO 是更好的选择 ,虽然学习曲线陡峭一些。
  2. /dev/mem 应该作为最后的手段,并且尽可能避免。 它的风险太高,而且很可能在新系统上被内核配置所限制。如果不得不考虑它,务必万分小心,并充分了解 CONFIG_STRICT_DEVMEM 的影响。

所以,遇到 SLES 15 (或其他新内核系统) 上 mmap PCI BAR 报 EINVAL 的问题,别死磕 /sysfs/../resourceX 了,试试 UIO 或者 VFIO 吧,大概率能解决问题,而且写出的代码也会更规范、更安全。