支持虛擬化技術的硬件平台主要做兩件事,一個是DMA Remapping,將DMA請求中的Guest的物理地址映射到Host的物理地址,另一個是中斷Remapping,將能remappable的中斷請求根據由VMM設置,位於內存的IRT(Interrupt Remapping Table)發送到指定的vcpu上。
實質就是在dma設備和memory之間加了一層,用於remapping和檢查。
通過dma remmapping,iommu可以支持將設備直接賦給guest,將dma表內設備對應的地址寫成guest對應的地址即可
iommu還支持interupt的remapping,從而可以將interrupt映射到特定的vm,支持vm對設備的直接操作
IOMMU
DMA重映射也稱為IOMMU,因為它的功能類似於用於IO內存訪問的內存管理單元(MMU)。不僅概念相似,而且與MMU的編程接口也非常相似,即分頁結構和EPT。
從高級的角度來看,主要區別在於DMA重映射在熟悉的PML5、4,PDPT,PD和PT的頂部使用了兩個表進行轉換。簡而言之,使用MMU進行的轉換如下:
· Hardware register => PML4 => PDPT => ...
而IOMMU的是:
· Hardware register => Root table => Context table => PML4 => PDPT => ...
該規范將上下文表中引用的表稱為第二級頁表,下圖說明了轉換流程。
IOMMU功能簡介
IOMMU主要功能包括DMA Remapping和Interrupt Remapping,這里主要講解DMA Remapping,Interrupt Remapping會獨立講解。對於DMA Remapping,IOMMU與MMU類似。IOMMU可以將一個設備訪問地址轉換為存儲器地址,下圖針對有無IOMMU情況說明IOMMU作用。
在沒有IOMMU的情況下,網卡接收數據時地址轉換流程,RC會將網卡請求寫入地址addr1直接發送到DDR控制器,然后訪問DRAM上的addr1地址,這里的RC對網卡請求地址不做任何轉換,網卡訪問的地址必須是物理地址。
對於有IOMMU的情況,網卡請求寫入地址addr1會被IOMMU轉換為addr2,然后發送到DDR控制器,最終訪問的是DRAM上addr2地址,網卡訪問的地址addr1會被IOMMU轉換成真正的物理地址addr2,這里可以將addr1理解為虛機地址。
root@zj-x86:~# awk '{print $1,$20,$22,$68,$20,$50,$51,$52,$(NF-4),$(NF-3),$(NF-2),$(NF-1),$(NF)}' /proc/interrupts | grep -i DMAR-MSI 104: 0 0 0 0 0 0 0 0 0 DMAR-MSI 6-edge dmar6 105: 0 0 0 0 0 0 0 0 0 DMAR-MSI 5-edge dmar5 106: 0 0 0 0 0 0 0 0 0 DMAR-MSI 4-edge dmar4 107: 0 0 0 0 0 0 0 0 0 DMAR-MSI 3-edge dmar3 108: 0 0 0 0 0 0 0 0 0 DMAR-MSI 2-edge dmar2 109: 0 0 0 0 0 0 0 0 0 DMAR-MSI 1-edge dmar1 110: 0 0 0 0 0 0 0 0 0 DMAR-MSI 0-edge dmar0 111: 0 0 0 0 0 0 0 0 0 DMAR-MSI 7-edge dmar7
家知道,I/O設備可以直接存取內存,稱為DMA(Direct Memory Access);DMA要存取的內存地址稱為DMA地址(也可稱為BUS address)。在DMA技術剛出現的時候,DMA地址都是物理內存地址,簡單直接,但缺點是不靈活,比如要求物理內存必須是連續的一整塊而且不能是高位地址等等,也不能充分滿足虛擬機的需要。后來dmar就出現了。 dmar意為DMA remapping,是Intel為支持虛擬機而設計的I/O虛擬化技術,I/O設備訪問的DMA地址不再是物理內存地址,而要通過DMA remapping硬件進行轉譯,DMA remapping硬件會把DMA地址翻譯成物理內存地址,並檢查訪問權限等等。負責DMA remapping操作的硬件稱為IOMMU。做個類比:大家都知道MMU是支持內存地址虛擬化的硬件,MMU是為CPU服務的;而IOMMU是為I/O設備服務的,是將DMA地址進行虛擬化的硬件。 IOMMA不僅將DMA地址虛擬化,還起到隔離、保護等作用,如下圖所示意,詳細請參閱Intel Virtualization Technology for Directed I/O
現在我們知道了dmar的概念,那么Linux中斷信息中出現的dmar0又是什么呢? 還是用MMU作類比吧,便於理解:當CPU訪問一個在地址翻譯表中不存在的地址時,就會觸發一個fault,Linux kernel的fault處理例程會判斷這是合法地址還是非法地址,如果是合法地址,就分配相應的物理內存頁面並建立從物理地址到虛擬地址的翻譯表項,如果是非法地址,就給進程發個signal,產生core dump。IOMMU也類似,當I/O設備進行DMA訪問也可能觸發fault,有些fault是recoverable的,有些是non-recoverable的,這些fault都需要Linux kernel進行處理,所以IOMMU就利用中斷(interrupt)的方式呼喚內核,這就是我們在/proc/interrupts中看到的dmar0那一行的意思。
https://www.ershicimi.com/p/a650d2fdfa57da45fa2a69015bbd5b4e
VFIO
VFIO就是內核針對IOMMU提供的軟件框架,支持DMA Remapping和Interrupt Remapping,這里只講DMA Remapping。VFIO利用IOMMU這個特性,可以屏蔽物理地址對上層的可見性,可以用來開發用戶態驅動,也可以實現設備透傳。
2.1 概念介紹
先介紹VFIO中的幾個重要概念,主要包括Group和Container。
Group:group 是IOMMU能夠進行DMA隔離的最小硬件單元,一個group內可能只有一個device,也可能有多個device,這取決於物理平台上硬件的IOMMU拓撲結構。設備直通的時候一個group里面的設備必須都直通給一個虛擬機。不能夠讓一個group里的多個device分別從屬於2個不同的VM,也不允許部分device在host上而另一部分被分配到guest里, 因為就這樣一個guest中的device可以利用DMA攻擊獲取另外一個guest里的數據,就無法做到物理上的DMA隔離。
Container:對於虛機,Container 這里可以簡單理解為一個VM Domain的物理內存空間。對於用戶態驅動,Container可以是多個Group的集合。
上圖中PCIe-PCI橋下的兩個設備,在發送DMA請求時,PCIe-PCI橋會為下面兩個設備生成Source Identifier,其中Bus域為紅色總線號bus,device和func域為0。這樣的話,PCIe-PCI橋下的兩個設備會找到同一個Context Entry和同一份頁表,所以這兩個設備不能分別給兩個虛機使用,這兩個設備就屬於一個Group。
2.2 使用示例
這里先以簡單的用戶態驅動為例,在設備透傳小節中,在分析如何利用vfio實現透傳。
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 */ // type 1 open | attatch
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);
對於dev下Group就是按照上一節介紹的Group划分規則產生的,上述代碼描述了如何使用VFIO實現映射,對於Group和Container的相關操作這里不做過多解釋,主要關注如何完成映射,下圖解釋具體工作流程。
首先,利用mmap映射出1MB字節的虛擬空間,因為物理地址對於用戶態不可見,只能通過虛擬地址訪問物理空間。然后執行ioctl的VFIO_IOMMU_MAP_DMA命令,傳入參數主要包含vaddr及iova,其中iova代表的是設備發起DMA請求時要訪問的地址,也就是IOMMU映射前的地址,vaddr就是mmap的地址。VFIO_IOMMU_MAP_DMA命令會為虛擬地址vaddr找到物理頁並pin住(因為設備DMA是異步的,隨時可能發生,物理頁面不能交換出去),然后找到Group對應的Contex Entry,建立頁表項,頁表項能夠將iova地址映射成上面pin住的物理頁對應的物理地址上去,這樣對用戶態程序完全屏蔽了物理地址,實現了用戶空間驅動。IOVA地址的00x100000對應DRAM地址0x100000000x10100000,size為1024 * 1024。一句話概述,VFIO_IOMMU_MAP_DMA這個命令就是將iova通過IOMMU映射到vaddr對應的物理地址上去。
話說,盤古開天的時候,設備訪問內存(DMA)就只接受物理地址,所以CPU要把一個地址告訴設備,就只能給物理地址。但設備的地址長度還比CPU的總線長度短,所以只能分配低地址來給設備用。所以CPU這邊的接口就只有dma=dma_alloc(dev, size),分配了物理地址,然后映射為內核的va,然后把pa作為dma地址,CPU提供給設備,設備訪問這個dma地址,就得到內存里面的那個數據了。
后來設備做強了,雖然地址總線不長,但可以帶一個頁表,把它能訪問的有限長度的dma地址轉換為和物理空間一樣長的物理地址。這樣就有了dma=dma_map(dev, va)。這樣,其實我們對同一個物理地址就有三個地址的概念了:CPU看到的地址va,設備看到的地址dma,和總線看到的pa。
設備帶個頁表,這不就是mmu嗎?於是,通用的iommu的概念(硬件上)就被發明了。所以dma_map(dev, va),在有iommu的設備上,就變成了對iommu的通用頁表操作。iova=iommu_alloc(), iommu_map(domain, iova, pa);
這里我們發現了兩個新概念,一個是iova,這個很好理解,就是原來的dma地址了(io的va嘛),另一個是domain,這本質是一個頁表,為什么要把這個頁表獨立封裝出來,這個我們很快會看到的。
我這個需要提醒一句,iommu用的頁表,和mmu用的頁表,不是同一個頁表,為了容易區分,我們把前者叫做iopt,后者叫pt。兩者都可以翻譯虛擬地址為物理地址,物理地址是一樣的,都是pa,而對於va,前者我們叫iova,后者我們叫va。
又到了后來,人們需要支持虛擬化,提出了VFIO的概念,需要在用戶進程中直接訪問設備,那我們就要支持在用戶態直接發起DMA操作了,用戶態發起DMA,它自己在分配iova,直接設置下來,要求iommu就用這個iova,那我內核對這個設備做dma_map,也要分配iova。這兩者沖突怎么解決呢?
dma_map還可以避開用戶態請求過的va空間,用戶態的請求沒法避開內核的dma_map的呀。
VFIO這樣解決:默認情況下,iommu上會綁定一個default_domain,它具有IOMMU_DOMAIN_DMA屬性,原來怎么弄就怎么弄,這時你可以調用dma_map()。但如果你要用VFIO,你就要先detach原來的驅動,改用VFIO的驅動,VFIO就給你換一個domain,這個domain的屬性是IOMMU_DOMAIN_UNMANAGED,之后你愛用哪個iova就用那個iova,你自己保證不會沖突就好,VFIO通過iommu_map(domain, iova, pa)來執行這種映射。
等你從VFIO上detach,把你的domain刪除了,這個iommu就會恢復原來的default_domain,這樣你就可以繼續用你的dma API了。
這種情況下,你必須給你的設備選一種應用模式,非此即彼。
很多設備,比如GPU,沒有用VFIO,也會自行創建unmanaged的domain,自己管理映射,這個就變成一個通用的接口了。
好了,這個是Linux內核的現狀(截止到4.20)。如果基於這個現狀,我們要同時讓用戶態和內核態都可以做mapping的話,唯一的手段是使用unmanaged模式,然后va都從用戶態分配(比如通過mmap),然后統一用iommu_map完成這個映射。
但實際上,Linux的這個框架,已經落后於硬件的發展了。因為現在大部分IOMMU,都支持多進程訪問。比如我有兩個進程同時從用戶態訪問設備,他們自己管理iova,這樣,他們給iommu提供的iova就可能是沖突的。所以,IOMMU硬件同時支持多張iopt,用進程的id作為下標(對於PCIE設備來說,就是pasid了)。
這樣,我們可以讓內核使用pasid=0的iopt,每個用戶進程用pasid=xxx的iopt,這樣就互相不會沖突了。
為了支持這樣的應用模式,ARM的Jean Philipse做了一套補丁,為domain增加pasid支持。他的方法是domain上可以bind多個pasid,bind的時候給你分配一個io_mm,然后你用iommu_sva_map()帶上這個io_mm來做mapping。
這種情況下,你不再需要和dma api隔離了,因為他會自動用pasid=0(實際硬件不一定是這樣的接口,這只是比喻)的iopt來做dma api,用其他pasid來做用戶態。這時你也不再需要unmanaged的domain了。你繼續用dma的domain,然后bind一個pasid上去即可。
但Jean這個補丁上傳的時候正好遇到Intel的Scalable Virtual IO的補丁在上傳,Intel要用這個特性來實現更輕量級的VFIO。原來的VFIO,是整個設備共享給用戶態的,有了pasid這個概念,我可以基於pasid分配資源,基於pasid共享給用戶態。但Jean的補丁要求使用的時候就要bind一個pasid上來。但VFIO是要分配完設備,等有進程用這個設備的時候才能提供pasid。
為了解決這個問題,Jean又加了一個aux domain的概念,你可以給一個iommu創建一個primary domain,和多個aux domain。那些aux domain可以晚點再綁定pasid上來。