設(shè)備直通給虛擬機(jī)能夠極大提升虛擬機(jī)對(duì)物理設(shè)備訪問(wèn)的性能,本文通過(guò)vfio內(nèi)核模塊和qemu用戶態(tài)實(shí)現(xiàn)介紹vfio設(shè)備直通時(shí)的關(guān)鍵部分,包括:用戶態(tài)訪問(wèn)設(shè)備IO地址空間,DMA重映射,中斷重映射等.
VFIO訪問(wèn)直通設(shè)備IO地址空間
1.PIO和MMIO
設(shè)備的IO地址空間的訪問(wèn)有PIO和MMIO兩種方式,前者通過(guò)獨(dú)立的IO端口訪問(wèn)設(shè)備,而MMIO是在物理內(nèi)存中映射一段區(qū)間,直接訪問(wèn)該內(nèi)存就可以訪問(wèn)設(shè)備的配置空間.在虛擬化的場(chǎng)景下,虛擬機(jī)通過(guò)PIO訪問(wèn)直通設(shè)備時(shí),首先會(huì)VM-exit到qemu,由qemu通過(guò)轉(zhuǎn)換表完成對(duì)該P(yáng)IO操作的轉(zhuǎn)發(fā).對(duì)于PCI設(shè)備而言,其bar空間地址是通過(guò)PIO的方式設(shè)置的,如果將設(shè)備的PIO訪問(wèn)完全暴露給虛擬機(jī),虛擬機(jī)修改了真實(shí)的物理設(shè)備的PCI Bar空間基地址配置,與host上不一致,可能會(huì)出現(xiàn)嚴(yán)重的問(wèn)題,所以對(duì)于設(shè)備的PIO訪問(wèn)需要建立轉(zhuǎn)換表,在VM-exit之后由qemu來(lái)完成設(shè)置的轉(zhuǎn)發(fā).
對(duì)于設(shè)備的MMIO空間訪問(wèn),則可以通過(guò)建立EPT頁(yè)表將設(shè)備的MMIO物理內(nèi)存映射到虛擬的MMIO地址空間,讓虛擬機(jī)能夠直接通過(guò)MMIO訪問(wèn)PCI設(shè)備的bar空間,提高IO性能.
2.獲取直通設(shè)備信息
通過(guò)VFIO提供的接口可以獲取到設(shè)備的基本信息,包括設(shè)備的描述符、region的數(shù)量等。
vfio_get_device:
fd = ioctl(group- >fd, VFIO_GROUP_GET_DEVICE_FD, name);
ret = ioctl(fd, VFIO_DEVICE_GET_INFO, &dev_info);
vbasedev- >fd = fd;
vbasedev- >group = group;
QLIST_INSERT_HEAD(&group- >device_list, vbasedev, next);
vbasedev- >num_irqs = dev_info.num_irqs;
vbasedev- >num_regions = dev_info.num_regions;
vbasedev- >flags = dev_info.flags;
2.直通設(shè)備PCI配置空間模擬
Qemu為每個(gè)PCI直通設(shè)備都建立一個(gè)虛擬數(shù)據(jù)結(jié)構(gòu) VFIOPCIDevice,保存物理PCI設(shè)備的相關(guān)信息,由vfio_get_device來(lái)獲取,保存到vbasedev中。
typedef struct VFIOPCIDevice {
PCIDevice pdev;
VFIODevice vbasedev;
VFIO設(shè)備作為qemu的設(shè)備模型的一部分,qemu對(duì)直通設(shè)備的模擬初始化入口在 vfio_realize,通過(guò)vfio_get_device獲取到直通設(shè)備的基本信息之后,會(huì)調(diào)用pread設(shè)備的fd獲取到設(shè)備的配置空間信息的一份拷貝,qemu會(huì)寫(xiě)入一些自定義的config配置。
vfio_realize:
/* Get a copy of config space */
ret = pread(vdev- >vbasedev.fd, vdev- >pdev.config,
MIN(pci_config_size(&vdev- >pdev), vdev- >config_size),
vdev- >config_offset);
if (ret < (int)MIN(pci_config_size(&vdev- >pdev), vdev- >config_size)) {
ret = ret < 0 ? -errno : -EFAULT;
error_setg_errno(errp, -ret, "failed to read device config space");
goto error;
}
/* vfio emulates a lot for us, but some bits need extra love */
vdev- >emulated_config_bits = g_malloc0(vdev- >config_size);
/* QEMU can choose to expose the ROM or not */
memset(vdev- >emulated_config_bits + PCI_ROM_ADDRESS, 0xff, 4);
/* QEMU can change multi-function devices to single function, or reverse */
vdev- >emulated_config_bits[PCI_HEADER_TYPE] =
PCI_HEADER_TYPE_MULTI_FUNCTION;
/* Restore or clear multifunction, this is always controlled by QEMU */
if (vdev- >pdev.cap_present & QEMU_PCI_CAP_MULTIFUNCTION) {
vdev- >pdev.config[PCI_HEADER_TYPE] |= PCI_HEADER_TYPE_MULTI_FUNCTION;
} else {
vdev- >pdev.config[PCI_HEADER_TYPE] &= ~PCI_HEADER_TYPE_MULTI_FUNCTION;
}
/*
* Clear host resource mapping info. If we choose not to register a
* BAR, such as might be the case with the option ROM, we can get
* confusing, unwritable, residual addresses from the host here.
*/
memset(&vdev- >pdev.config[PCI_BASE_ADDRESS_0], 0, 24);
memset(&vdev- >pdev.config[PCI_ROM_ADDRESS], 0, 4);
3.直通設(shè)備MMIO映射
直通PCI設(shè)備的MMIO內(nèi)存主要是指其Bar空間,qemu使用vfio_populate_device函數(shù)調(diào)用VFIO接口獲取到PCI設(shè)備的Bar空間信息,然后通過(guò)vfio_region_setup獲取到對(duì)應(yīng)region的信息,并將qemu內(nèi)存虛擬化的MemoryRegion設(shè)置為IO類(lèi)型的region。重要的是,qemu會(huì)為該IO類(lèi)型的MemoryRegion設(shè)置ops為vfio_region_ops,這樣后續(xù)對(duì)于該塊內(nèi)存的讀寫(xiě)會(huì)經(jīng)過(guò)qemu VFIO模塊注冊(cè)的接口來(lái)進(jìn)行。
vfio_populate_device:
for (i = VFIO_PCI_BAR0_REGION_INDEX; i < VFIO_PCI_ROM_REGION_INDEX; i++) {
char *name = g_strdup_printf("%s BAR %d", vbasedev- >name, i);
ret = vfio_region_setup(OBJECT(vdev), vbasedev, &vdev- >bars[i].region, i, name);
- > vfio_get_region_info
- > memory_region_init_io(region- >mem, obj, &vfio_region_ops,
region, name, region- >size);
QLIST_INIT(&vdev- >bars[i].quirks);
}
ret = vfio_get_region_info(vbasedev, VFIO_PCI_CONFIG_REGION_INDEX, ®_info);
- > ioctl(vbasedev- >fd, VFIO_DEVICE_GET_REGION_INFO, *info))
ret = ioctl(vdev- >vbasedev.fd, VFIO_DEVICE_GET_IRQ_INFO, &irq_info);
到這里已經(jīng)獲取到了PCI設(shè)備的MMIO內(nèi)存信息,但是還沒(méi)有真正的將物理內(nèi)存中的Bar空間映射到qemu,這一動(dòng)作在vfio_bars_setup中完成,vfio_region_mmap會(huì)對(duì)region中每個(gè)需要map的內(nèi)存地址完成映射,然后將映射的物理內(nèi)存通過(guò)qemu注冊(cè)到虛擬機(jī)作為一段虛擬機(jī)的物理地址空間。
vfio_bars_setup:
for (i = 0; i < PCI_ROM_SLOT; i++)
vfio_bar_setup(vdev, i);
vfio_region_mmap(&bar- >region)
for (i = 0; i < region- >nr_mmaps; i++) {
region- >mmaps[i].mmap = mmap(NULL, region- >mmaps[i].size, prot,
MAP_SHARED, region- >vbasedev- >fd,
region- >fd_offset +
region- >mmaps[i].offset);
memory_region_init_ram_device_ptr
memory_region_add_subregion
pci_register_bar(&vdev- >pdev, nr, type, bar- >region.mem);
這里的映射mmap接口對(duì)應(yīng)的是VFIO設(shè)備在內(nèi)核中注冊(cè)的vfio_pci_mmap 函數(shù),在內(nèi)核中,該函數(shù)會(huì)為vma注冊(cè)一個(gè)mmap的ops,對(duì)應(yīng)著注冊(cè)了一個(gè)缺頁(yè)處理函數(shù),當(dāng)用戶態(tài)程序訪問(wèn)該段虛擬內(nèi)存缺頁(yè)時(shí),調(diào)用注冊(cè)的缺頁(yè)處理函數(shù),完成虛擬地址到實(shí)際物理地址的映射。
vfio_pci_mmap:
vma- >vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;
vma- >vm_ops = &vfio_pci_mmap_ops;
- > .fault = vfio_pci_mmap_fault,
- > if (remap_pfn_range(vma, vma- >vm_start, vma- >vm_pgoff,
vma- >vm_end - vma- >vm_start, vma- >vm_page_prot))
簡(jiǎn)單來(lái)說(shuō),對(duì)于MMIO內(nèi)存的的映射,主要是將物理內(nèi)存中的MMIO空間映射到了qemu的虛擬地址空間,然后再由qemu將該段內(nèi)存注冊(cè)進(jìn)虛擬機(jī)作為虛擬機(jī)的一段物理內(nèi)存,在這個(gè)過(guò)程中會(huì)建立從gpa到hpa的EPT頁(yè)表映射,提升MMIO的性能。
DMA重映射
首先關(guān)于DMA,設(shè)備通過(guò)DMA可以直接使用iova地址訪問(wèn)物理內(nèi)存,從iova到實(shí)際物理地址的映射是在IOMMU中完成的,一般在dma_allooc分配設(shè)備能夠訪問(wèn)的內(nèi)存的時(shí)候,會(huì)分配iova地址和實(shí)際的物理地址空間,并在iommu中建立映射關(guān)系。 所以說(shuō)要讓設(shè)備進(jìn)行DMA最關(guān)鍵的幾個(gè)部分:
- 設(shè)備能夠識(shí)別的地址:IOVA
- 一段物理內(nèi)存
- IOVA到物理內(nèi)存在IOMMU中的映射關(guān)系
基于這幾點(diǎn)來(lái)看VFIO的DMA重映射就比較清晰,首先從VFIO設(shè)備的初始化開(kāi)始,在獲取設(shè)備信息之前會(huì)先獲取到設(shè)備所屬的group和Container,并調(diào)用VFIO_SET_IOMMU完成container和IOMMU的綁定,并attach由VFIO管理的所有設(shè)備。此外注意到這里的 pci_device_iommu_address_space 函數(shù),意思是qemu為設(shè)備dma注冊(cè)了一段專門(mén)的地址空間,這段內(nèi)存作為虛擬機(jī)的一段物理內(nèi)存存在,在VFIO_SET_IOMMU之后,注冊(cè)該地址空間,其region_add函數(shù)為 vfio_listener_region_add,意思是當(dāng)內(nèi)存空間布局發(fā)生變化這里是增加內(nèi)存的時(shí)候都會(huì)調(diào)用該接口。
vfio_realize:
group = vfio_get_group(groupid, pci_device_iommu_address_space(pdev), errp);
vfio_connect_container(group, as, errp)
ret = ioctl(fd, VFIO_SET_IOMMU, container- >iommu_type);
container- >listener = vfio_memory_listener;
memory_listener_register(&container- >listener, container- >space- >as);
- > .region_add = vfio_listener_region_add,
那么跟DMA有什么關(guān)系呢,當(dāng)為設(shè)備進(jìn)行DMA分配一塊內(nèi)存時(shí),實(shí)際是以MemoryRegion的形式存在的,也就是說(shuō)虛擬機(jī)進(jìn)行dma alloc 會(huì)調(diào)用region_add函數(shù),進(jìn)而調(diào)用注冊(cè)的memory_listener_region_add函數(shù),MemoryRegion有了意味著分配了一塊物理內(nèi)存,還需要IOVA和映射關(guān)系才行。這里,IOVA地址使用的是section->offset_within_address_space,為什么可以這樣,因?yàn)镮OVA地址只是作為設(shè)備識(shí)別的地址,只要建立了映射關(guān)系就有意義。
vfio_listener_region_add:
iova = TARGET_PAGE_ALIGN(section- >offset_within_address_space);
/* Here we assume that memory_region_is_ram(section- >mr)==true */
vaddr = memory_region_get_ram_ptr(section- >mr) +
section- >offset_within_region +
(iova - section- >offset_within_address_space);
ret = vfio_dma_map(container, iova, int128_get64(llsize),
vaddr, section- >readonly);
vfio_dma_map:
struct vfio_iommu_type1_dma_map map = {
.argsz = sizeof(map),
.flags = VFIO_DMA_MAP_FLAG_READ,
.vaddr = (__u64)(uintptr_t)vaddr,
.iova = iova,
.size = size,
};
ioctl(container- >fd, VFIO_IOMMU_MAP_DMA, &map)
建立映射的關(guān)鍵在于vfio_dma_map,通過(guò)ioctl調(diào)用container->fd接口VFIO_IOMMU_MAP_DMA完成DMA重映射。為什么是container->fd,因?yàn)閂FIO Container管理內(nèi)存資源,與IOMMU直接綁定,而IOMMU是完成IOVA到實(shí)際物理內(nèi)存映射的關(guān)鍵。值得注意的是qemu只知道這一段內(nèi)存的虛擬地址vaddr,所以將vaddr,iova和size傳給內(nèi)核,由內(nèi)核獲取物理內(nèi)存信息完成映射。
vfio_dma_do_map:
vfio_pin_map_dma
while (size) {
/* Pin a contiguous chunk of memory */
npage = vfio_pin_pages_remote(dma, vaddr + dma- >size,
size > > PAGE_SHIFT, &pfn, limit);
/* Map it! */
vfio_iommu_map(iommu, iova + dma- >size, pfn, npage,
dma- >prot);
list_for_each_entry(d, &iommu- >domain_list, next)
iommu_map(d- >domain, iova, (phys_addr_t)pfn < < PAGE_SHIFT,
npage < < PAGE_SHIFT, prot | d- >prot);
arm_smmu_map
__arm_lpae_map
size -= npage < < PAGE_SHIFT;
dma- >size += npage < < PAGE_SHIFT;
}
內(nèi)核完成建立iova到物理內(nèi)存的映射之前會(huì)將分配的DMA內(nèi)存給pin住,使用vfio_pin_pages_remote接口可以獲取到虛擬地址對(duì)應(yīng)的物理地址和pin住的頁(yè)數(shù)量,然后vfio_iommu_map進(jìn)而調(diào)用iommu以及smmu的map函數(shù),最終用iova,物理地址信息pfn以及要映射的頁(yè)數(shù)量在設(shè)備IO頁(yè)表中建立映射關(guān)系。
+--------+ iova +--------+ gpa +----+
| device | - > | memory | < - | vm |
+--------+ +--------+ +----+
最終完成了DMA重映射,設(shè)備使用qemu分配的iova地址通過(guò)IOMMU映射訪問(wèn)內(nèi)存,虛擬機(jī)使用gpa通過(guò)Stage 2頁(yè)表映射訪問(wèn)內(nèi)存
中斷重映射
對(duì)于PCI直通設(shè)備中斷的虛擬化,主要包括三種類(lèi)型INTx,Msi和Msi-X。
1.INTx中斷初始化及enable
對(duì)于INTx類(lèi)型的中斷,在初始化的時(shí)候就進(jìn)行使能了,qemu通過(guò)VFIO device的接口將中斷irq set設(shè)置到內(nèi)核中,并且會(huì)注冊(cè)一個(gè)eventfd,設(shè)置了eventfd的handler,當(dāng)發(fā)生intx類(lèi)型的中斷時(shí),內(nèi)核會(huì)通過(guò)eventfd通知qemu進(jìn)行處理,qemu會(huì)通知虛擬機(jī)進(jìn)行處理。
vfio_realize:
if (vfio_pci_read_config(&vdev- >pdev, PCI_INTERRUPT_PIN, 1)) {
pci_device_set_intx_routing_notifier(&vdev- >pdev, vfio_intx_update);
ret = vfio_intx_enable(vdev, errp);
*pfd = event_notifier_get_fd(&vdev- >intx.interrupt);
qemu_set_fd_handler(*pfd, vfio_intx_interrupt, NULL, vdev);
ret = ioctl(vdev- >vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set);
2.MSI-X初始化
MSIX在vfio_realzie初始化時(shí),首先獲取到物理設(shè)備的中斷相關(guān)的配置信息,將其設(shè)置到注冊(cè)給對(duì)應(yīng)的MMIO內(nèi)存中
vfio_msix_early_setup:
pos = pci_find_capability(&vdev- >pdev, PCI_CAP_ID_MSIX);
if (pread(fd, &ctrl, sizeof(ctrl),
vdev- >config_offset + pos + PCI_MSIX_FLAGS) != sizeof(ctrl)) {
if (pread(fd, &table, sizeof(table),
vdev- >config_offset + pos + PCI_MSIX_TABLE) != sizeof(table)) {
if (pread(fd, &pba, sizeof(pba),
vdev- >config_offset + pos + PCI_MSIX_PBA) != sizeof(pba)) {
ctrl = le16_to_cpu(ctrl);
table = le32_to_cpu(table);
pba = le32_to_cpu(pba);
msix = g_malloc0(sizeof(*msix));
msix- >table_bar = table & PCI_MSIX_FLAGS_BIRMASK;
msix- >table_offset = table & ~PCI_MSIX_FLAGS_BIRMASK;
msix- >pba_bar = pba & PCI_MSIX_FLAGS_BIRMASK;
msix- >pba_offset = pba & ~PCI_MSIX_FLAGS_BIRMASK;
msix- >entries = (ctrl & PCI_MSIX_FLAGS_QSIZE) + 1;
vfio_msix_setup:
msix_init(&vdev- >pdev, vdev- >msix- >entries,
vdev- >bars[vdev- >msix- >table_bar].region.mem,
vdev- >msix- >table_bar, vdev- >msix- >table_offset,
vdev- >bars[vdev- >msix- >pba_bar].region.mem,
vdev- >msix- >pba_bar, vdev- >msix- >pba_offset, pos);
memory_region_init_io(&dev- >msix_table_mmio, OBJECT(dev), &msix_table_mmio_ops, dev,
"msix-table", table_size);
memory_region_add_subregion(table_bar, table_offset, &dev- >msix_table_mmio);
memory_region_init_io(&dev- >msix_pba_mmio, OBJECT(dev), &msix_pba_mmio_ops, dev,
"msix-pba", pba_size);
memory_region_add_subregion(pba_bar, pba_offset, &dev- >msix_pba_mmio);
vfio_realize:
/* QEMU emulates all of MSI & MSIX */
if (pdev- >cap_present & QEMU_PCI_CAP_MSIX) {
memset(vdev- >emulated_config_bits + pdev- >msix_cap, 0xff,
MSIX_CAP_LENGTH);
if (pdev- >cap_present & QEMU_PCI_CAP_MSI) {
memset(vdev- >emulated_config_bits + pdev- >msi_cap, 0xff,
vdev- >msi_cap_size);
- MSI/MSI-X enable 與 irqfd的注冊(cè)
當(dāng)虛擬機(jī)因?yàn)閷?xiě)PCI配置空間而發(fā)生VM-exit時(shí),最終會(huì)完成msi和msix的使能,以MSIX的使能為例,在qemu側(cè)會(huì)設(shè)置eventfd的處理函數(shù),并通過(guò)kvm將irqfd注冊(cè)到內(nèi)核中,進(jìn)而注冊(cè)虛擬中斷給虛擬機(jī)。
kvm_cpu_exec:
vfio_pci_write_config:
vfio_msi_enable(vdev);
vfio_msix_enable(vdev);
for (i = 0; i < vdev- >nr_vectors; i++) {
if (event_notifier_init(&vector- >interrupt, 0)) {
qemu_set_fd_handler(event_notifier_get_fd(&vector- >interrupt),
vfio_msi_interrupt, NULL, vector);
vfio_add_kvm_msi_virq(vdev, vector, i, false);
kvm_irqchip_add_msi_route(kvm_state, vector_n, &vdev- >pdev);
vfio_add_kvm_msi_virq
kvm_irqchip_add_irqfd_notifier_gsi
kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
/* Set interrupt type prior to possible interrupts */
vdev- >interrupt = VFIO_INT_MSI;
ret = vfio_enable_vectors(vdev, false);
ret = ioctl(vdev- >vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set);
小結(jié)
要讓設(shè)備直通給虛擬機(jī),需要將設(shè)備的DMA能力、中斷響應(yīng)和IO地址空間訪問(wèn)安全地暴露給用戶態(tài),本文主要介紹了VFIO設(shè)備直通關(guān)鍵的幾個(gè)環(huán)節(jié),包括如何在用戶態(tài)訪問(wèn)物理設(shè)備的IO地址空間、如何進(jìn)行DMA重映射和中斷重映射。
-
dma
+關(guān)注
關(guān)注
3文章
555瀏覽量
100227 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
897瀏覽量
27971 -
MMU
+關(guān)注
關(guān)注
0文章
91瀏覽量
18237 -
串口中斷
+關(guān)注
關(guān)注
0文章
64瀏覽量
13824 -
qemu
+關(guān)注
關(guān)注
0文章
57瀏覽量
5315
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論