轉載 https://blog.csdn.net/bemind1/article/details/99678642
What is VFIO?
-
VFIO是一個可以安全的把設備I/O、中斷、DMA等暴露到用戶空間(userspace),從而可以在用戶空間完成設備驅動的框架。
-
得益於vfio低開銷的用戶空間直接設備訪問,虛擬機設備分配(device assignment)、高性能應用等可以獲得更高的I/O性能。
2. IOMMU
實現用戶空間設備驅動,最困難的在於如何將DMA以安全可控的方式暴露到用戶空間:
-
提供DMA的設備通常可以寫內存的任意頁,因此使用戶空間擁有創建DMA的能力就等同於用戶空間擁有了root權限,惡意的設備可能利用此發動DMA攻擊。
-
I/O memory management unit(IOMMU)的引入對設備進行了限制,設備I/O地址需要經過IOMMU重映射為內存物理地址,如圖2-1。惡意的或存在錯誤的設備不能讀寫沒有被明確映射過的內存,運行在cpu上的操作系統以互斥的方式管理MMU與IOMMU,物理設備不能繞行或污染可配置的內存管理表項。
圖 2-1 Comparison IOMMU and MMU
IOMMU其他好處:
-
IOMMU可以將連續的虛擬地址映射到不連續的多個物理內存片段,從而支持vectored I/O(scatter-gather list);
-
對於不能尋址全部物理地址空間的設備,通過IOMMU的重映射,從而避免了將數據從設備可訪問的外圍地址空間拷入拷出設備無法訪址的物理地址空間的額外開銷(避免了bounce buffer)。
3. Device,group, container
由於device本身的特性、互連(interconnect)及IOMMU的拓撲等,IOMMU提供device 隔離(ioslation)的最小粒度是group,而不是device。如一個pci device可能包括多個function,而這些function之間數據傳遞可以通過專用通道(backdoor),而不經過IOMMU等等,所以device並不適合做隔離的最小單元。
container可以包含多個group,這些group共享頁表信息。
4. vfio use example
詳看[linux-rootdir]/Documentation/vfio.txt
-
Linux kernel實現
5.1. 相關內核組件
5.1.1 內核組件概圖
圖5-1是vfio內核組件概圖:
圖5-1 vfio內核組件概圖
vfio interface:vfio通過設備文件向userspace提供統一訪問接口,包括container、group、device。
vfio_iommu_driver:為vfio提供了IOMMU重映射驅動,即向用戶空間暴露DMA操作,如container的ioctl選項VFIO_IOMMU_MAP_DMA即由vfio containter設備文件對應的 file_operations 的ioctl轉發到vfio_iommu_driver的ioctl實現,已實現的vfio_iommu_driver包括vfio_iommu_type1、vfio_spapr_eeh等,這里重點分析vfio_iommu_type1。
vfio-pci:vfio支持pci設備pass-through,vfio-pci作為pci driver掛載到pci總線,提供將pci設備io、interrupt暴露到用戶空間實現。
5.1.2. 各組件之間如何關聯
圖3是vfio關鍵數據結構概圖:
圖5-2 vfio關鍵數據結構概圖
注:M表示一對多
-
設備文件入口
a) Container
userspace通過open設備文件/dev/vfio/vfio獲得container對應的文件描述符:
/* Create a new container */ int container = open("/dev/vfio/vfio", O_RDWR); |
文件標識符container關聯struct file_operations vfio_fops,這是在vfio/vfio.c中通過注冊miscdevice實現的:
1793 static struct miscdevice vfio_dev = { 1794 .minor = VFIO_MINOR, 1795 .name = "vfio", 1796 .fops = &vfio_fops, 1797 .nodename = "vfio/vfio", 1798 .mode = S_IRUGO | S_IWUGO, 1799 }; |
b) Group
userspace通過open設備文件/dev/vfio/{group-num}獲得group對應的文件描述符,假設
group-num為26:
/* Open the group */ group = open("/dev/vfio/26", O_RDWR); |
其通過注冊字符設備關聯struct file_operations vfio_group_fops實現:
1567 static const struct file_operations vfio_group_fops = { 1568 .owner = THIS_MODULE, 1569 .unlocked_ioctl = vfio_group_fops_unl_ioctl, 1570 #ifdef CONFIG_COMPAT 1571 .compat_ioctl = vfio_group_fops_compat_ioctl, 1572 #endif 1573 .open = vfio_group_fops_open, 1574 .release = vfio_group_fops_release, 1575 }; |
接下來,userspace通過group fd關聯的ioctl選項 VFIO_GROUP_SET_CONTAINER,
將group加入container:
/* Add the group to the container */ ioctl(group, VFIO_GROUP_SET_CONTAINER, &container); |
c) Device
與container和group不同,device的設備文件並不暴露在userspace,userspace通過group fd關聯的ioctl選項VFIO_GROUP_GET_DEVICE_FD,得到device對應文件描述符,如:
/* Get a file descriptor for the device */ device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0"); |
在kernel中,通過group對應的ioctl==》vfio_group_get_device_fd:
- 遍歷group得到對應vfio_device后,為device分配fd及struct file,並使兩者關聯,其中struct file關聯的是匿名的inode。
Device fd關聯的struct file_operations為vfio_device_fops:
1646 static const struct file_operations vfio_device_fops = { 1647 .owner = THIS_MODULE, 1648 .release = vfio_device_fops_release, 1649 .read = vfio_device_fops_read, 1650 .write = vfio_device_fops_write, 1651 .unlocked_ioctl = vfio_device_fops_unl_ioctl, 1652 #ifdef CONFIG_COMPAT 1653 .compat_ioctl = vfio_device_fops_compat_ioctl, 1654 #endif 1655 .mmap = vfio_device_fops_mmap, 1656 }; |
另外,vfio_device_fops方法實現主要是封裝了vfio_device對應的struct_device_ops,
該方法在創建vfio_device時賦值。
2) vfio-pci如何與vfio interface關聯?
vfio-pci實際上被注冊為pci driver,在文件vfio/pci/vfio_pci.c:
1206 static struct pci_driver vfio_pci_driver = { 1207 .name = "vfio-pci", 1208 .id_table = NULL, /* only dynamic ids */ 1209 .probe = vfio_pci_probe, 1210 .remove = vfio_pci_remove, 1211 .err_handler = &vfio_err_handlers, 1212 };
|
1361 /* Register and scan for devices */ 1362 ret = pci_register_driver(&vfio_pci_driver); |
插入新的pci設備或驅動,並完成pci_bus_match之后,會調用pci driver對應的probe方法,在函數vfio_pci_probe中,會創建vfio_group、vfio_device並把vfio_device加入vfio_group鏈表,
In function vfio_pci_probe:==>vfio_add_group_dev:==>vfio_create_group分配vfio_group數據結構並完成初始化,並在初始化過程中使vfio_group指針關聯idr整數(idr是內核提供的一種整數關聯指針的機制,idr類似於身份證,唯一關聯對應指針):
In file vfio/vfio.c:
267 * Group minor allocation/free - both called with vfio.group_lock held 268 */ 269 static int vfio_alloc_group_minor(struct vfio_group *group) 270 { 271 return idr_alloc(&vfio.group_idr, group, 0, MINORMASK + 1, GFP_KERNEL); 272 }
|
接下來是vfio interface與vfio-pci關聯的關鍵一步:
在打開group設備文件,調用對應file_operations的open方法時,In function vfio_group_fops_open中通過idr得到在vfio-pci初始化時創建的vfio_group:
467 static struct vfio_group *vfio_group_get_from_minor(int minor) 468 { 469 struct vfio_group *group; 470 471 mutex_lock(&vfio.group_lock); 472 group = idr_find(&vfio.group_idr, minor); 473 if (!group) { 474 mutex_unlock(&vfio.group_lock); 475 return NULL; 476 } 477 vfio_group_get(group); 478 mutex_unlock(&vfio.group_lock); 479 480 return group; 481 } |
綜上,vfio-pci被實現為pci driver,在初始化時創建vfio_group、vfio_device,並使vfio_group關聯idr,而在打開group設備文件時,再通過idr獲得已分配的vfio_group,從而將vfio interface與vfio-pci關聯起來。
3) vfio_iommu_driver如何與vfio interface關聯?
Userspace通過顯式的ioctl調用為container關聯對應的vfio_iommu_driver,如:
/* Enable the IOMMU model we want */ ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU); |
在內核中會遍歷已注冊的vfio_iommu_driver,並賦給vfio_container對應成員。
5.2. 將DMA暴露到userspace
實現vfio,DMA是最棘手的部分,IOMMU的引入解決了將DMA暴露到用戶空間的安全性問題。
Userspace設置IOMMU的重映射的方式如下:
/* 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); |
以上將container包含的所有設備的設備地址空間[0,1MB)映射到物理內存。
注:iova:I/O虛擬地址,用作配置DMA的地址
vaddr:用戶空間虛擬地址
對於ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map),查看container設備文件對應的ioctl,該選項被轉發到container關聯的vfio_iommu_driver對應的ioctl,查看vfio_iommu_type1對應的ioctl實現:
In function vfio_iommu_type1_ioctl:==>vfio_dma_do_map:
- 如圖5-2,在數據結構vfio_iommu中包含一個紅黑樹(struct rb_root dma_list),用於記錄(iova, vaddr)的映射;在vfio_dma_do_map中首先檢查dma_list,確保已映射的dma區域不與傳入的、請求映射到iova的區域重疊;
- 接着轉換得到用戶空間虛擬地址vaddr對應的物理地址,並pin對應的內存區域;
- 最后,將(iova, pfn)傳給iommu組件完成映射。
圖5-2包含vfio_iommu_type1實現關鍵的數據結構:vfio_iommu, vfio_domain, vfio_group, vfio_dma等:
- iommu_domain, iommu_group是更底層iommu驅動組件抽象的數據結構;
- domain是一組資源的集合,包含了物理內存及可訪問的設備, 同一個domain中的設備共享頁表區域,container與domain的區別在於:
- container是vfio interface使用的概念,domain是更底層iommu_driver使用的概念;
- container與domain並不是對等的,一個container內的設備可能划分到多個domain之中,由於處在同一個container中的設備共享頁表,每個(iova,pfn)的映射必須同時傳遞到所有的domain中。
vfio_iommu_map是將(iova, pfn)傳遞給iommu的關鍵函數,其遍歷所有已划分的
domain,建立(iova,pfn)映射表:
In file vfio_iommu_type1.c
534 static int vfio_iommu_map(struct vfio_iommu *iommu, dma_addr_t iova, 535 unsigned long pfn, long npage, int prot) 536 { 537 struct vfio_domain *d; 538 int ret; 539 540 list_for_each_entry(d, &iommu->domain_list, next) { 541 ret = iommu_map(d->domain, iova, (phys_addr_t)pfn << PAGE_SHIFT, 542 npage << PAGE_SHIFT, prot | d->prot); 543 if (ret) { 544 if (ret != -EBUSY || 545 map_try_harder(d, iova, pfn, npage, prot)) 546 goto unwind; 547 } 548 549 cond_resched(); 550 } 551 552 return 0; ... 559 } |
5.3. 將I/O暴露到userspace
I/O暴露到userspace比較簡單,只是把I/O物理地址remap到userspace,對於pci設備包括pci config space、bar等。
在userspace可按照如下方式訪問I/O區域:
/* 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, ®); /* Setup mappings... read/write offsets, mmaps * For PCI devices, config space is a region */ } |
首先分析ioctl(device, VFIO_DEVICE_GET_REGION_INFO, ®),經device fd對應的struct file_operations vfio_device_fops{.ioctl} ==> struct vfio_device{vfio_device_ops{.ioctl}} :
1086 static const struct vfio_device_ops vfio_pci_ops = { 1087 .name = "vfio-pci", 1088 .open = vfio_pci_open, 1089 .release = vfio_pci_release, 1090 .ioctl = vfio_pci_ioctl, 1091 .read = vfio_pci_read, 1092 .write = vfio_pci_write, 1093 .mmap = vfio_pci_mmap, 1094 .request = vfio_pci_request, 1095 }; |
這里分析pci bar,其他的io region重映射方式類似;
581 case VFIO_PCI_BAR0_REGION_INDEX ... VFIO_PCI_BAR5_REGION_INDEX: 582 info.offset = VFIO_PCI_INDEX_TO_OFFSET(info.index); 583 info.size = pci_resource_len(pdev, info.index); 584 if (!info.size) { 585 info.flags = 0; 586 break; 587 } 588 589 info.flags = VFIO_REGION_INFO_FLAG_READ | 590 VFIO_REGION_INFO_FLAG_WRITE; 591 if (IS_ENABLED(CONFIG_VFIO_PCI_MMAP) && 592 pci_resource_flags(pdev, info.index) & 593 IORESOURCE_MEM && info.size >= PAGE_SIZE) { 594 info.flags |= VFIO_REGION_INFO_FLAG_MMAP; 595 if (info.index == vdev->msix_bar) { 596 ret = msix_sparse_mmap_cap(vdev, &caps); 597 if (ret) 598 return ret; 599 } 600 } 601 602 break; |
==>VFIO_PCI_INDEX_TO_OFFSET
22 #define VFIO_PCI_OFFSET_SHIFT 40 23 24 #define VFIO_PCI_OFFSET_TO_INDEX(off) (off >> VFIO_PCI_OFFSET_SHIFT) 25 #define VFIO_PCI_INDEX_TO_OFFSET(index) ((u64)(index) << VFIO_PCI_OFFSET_SHIFT) 26 #define VFIO_PCI_OFFSET_MASK (((u64)(1) << VFIO_PCI_OFFSET_SHIFT) - 1) |
pci bar對應的offset只是index <<40,而當userspace通過mmap、read/write等訪問對應區域時,對於傳入的參數ppos,ppos低40位存儲了實際的偏移量,ppos >> 40即可得到pci bar對應的index,有了這個index,再通過pci_resource_start、pci_resoucre_end、pci_resource_len等就可得到pci bar io region對應的開始地址、結束地址、長度等信息,查看mmap實現:
==>vfio_pci_mmap
1001 static int vfio_pci_mmap(void *device_data, struct vm_area_struct *vma) 1002 { ... 1009 index = vma->vm_pgoff >> (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT); 1010 ... 1020 phys_len = pci_resource_len(pdev, index); 1021 req_len = vma->vm_end - vma->vm_start; 1022 pgoff = vma->vm_pgoff & 1023 ((1U << (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)) - 1); 1024 req_start = pgoff << PAGE_SHIFT; ... 1058 vma->vm_private_data = vdev; 1059 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); 1060 vma->vm_pgoff = (pci_resource_start(pdev, index) >> PAGE_SHIFT) + pgoff; 1061 1062 return remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, 1063 req_len, vma->vm_page_prot); 1064 }
|
Line 1009之所以是(VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)而不是(VFIO_PCI_OFFSET_SHIFT)是因為vma->vm_pgoff表示的是頁粒度的偏移量(offset of the area in the file, in pages)。
Line 1060得到實際要訪問的io區域,最后remap_pfn_range負責建立該物理區域的頁表。
5.4. 將interrupt暴露到userspace
將interrupt暴露到userspace,利用了linux提供的系統調用eventfd。
int eventfd(unsigned int initval, int flags); |
eventfd創建了一個文件描述符用於事件通知:
- 創建了一個’eventfd object’,可以作為userspace應用程序間事件wait/notify機制;
- 也可以用於內核向userspace事件通知;
- 內核維護一個64- bit的integer counter,可由’initval’賦初值;在userspace,當read時:
- 若未置EFD_SEMAPHORE位,counter為非零值,則返回8bytes的counter值,
並將該counter reset為零;
- 如counter為0, 若置EFD_NONBLOCK,則返回error EAGAIN,否則阻塞。
- 在kernel space, 維護了struct eventfd_ctx數據結構, 可以通過調用eventfd_signal()函數增加eventfd counter, 並通知用戶空間;
- 在userspace, eventfd返回的文件描述符支持select/poll/epoll,從而實現kernel到userspace的異步事件通知;
- eventfd在userspace的用法可參照http://linux.die.net/man/2/eventfd
- eventfd在kernel的使用參照linux kernel源代碼[linux-kernel-root]/fs/eventfd.c.
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, &info);
/* Setup IRQs... eventfds, VFIO_DEVICE_SET_IRQ */ int irqfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); .... irq_set->data = irqfd; ioctl(device, VFIO_DEVICE_SET_IRQ, irq_set);
/* for example , create a pthread for waiting irqfd */ pthread_create(&irq_thread[i], NULL, irq_handler_func, NULL); }
static irq_handler_func(void *arg) { ..... /* select/poll/epoll to wait */ ... read(irqfd,...); } |
在userspace可以按照以下方式設置與處理中斷:
- 首先通過ioctl(VFIO_DEVICE_GET_IRQ_INFO)得到中斷信息,
- 接着調用eventfd得到文件描述符irqfd,然后通過ioctl(VFIO_DEVICE_SET_IRQ)設置中斷,使該文件描述符與中斷關聯。
- 接下來, userspace可以創建一個新的線程(可選的實現),在該線程中將irqfd加入select/poll/epoll的等待隊列,
- 當接收到kernel space事件通知時,調用read(irqfd,...)減少eventfd counter,接着轉發到Userspace設備驅動完成中斷處理
在kernel space, 可以看一下ioctl(VFIO_DEVICE_SET_IRQ)實現,這里以pci msix中斷為例。
vfio_pci_ioctl()==>vfio_pci_set_irqs_ioctl() ==> msix:vfio_pci_set_msi_trigger()
==>vfio_msi_set_block==>vfio_set_block_vector_signal():
308 static int vfio_msi_set_vector_signal(struct vfio_pci_device *vdev, 309 int vector, int fd, bool msix) 310 { 311 struct pci_dev *pdev = vdev->pdev; 312 struct eventfd_ctx *trigger; 313 int irq, ret; 314 315 if (vector < 0 || vector >= vdev->num_ctx) 316 return -EINVAL; 317 318 irq = msix ? vdev->msix[vector].vector : pdev->irq + vector; ... 337 trigger = eventfd_ctx_fdget(fd); 338 if (IS_ERR(trigger)) { 339 kfree(vdev->ctx[vector].name); 340 return PTR_ERR(trigger); 341 } ... 357 ret = request_irq(irq, vfio_msihandler, 0, 358 vdev->ctx[vector].name, trigger); 359 if (ret) { 360 kfree(vdev->ctx[vector].name); 361 eventfd_ctx_put(trigger); 362 return ret; 363 } ... |
Line 337調用eventfd_ctx_fdget()由fd得到該文件描述符對應的struct eventfd_ctx, line 357注冊中斷,並將該eventfd_ctx作為參數傳給中斷處理函數vfio_msihandler():
239 /* 240 * MSI/MSI-X 241 */ 242 static irqreturn_t vfio_msihandler(int irq, void *arg) 243 { 244 struct eventfd_ctx *trigger = arg; 245 246 eventfd_signal(trigger, 1); 247 return IRQ_HANDLED; 248 } |
在中斷發生時,在中斷處理函數中會調用eventfd_signal(), userspace接到通知,等待的select/poll/epoll會返回,接着利用read()遞減eventfd關聯的counter,並調用userspace中的設備driver完成實際的中斷處理任務。
Line 335調用eventfd_ctx_fdget()由fd得到該文件描述符對應的struct eventfd_ctx, line 355注冊中斷,
並將該eventfd_ctx作為參數傳給中斷處理函數vfio_msihandler():
6. 總結
這篇文檔着重介紹了vfio和iommu的概念,並對vfio使用及linux kernel實現進行的分析。
在分析linux kernel實現時,除介紹vfio各模塊組織架構及如何關聯外,着重從如何將DMA暴露到userspace、如何將I/O暴露到userspace、如何將中斷暴露到userspace等三個方面分析了vfio實現。
對於vfio如何使用,可以參考第4部分vfio use example,及[qemu-root]/hw/vfio/pci.c。