VFIO 的 API 有个特点:没有独立的库函数,一切通过 ioctl 完成 (外加 open/close/mmap/pread/pwrite 这类基础系统调用)。所以"函数列表"实际上是三张 ioctl 命令表,按 container → group → device 三层对象组织。下面按学习顺序梳理。
一、总体模型:三个对象,一套 ioctl
/dev/vfio/vfio ──open()──► container fd (IOMMU 域容器,DMA 映射挂这)
/dev/vfio/N ──open()──► group fd (IOMMU 组,设备必须按组隔离)
└─ ioctl 拿到 ──► device fd (具体设备,BAR/中断/配置空间走这)
头文件:<linux/vfio.h>(Type1 IOMMU 的结构体也在其中,老内核在 <linux/vfio_iommu_type1.h>)。
二、Container 级 ioctl
| 命令 | 参数结构体 | 作用 |
|---|---|---|
VFIO_GET_API_VERSION |
无(返回值) | 返回内核 VFIO API 版本,应等于 VFIO_API_VERSION |
VFIO_CHECK_EXTENSION |
int(扩展类型) | 检查是否支持某 IOMMU 模型:VFIO_TYPE1_IOMMU / VFIO_TYPE1v2_IOMMU / VFIO_NOIOMMU_IOMMU |
VFIO_SET_IOMMU |
int(模型类型) | 绑定 IOMMU 模型。必须先挂至少一个 group,否则 EINVAL |
VFIO_IOMMU_GET_INFO |
vfio_iommu_type1_info |
查询页大小掩码、可用 IOVA 窗口(5.4+ 通过 cap 扩展) |
VFIO_IOMMU_MAP_DMA |
vfio_iommu_type1_dma_map |
建立 iova→物理页映射(你之前那段代码) |
VFIO_IOMMU_UNMAP_DMA |
vfio_iommu_type1_dma_unmap |
撤销映射,返回值里带实际解除的 unmapped 大小 |
| 关键结构体: |
c
struct vfio_iommu_type1_dma_map {
__u32 argsz;
__u32 flags; // VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE
__u64 vaddr; // 进程虚拟地址
__u64 iova; // 设备侧地址
__u64 size;
};
struct vfio_iommu_type1_dma_unmap {
__u32 argsz;
__u32 flags;
__u64 iova;
__u64 size;
__u64 unmapped; // 输出:实际解除的字节数
};
三、Group 级 ioctl
| 命令 | 参数 | 作用 |
|---|---|---|
VFIO_GROUP_GET_STATUS |
vfio_group_status |
查询组是否可用(VFIO_GROUP_FLAGS_VIABLE)及是否已挂容器 |
VFIO_GROUP_SET_CONTAINER |
int container_fd |
把组加入容器 |
VFIO_GROUP_UNSET_CONTAINER |
int container_fd |
从容器移除 |
VFIO_GROUP_GET_DEVICE_FD |
char *设备名 |
核心命令 :按设备名(如 "0000:01:00.0")拿到 device fd |
四、Device 级 ioctl
| 命令 | 结构体 | 作用 |
|---|---|---|
VFIO_DEVICE_GET_INFO |
vfio_device_info |
设备类型标志、region 数、IRQ 数 |
VFIO_DEVICE_GET_REGION_INFO |
vfio_region_info |
查每个 region 的 offset/size/flags(BAR、配置空间都算 region) |
VFIO_DEVICE_GET_IRQ_INFO |
vfio_irq_info |
查每个 IRQ index 的向量数(如 MSI-X 有多少条) |
VFIO_DEVICE_SET_IRQS |
vfio_irq_set |
配置中断:绑 eventfd、mask/unmask |
VFIO_DEVICE_RESET |
无 | 设备复位(PCI FLR),初始化前建议做一次 |
VFIO_DEVICE_GET_PCI_HOT_RESET_INFO / VFIO_DEVICE_PCI_HOT_RESET |
vfio_pci_hot_reset(_info) |
PCI 热复位(涉及同组设备时用) |
| 关键结构体: |
c
struct vfio_device_info {
__u32 argsz;
__u32 flags; // VFIO_DEVICE_FLAGS_PCI | VFIO_DEVICE_FLAGS_RESET
__u32 num_regions; // PCI 设备为 9(BAR0-5、ROM、CONFIG、VGA)
__u32 num_irqs; // PCI 设备为 5(INTX/MSI/MSIX/ERR/REQ)
};
struct vfio_region_info {
__u32 argsz;
__u32 flags; // READ/WRITE/MMAP
__u32 index;
__u32 cap_offset; // 稀疏 mmap 等扩展
__u64 size;
__u64 offset; // 在 device fd 上的偏移,pread/mmap 都用它
};
PCI 设备的 region/IRQ 索引宏:
c
VFIO_PCI_BAR0_REGION_INDEX ~ VFIO_PCI_BAR5_REGION_INDEX // 0~5
VFIO_PCI_ROM_REGION_INDEX // 6
VFIO_PCI_CONFIG_REGION_INDEX // 7
VFIO_PCI_INTX_IRQ_INDEX, MSI, MSIX, ERR, REQ // IRQ: 0~4
五、两个高频操作:访问 BAR 和收中断
1. 映射 BAR0 到用户态(访问设备寄存器)
c
struct vfio_region_info reg = {
.argsz = sizeof(reg),
.index = VFIO_PCI_BAR0_REGION_INDEX,
};
ioctl(dev_fd, VFIO_DEVICE_GET_REGION_INFO, ®);
// MMIO 寄存器:mmap 后直接读写
void *bar0 = mmap(NULL, reg.size, PROT_READ | PROT_WRITE,
MAP_SHARED, dev_fd, reg.offset);
// 配置空间等慢速访问:用 pread/pwrite(注意 offset 是 reg.offset + 寄存器偏移)
uint16_t vendor_id;
pread(dev_fd, &vendor_id, 2, reg_cfg.offset + 0x00);
2. 绑定 MSI-X 中断到 eventfd
c
int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
struct vfio_irq_set *irq_set = calloc(1, sizeof(*irq_set) + sizeof(int));
irq_set->argsz = sizeof(*irq_set) + sizeof(int);
irq_set->flags = VFIO_IRQ_SET_DATA_EVENTFD | VFIO_IRQ_SET_ACTION_TRIGGER;
irq_set->index = VFIO_PCI_MSIX_IRQ_INDEX;
irq_set->start = 0; // 从第 0 个向量开始
irq_set->count = 1; // 绑 1 个向量
*(int *)irq_set->data = efd; // data 区就是 fd 数组
ioctl(dev_fd, VFIO_DEVICE_SET_IRQS, irq_set);
// 之后 epoll/read(efd) 即可收到中断通知
六、完整初始化序列(把所有命令串起来)
c
struct vfio_dev {
int container_fd, group_fd, device_fd;
};
int vfio_init(struct vfio_dev *d, int group_id, const char *dev_name)
{
// 1. 打开 container
d->container_fd = open("/dev/vfio/vfio", O_RDWR);
// 2. 验证 API 版本和 IOMMU 模型
if (ioctl(d->container_fd, VFIO_GET_API_VERSION) != VFIO_API_VERSION)
return -1;
if (!ioctl(d->container_fd, VFIO_CHECK_EXTENSION, VFIO_TYPE1v2_IOMMU))
return -1;
// 3. 打开 group(如 /dev/vfio/42)
char path[64];
snprintf(path, sizeof(path), "/dev/vfio/%d", group_id);
d->group_fd = open(path, O_RDWR);
// 4. 组挂入容器(此时才允许 SET_IOMMU)
ioctl(d->group_fd, VFIO_GROUP_SET_CONTAINER, &d->container_fd);
ioctl(d->container_fd, VFIO_SET_IOMMU, VFIO_TYPE1v2_IOMMU);
// 5. 拿设备 fd
d->device_fd = ioctl(d->group_fd, VFIO_GROUP_GET_DEVICE_FD, dev_name);
// 6. 复位设备
ioctl(d->device_fd, VFIO_DEVICE_RESET);
return 0;
}
// 之后:mmap 大页 → VFIO_IOMMU_MAP_DMA(你已有的代码)
// → GET_REGION_INFO + mmap BAR → SET_IRQS → 驱动运行
七、系统侧准备(API 之外的事)
bash
# 1. 开 IOMMU(Intel 需要内核参数;AMD 默认开)
# grub: intel_iommu=on iommu=pt
# 2. 确认设备属于哪个组
readlink /sys/bus/pci/devices/0000:01:00.0/iommu_group
# 3. 组内所有设备必须都从原驱动解绑(否则组不可用)
echo 0000:01:00.0 > /sys/bus/pci/drivers/ixgbe/unbind
# 4. 绑到 vfio-pci
echo vfio-pci > /sys/bus/pci/devices/0000:01:00.0/driver_override
echo 0000:01:00.0 > /sys/bus/pci/drivers/vfio-pci/bind
# 5. 权限(非 root 跑时)
chmod 666 /dev/vfio/vfio /dev/vfio/42
八、常见错误速查
| 报错位置 | errno | 原因 |
|---|---|---|
open("/dev/vfio/N") |
ENOENT | IOMMU 没开启,组不存在 |
VFIO_GROUP_SET_CONTAINER |
EINVAL | container fd 无效 |
VFIO_SET_IOMMU |
EINVAL | 没先挂任何 group,或模型不支持 |
VFIO_GROUP_GET_DEVICE_FD |
ENODEV | 设备不在该组 / 没绑到 vfio-pci |
| 打开 group 后状态不 viable | --- | 组内还有设备被原驱动占用(最常见!) |
VFIO_IOMMU_MAP_DMA |
EINVAL | iova 区间与已有映射重叠 |
VFIO_IOMMU_MAP_DMA |
ENOMEM | pin 物理页失败(内存不足/ulimit -l 太小) |
九、进阶方向(了解即可)
- No-IOMMU 模式 :
VFIO_NOIOMMU_IOMMU,无 IOMMU 平台的逃生通道,不安全(设备可 DMA 任意物理内存),需内核参数enable_unsafe_noiommu_mode=1。此模式下用不了 MAP_DMA,回到virt_to_phys路线。 - 脏页跟踪、热迁移 :
VFIO_IOMMU_DIRTY_PAGES等扩展,虚拟化场景用。 - iommufd(内核 6.0+) :新的
/dev/iommu框架逐步替代 Type1 后端,接口变了(VFIO_DEVICE_BIND_IOMMUFD),但目前主流发行版和 DPDK 仍以 Type1 为主。 - 不想手写 :DPDK 的
rte_vfio_container_dma_map()/rte_vfio_get_group_num()等封装就是上面这套流程的成熟实现,源码在lib/eal/linux/eal_vfio.c,是最好的学习范例。
学习路径建议
按你已有的代码进度,下一步依次是:GET_REGION_INFO + mmap BAR (能读写寄存器)→ SET_IRQS 绑 eventfd (能收中断)→ 用寄存器/描述符环驱动设备。对照第六节序列,你当前处于第 5 步之后,MAP_DMA 已就位------剩下的就是设备本身的协议了。