linux用戶層驅動--VFIO(四)

VFIO——將設備暴露到用戶態

在開始之前我們先要說一個東西就是 DMA,直接讓設備訪問內存,可以不通過 CPU 搬運數據。
在這裏插入圖片描述
這是一個比較簡單的體系結構圖,設備 和 CPU 通過存儲控制器訪問存儲器。一個簡單的 case 是 CPU 向存儲器寫數據,然後設備從存儲器讀數據。這麼快來一切都很正常。但是實際上 CPU 是有一層緩存的,例如下面這樣的。
在這裏插入圖片描述
CPU 想內存寫數據,但是先要清空到不一致的緩存,然後設備再去讀數據,不然設備讀到的數據和 CPU 實際的數據會不一致(因爲緩存裏的數據可能和存儲器的不一致),而且實際上緩存也不只是一層,所以需要一箇中間層來保證 從 CPU 的角度和從設備的角度內存都是一致的,所以就有了下面這個結構。
在這裏插入圖片描述
CPU 和 設備都會走緩存驗證一遍以後,再落到存儲器上,這樣帶上緩存以後大家的一致性都是一樣的了。所以從設備的角度,設備也擁有了緩存,實際上這個和 IOMMU 關係不是很大,接下來設備其實也可以和 CPU 一樣有一層 MMU,也就是地址到存儲器物理地址的轉換。注意,這裏我用了地址,因爲對 CPU 來說是虛擬地址,但是對設備來說是一個總線域的地址。這裏要明確區分一下,一個是總線地址,是從設備的角度來看的,一個是 CPU 的虛擬地址,這是從 CPU 角度來看的,兩個是不同的東西。將總線域地址轉換成存儲器物理地址的設備就叫 IOMMU。
在這裏插入圖片描述
如果沒有 IOMMU,DMA 也能照常工作,IOMMU 的主要作用就是保護功能,防止使用 DMA 的設備訪問任意存儲器的物理地址。

IOMMU 在不同架構上名字不太一樣,AMD 叫 AMD-Vi,最開始針對的設備只是顯卡,Intel 叫 VT-d,arm 叫 SMMU,具體對應的手冊也不太一樣,但是主要解決的問題是一致的。在 VTd 中,dmar (DMA remapping) 就是那個 IOMMU 設備,通過中斷的方式實現類似 page fault 一樣的內存分配行爲。DMA 傳輸是由 CPU 發起的:CPU 會告訴 DMA 控制器,幫忙將 xxx 地方的數據搬到 xxx 地方。CPU 發完指令之後,就當甩手掌櫃了。IOMMU 有點像 MMU 是一個將設備地址翻譯到內存地址的頁表體系,也會有對應的頁表,這個東西在虛擬化中也非常有用,可以將原本有軟件模擬的設備,用直接的硬件替代,而原本的隔離通過 IOMMU 來完成。如下圖所示,原本需要通過軟件模擬的驅動設備可以通過 IOMMU 以安全的方式來直接把硬件設備分配個用戶態的 Guest OS。
在這裏插入圖片描述
理論上講沒有 IOMMU 實際上是可以工作的,但是硬件的角度,設備就擁有了整個存儲器的全局視圖,這是無論如何都非常不合理的事情,不應該讓設備擁有訪問任意物理內存的能力。

這裏要提的另外一個功能就是對中斷的隔離,類似於下面的通過在中斷請求中添加標識來重定向中斷到對應的中斷回調上。
在這裏插入圖片描述
VFIO 的作用就是通過 IOMMU 以安全的方式來將設備的訪問直接暴露到用戶空間,而不用專門完成某個驅動等待合併到上游或者使用之前的對 IOMMU 沒有感知的 UIO 的框架。通過 VFIO 向用戶態開放 IOMMU 的功能,編寫用戶態的驅動。

對於 IOMMU 來說,隔離的級別不一定是單個設備,比如一個後面有幾個設備的 PCI 橋,從 PCI 橋角度來說,都是來自 PCI 橋的總線事務。所以 IOMMU 有一個 iommu_group的概念,代表一組與其他設備隔離的設備的集合。

IOMMU 根據手冊上講還有一個域的概念,可以簡單理解爲一段物理地址的抽象。

在 iommu_group的層級上,VFIO 封裝了一層 container class,這個的作用對應於希望能夠在不同的iommu_group 之間共享 TLB 和 page tables,這個就是一個集合的概念,跟容器的那個概念沒啥關係,一個集合總歸要有個名字。通過把 host 的 device 和 driver 解綁,然後綁定到 VFIO 的 driver 上,就會有個/dev/vfio/$GROUP/ 出現,然後這個 $GROUP代表的就是這個 device 的 iommu_group號,如果要使用 VFIO 就要把這個 group 下的所有 device 都解綁纔可以。

通過打開/dev/vfio/vfio就能創建一個 VFIO 的 container,然後再打開/dev/vfio/$GROUP用VFIO_GROUP_SET_CONTAINER ioctl 把文件描述傳進去,就把 group 加進去了,如果支持多個 group 共享頁表等結構,還可以把相應的 group 也加進去。(再強調一遍這個頁表是總線地址到存儲器物理地址,IOMMU 管理的那個頁表)。

下面舉個官方的栗子,獲取 PCI 設備 0000:06:0d.0 的 group_id (PCI 命名的規則是 domain🚌slot.func)

$ readlink /sys/bus/pci/devices/0000:06:0d.0/iommu_group
../../../../kernel/iommu_groups/26

使用之前需要你已經加載了 VFIO 模塊

modprobe vfio-pci

解綁 PCI 設備,然後創建一個 container id

$ lspci -n -s 0000:06:0d.0
06:0d.0 0401: 1102:0002 (rev 08)
# echo 0000:06:0d.0 > /sys/bus/pci/devices/0000:06:0d.0/driver/unbind
# echo 1102 0002 > /sys/bus/pci/drivers/vfio-pci/new_id

然後尋找其他同屬於一個 group 的設備

$ ls -l /sys/bus/pci/devices/0000:06:0d.0/iommu_group/devices
total 0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:00:1e.0 ->
	../../../../devices/pci0000:00/0000:00:1e.0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:06:0d.0 ->
	../../../../devices/pci0000:00/0000:00:1e.0/0000:06:0d.0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:06:0d.1 ->
	../../../../devices/pci0000:00/0000:00:1e.0/0000:06:0d.1

PCI 橋 0000:00:1e.0 後面掛了兩個設備,一個是剛纔加進去的 0000:06:0d.0,還有一個是 0000:06:0d.1,通過上面的步奏加進去就可以。

最後一步是讓用戶有權限使用這個 group。

# chown user:user /dev/vfio/26

下面就是一個樣例,從用戶態使用 VFIO,整個的使用方式是通過 ioctl來獲取中斷相關信息,以及註冊中斷處理函數,然後也是通過 ioctl來獲取region信息,然後調用相應的mmap函數,讓 CPU 可以訪問內存。

int container, group, device, i;
struct vfio_group_status group_status =
				{ .argsz = sizeof(group_status) };
struct vfio_iommu_type1_info iommu_info = { .argsz = sizeof(iommu_info) };
struct vfio_iommu_type1_dma_map dma_map = { .argsz = sizeof(dma_map) };
struct vfio_device_info device_info = { .argsz = sizeof(device_info) };

/* Create a new container */
container = open("/dev/vfio/vfio", O_RDWR);

if (ioctl(container, VFIO_GET_API_VERSION) != VFIO_API_VERSION)
	/* Unknown API version */

if (!ioctl(container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU))
	/* Doesn't support the IOMMU driver we want. */

/* Open the group */
group = open("/dev/vfio/26", O_RDWR);

/* Test the group is viable and available */
ioctl(group, VFIO_GROUP_GET_STATUS, &group_status);

if (!(group_status.flags & VFIO_GROUP_FLAGS_VIABLE))
	/* Group is not viable (ie, not all devices bound for vfio) */

/* Add the group to the container */
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);

/* Enable the IOMMU model we want */
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);

/* Get addition IOMMU info */
ioctl(container, VFIO_IOMMU_GET_INFO, &iommu_info);

/* Allocate some space and setup a DMA mapping */
dma_map.vaddr = mmap(0, 1024 * 1024, PROT_READ | PROT_WRITE,
		     MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
dma_map.size = 1024 * 1024;
dma_map.iova = 0; /* 1MB starting at 0x0 from device view */
dma_map.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE;

ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);

/* Get a file descriptor for the device */
device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0");

/* Test and setup the device */
ioctl(device, VFIO_DEVICE_GET_INFO, &device_info);

for (i = 0; i < device_info.num_regions; i++) {
	struct vfio_region_info reg = { .argsz = sizeof(reg) };

	reg.index = i;

	ioctl(device, VFIO_DEVICE_GET_REGION_INFO, &reg);

	/* Setup mappings... read/write offsets, mmaps
	 * For PCI devices, config space is a region */
}

for (i = 0; i < device_info.num_irqs; i++) {
	struct vfio_irq_info irq = { .argsz = sizeof(irq) };

	irq.index = i;

	ioctl(device, VFIO_DEVICE_GET_IRQ_INFO, &irq);

	/* Setup IRQs... eventfds, VFIO_DEVICE_SET_IRQS */
}

/* Gratuitous device reset and go... */
ioctl(device, VFIO_DEVICE_RESET);

在include/linux/vfio.h裏面有完整的 API,這裏就簡單略過。

在理解了一些基本原理和使用方式之後再來看 VFIO 的代碼應該叫就比較容易理解了。

首先是作爲 PCI 設備的 probe。主要是通過 vfio_iommu_group_get分配 iommu_group,然後調用vfio_add_group_dev初始化設備回調接口vfio_pci_ops,而remove就是反過來把對應的結構釋放掉就可以。然後再看註冊的回調函數結構體。

static const struct vfio_device_ops vfio_pci_ops = {
	.name		= "vfio-pci",
	.open		= vfio_pci_open,
	.release	= vfio_pci_release,
	.ioctl		= vfio_pci_ioctl,
	.read		= vfio_pci_read,
	.write		= vfio_pci_write,
	.mmap		= vfio_pci_mmap,
	.request	= vfio_pci_request,
};

這裏分析幾個關鍵的函數,他們會通過file_operations vfio_fops被間接的調用。

首先是 mmap,就是在調用vfio_pci_mmap的時候最終調用remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, req_len, vma->vm_page_prot); 來將物理內存映射到用戶態空間,這就是上面的栗子中 mmap 系統調用的入口,而具體要映射的物理內存是通過一系列pci_resource_xxx宏從 PCI bar 空間讀出來的配置。

然後是 ioctl 接口,這個接口比較豐富,也簡單的看一下。比如 VFIO_DEVICE_SET_IRQS會通過使用用戶態傳進來的結構體,調用vfio_pci_set_irqs_ioctl註冊中斷處理函數。而通過vfio_ioctl_set_iommu會設置 container 的 iommu_group 以及對應的 driver。read/write接口都是用於修改 PCI 配置信息的。

簡單的來說,VFIO 的主要工作是把設備通過 IOMMU 映射的 DMA 物理內存地址映射到用戶態中,讓用戶態程序可以自行操縱設備的傳輸,並且可以保證一定程度的安全,另外可以自行註冊中斷處理函數,從而在用戶態實現設備的驅動程序,通過這樣的框架,可以在 DPDK 中充分發揮用戶態協議棧的威力。

原文鏈接:https://ggaaooppeenngg.github.io/zh-CN/2017/06/05/VFIO-——將-DMA-映射暴露給用戶態/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章