DMA和swiotlb¶
swiotlb是Linux核心DMA層使用的一種記憶體緩衝區分配器。當執行DMA的裝置由於硬體限制或其他要求無法直接訪問目標記憶體緩衝區時,通常會使用它。在這種情況下,DMA層呼叫swiotlb來分配一個符合限制的臨時記憶體緩衝區。DMA在此臨時記憶體緩衝區之間進行,CPU在臨時緩衝區和原始目標記憶體緩衝區之間複製資料。這種方法通常稱為“彈跳緩衝(bounce buffering)”,臨時記憶體緩衝區稱為“彈跳緩衝區(bounce buffer)”。
裝置驅動程式不直接與swiotlb互動。相反,驅動程式將它們管理的裝置的DMA屬性告知DMA層,並在程式設計裝置進行DMA時使用正常的DMA對映、解除對映和同步API。這些API利用裝置DMA屬性和核心範圍設定來確定是否需要彈跳緩衝。如果需要,DMA層管理彈跳緩衝區的分配、釋放和同步。由於DMA屬性是針對每個裝置的,因此係統中的某些裝置可能使用彈跳緩衝,而其他裝置則不使用。
由於CPU在彈跳緩衝區和原始目標記憶體緩衝區之間複製資料,所以進行彈跳緩衝比直接對原始記憶體緩衝區進行DMA要慢,並且會消耗更多的CPU資源。因此,它僅在為提供DMA功能而必要時才使用。
使用場景¶
swiotlb最初是為了處理具有定址限制的裝置的DMA而建立的。隨著物理記憶體大小超過4 GiB,某些裝置只能提供32位DMA地址。透過在4 GiB線以下分配彈跳緩衝區記憶體,這些具有定址限制的裝置仍然可以工作並執行DMA。
最近,機密計算(CoCo)虛擬機器預設對客戶機虛擬機器的記憶體進行加密,並且宿主管理程式和VMM無法訪問該記憶體。為了讓宿主代表客戶機執行I/O,I/O必須指向未加密的客戶機記憶體。CoCo虛擬機器設定了一個核心範圍的選項,強制所有DMA I/O都使用彈跳緩衝區,並且彈跳緩衝區記憶體被設定為未加密。宿主對彈跳緩衝區記憶體進行DMA I/O,Linux核心DMA層執行“同步”操作,使CPU將資料複製到/從原始目標記憶體緩衝區。CPU複製操作在未加密和加密記憶體之間架起橋樑。這種彈跳緩衝區的使用使得裝置驅動程式在CoCo虛擬機器中可以“正常工作”,無需修改即可處理記憶體加密的複雜性。
彈跳緩衝區還出現其他邊緣情況。例如,當為DMA操作設定IOMMU對映以用於被視為“不受信任”的裝置時,該裝置應僅被授予訪問包含傳輸資料的記憶體的許可權。但如果該記憶體僅佔用IOMMU粒度的一部分,則粒度的其他部分可能包含不相關的核心資料。由於IOMMU訪問控制是按粒度進行的,不受信任的裝置可以訪問不相關的核心資料。透過對DMA操作進行彈跳緩衝並確保彈跳緩衝區的未使用部分不包含任何不相關的核心資料,可以解決此問題。
核心功能¶
主要的swiotlb API是swiotlb_tbl_map_single()和swiotlb_tbl_unmap_single()。“map”API分配指定大小(以位元組為單位)的彈跳緩衝區,並返回該緩衝區的物理地址。緩衝區記憶體是物理連續的。期望DMA層將物理記憶體地址對映到DMA地址,並將DMA地址返回給驅動程式以程式設計到裝置中。如果DMA操作指定多個記憶體緩衝區段,則必須為每個段分配一個單獨的彈跳緩衝區。swiotlb_tbl_map_single()始終執行“同步”操作(即CPU複製),以初始化彈跳緩衝區以匹配原始緩衝區的內容。
swiotlb_tbl_unmap_single()執行相反的操作。如果DMA操作可能更新了彈跳緩衝區記憶體且未設定DMA_ATTR_SKIP_CPU_SYNC,則unmap會執行“同步”操作,使CPU將資料從彈跳緩衝區複製回原始緩衝區。然後釋放彈跳緩衝區記憶體。
swiotlb還提供與dma_sync_*() API對應的“同步”API,驅動程式可以在緩衝區控制在CPU和裝置之間轉換時使用這些API。swiotlb“同步”API使CPU在原始緩衝區和彈跳緩衝區之間複製資料。與dma_sync_*() API一樣,swiotlb“同步”API支援執行部分同步,其中只有彈跳緩衝區的子集被複制到/從原始緩衝區。
核心功能約束¶
swiotlb的map/unmap/sync API必須是非阻塞的,因為它們由相應的DMA API呼叫,而DMA API可能在不允許阻塞的上下文中執行。因此,swiotlb分配的預設記憶體池必須在啟動時預分配(但請參閱下面的動態swiotlb)。由於swiotlb分配必須是物理連續的,因此整個預設記憶體池被分配為一個單一的連續塊。
需要預分配預設的swiotlb池會在啟動時造成權衡。池應該足夠大,以確保彈跳緩衝區請求總能得到滿足,因為非阻塞要求意味著請求不能等待空間可用。但是,一個大的池可能會浪費記憶體,因為這種預分配的記憶體不可用於系統中的其他用途。這種權衡在所有DMA I/O都使用彈跳緩衝區的CoCo虛擬機器中尤為突出。這些虛擬機器使用啟發式方法將預設池大小設定為記憶體的~6%,最大為1 GiB,這可能非常浪費記憶體。相反,根據虛擬機器中工作負載的I/O模式,啟發式方法可能生成一個不足的大小。下面描述的動態swiotlb功能可以提供幫助,但有侷限性。更好地管理swiotlb預設記憶體池大小仍然是一個開放問題。
從swiotlb進行的單次分配限制為IO_TLB_SIZE * IO_TLB_SEGSIZE位元組,根據當前定義為256 KiB。當裝置的DMA設定使得裝置可能使用swiotlb時,DMA段的最大大小必須限制在該256 KiB。該值透過dma_map_mapping_size()和swiotlb_max_mapping_size()傳遞給更高級別的核心程式碼。如果更高級別的程式碼未能考慮此限制,它可能會發出對swiotlb來說過大的請求,並收到“swiotlb full”錯誤。
一個關鍵的裝置DMA設定是“min_align_mask”,它是一個2的冪減1,使得一些低位被設定,或者它可以為零。swiotlb分配確保彈跳緩衝區物理地址的這些min_align_mask位與原始緩衝區地址中的相同位匹配。當min_align_mask非零時,它可能會在彈跳緩衝區的地址中產生一個“對齊偏移”,從而略微減小分配的最大大小。這種潛在的對齊偏移反映在swiotlb_max_mapping_size()返回的值中,這可能出現在諸如/sys/block/<device>/queue/max_sectors_kb之類的位置。例如,如果裝置不使用swiotlb,max_sectors_kb可能是512 KiB或更大。如果裝置可能使用swiotlb,max_sectors_kb將是256 KiB。當min_align_mask非零時,max_sectors_kb可能更小,例如252 KiB。
swiotlb_tbl_map_single()還接受一個“alloc_align_mask”引數。該引數指定彈跳緩衝區空間的分配必須從物理地址的alloc_align_mask位為零的位置開始。但如果min_align_mask非零,實際的彈跳緩衝區可能從更大的地址開始。因此,在彈跳緩衝區開始之前可能分配有預填充空間。類似地,彈跳緩衝區的末尾會向上舍入到alloc_align_mask邊界,可能導致後填充空間。任何預填充或後填充空間都不會由swiotlb程式碼初始化。“alloc_align_mask”引數在IOMMU程式碼為不受信任的裝置進行對映時使用。它被設定為粒度大小-1,以便彈跳緩衝區完全從不用於任何其他目的的粒度中分配。
資料結構概念¶
用於swiotlb彈跳緩衝區的記憶體是從整個系統記憶體中作為一個或多個“池”分配的。預設池在系統啟動期間分配,預設大小為64 MiB。預設池大小可以透過“swiotlb=”核心啟動行引數修改。如上所述,預設大小也可以根據其他條件進行調整,例如在CoCo虛擬機器中執行。如果啟用了CONFIG_SWIOTLB_DYNAMIC,則可以在系統生命週期的後期分配額外的池。每個池必須是物理記憶體的連續範圍。預設池分配在4 GiB物理地址線以下,因此它適用於只能定址32位物理記憶體的裝置(除非架構特定的程式碼提供了SWIOTLB_ANY標誌)。在CoCo虛擬機器中,池記憶體在使用swiotlb之前必須解密。
每個池被劃分為大小為IO_TLB_SIZE的“槽”,根據當前定義為2 KiB。IO_TLB_SEGSIZE個連續槽(128個槽)構成所謂的“槽集”。當分配彈跳緩衝區時,它佔用一個或多個連續槽。一個槽從不被多個彈跳緩衝區共享。此外,彈跳緩衝區必須從單個槽集中分配,這導致最大彈跳緩衝區大小為IO_TLB_SIZE * IO_TLB_SEGSIZE。如果滿足對齊和大小約束,多個較小的彈跳緩衝區可以共存於一個槽集中。
槽也被分組到“區域”中,約束是一個槽集完全存在於一個區域中。每個區域都有自己的自旋鎖,必須持有該鎖才能操作該區域中的槽。這種區域劃分避免了在swiotlb大量使用時(例如在CoCo虛擬機器中)爭用單個全域性自旋鎖。區域數量預設為系統中CPU的數量,以實現最大並行度,但由於一個區域不能小於IO_TLB_SEGSIZE個槽,因此可能需要將多個CPU分配到同一個區域。區域數量也可以透過“swiotlb=”核心啟動引數設定。
分配彈跳緩衝區時,如果與呼叫CPU關聯的區域沒有足夠的可用空間,則會按順序嘗試與其他CPU關聯的區域。對於嘗試的每個區域,必須在嘗試分配之前獲取該區域的自旋鎖,因此如果swiotlb整體相對繁忙,可能會發生爭用。但是,只有當所有區域都沒有足夠的可用空間時,分配請求才會失敗。
IO_TLB_SIZE、IO_TLB_SEGSIZE和區域的數量都必須是2的冪,因為程式碼使用移位和位掩碼進行許多計算。如有必要,區域數量會向上舍入到2的冪,以滿足此要求。
預設池以PAGE_SIZE對齊方式分配。如果swiotlb_tbl_map_single()的alloc_align_mask引數指定了更大的對齊方式,則每個槽集中的一個或多個初始槽可能不滿足alloc_align_mask標準。因為彈跳緩衝區分配不能跨越槽集邊界,所以消除這些初始槽會有效地減小彈跳緩衝區的最大大小。目前,這不是問題,因為alloc_align_mask是根據IOMMU粒度大小設定的,並且粒度不能大於PAGE_SIZE。但如果將來發生變化,初始池分配可能需要以大於PAGE_SIZE的對齊方式進行。
動態swiotlb¶
當啟用CONFIG_SWIOTLB_DYNAMIC時,swiotlb可以按需擴充套件可用作彈跳緩衝區的記憶體量。如果彈跳緩衝區請求因可用空間不足而失敗,則會啟動一個非同步後臺任務,從通用系統記憶體中分配記憶體並將其轉換為swiotlb池。建立額外的池必須非同步進行,因為記憶體分配可能會阻塞,如上所述,swiotlb請求不允許阻塞。一旦後臺任務啟動,彈跳緩衝區請求會建立一個“瞬時池”以避免返回“swiotlb full”錯誤。瞬時池的大小與彈跳緩衝區請求的大小相同,並在彈跳緩衝區釋放時刪除。此瞬時池的記憶體來自通用系統記憶體原子池,因此建立不會阻塞。建立瞬時池的成本相對較高,尤其是在記憶體必須解密的CoCo虛擬機器中,因此它僅作為權宜之計,直到後臺任務可以新增另一個非瞬時池。
新增動態池有侷限性。與預設池一樣,記憶體必須是物理連續的,因此大小限制為MAX_PAGE_ORDER頁(例如,典型x86系統上為4 MiB)。由於記憶體碎片,最大大小分配可能不可用。動態池分配器會嘗試較小的大小,直到成功,但最小大小為1 MiB。考慮到足夠的系統記憶體碎片,動態新增池可能根本不會成功。
動態池中的區域數量可能與預設池中的區域數量不同。由於新池的大小通常最多隻有幾MiB,因此區域數量可能會更少。例如,新池大小為4 MiB,最小區域大小為256 KiB,只能建立16個區域。如果系統有超過16個CPU,多個CPU必須共享一個區域,從而導致更多的鎖爭用。
透過動態swiotlb新增的新池以線性列表形式連結在一起。swiotlb程式碼經常需要搜尋包含特定swiotlb物理地址的池,因此該搜尋是線性的,在大量動態池的情況下效能不佳。可以改進資料結構以加快搜索速度。
總的來說,動態swiotlb最適合具有相對較少CPU的小型配置。它允許預設的swiotlb池更小,從而避免記憶體浪費,並在需要時透過動態池提供更多空間(只要碎片不是障礙)。它對大型CoCo虛擬機器的作用較小。
資料結構詳情¶
swiotlb由四個主要資料結構管理:io_tlb_mem、io_tlb_pool、io_tlb_area和io_tlb_slot。io_tlb_mem描述了一個swiotlb記憶體分配器,其中包括預設記憶體池以及與之連結的任何動態或瞬時池。swiotlb使用情況的有限統計資料按記憶體分配器儲存,並存儲在該資料結構中。當設定CONFIG_DEBUG_FS時,這些統計資料在/sys/kernel/debug/swiotlb下可用。
io_tlb_pool描述了一個記憶體池,可以是預設池、動態池或瞬時池。描述包括池中記憶體的起始和結束地址、指向io_tlb_area結構陣列的指標,以及指向與池關聯的io_tlb_slot結構陣列的指標。
io_tlb_area描述了一個區域。主要欄位是用於序列化訪問該區域中槽的自旋鎖。池的io_tlb_area陣列為每個區域都有一個條目,並使用從呼叫處理器ID派生的基於0的區域索引進行訪問。區域的存在僅僅是為了允許從多個CPU並行訪問swiotlb。
io_tlb_slot描述了池中的單個記憶體槽,大小為IO_TLB_SIZE(目前為2 KiB)。io_tlb_slot陣列透過從彈跳緩衝區地址相對於池的起始記憶體地址計算的槽索引進行索引。struct io_tlb_slot的大小為24位元組,因此開銷約為槽大小的1%。
io_tlb_slot陣列旨在滿足幾個要求。首先,DMA API和相應的swiotlb API使用彈跳緩衝區地址作為彈跳緩衝區的識別符號。此地址由swiotlb_tbl_map_single()返回,然後作為引數傳遞給swiotlb_tbl_unmap_single()和swiotlb_sync_*()函式。原始記憶體緩衝區地址顯然必須作為引數傳遞給swiotlb_tbl_map_single(),但它不傳遞給其他API。因此,swiotlb資料結構必須儲存原始記憶體緩衝區地址,以便在執行同步操作時使用。此原始地址儲存在io_tlb_slot陣列中。
其次,io_tlb_slot陣列必須處理部分同步請求。在這種情況下,swiotlb_sync_*()的引數不是彈跳緩衝區起始地址,而是彈跳緩衝區中間的某個地址,並且swiotlb程式碼不知道彈跳緩衝區的起始地址。但swiotlb程式碼必須能夠計算出相應的原始記憶體緩衝區地址,以便執行“同步”所指示的CPU複製。因此,調整後的原始記憶體緩衝區地址會填充到彈跳緩衝區佔用的每個槽的struct io_tlb_slot中。彈跳緩衝區的調整後“alloc_size”也記錄在每個struct io_tlb_slot中,以便對“同步”操作的大小執行健全性檢查。“alloc_size”欄位除了健全性檢查外不使用。
第三,io_tlb_slot陣列用於跟蹤可用槽位。struct io_tlb_slot中的“list”欄位記錄了從該槽位開始有多少個連續的可用槽位。“0”表示該槽位已被佔用。“1”表示只有當前槽位可用。“2”表示當前槽位和下一個槽位可用,以此類推。最大值為IO_TLB_SEGSIZE,可以出現在槽集中的第一個槽位,表示整個槽集可用。這些值在搜尋用於新彈跳緩衝區的可用槽位時使用。它們在新彈跳緩衝區分配和釋放時更新。在池建立時,對於每個槽集中的槽位,“list”欄位從IO_TLB_SEGSIZE初始化到1。
第四,io_tlb_slot陣列跟蹤為滿足上述alloc_align_mask要求而分配的任何“填充槽”。當swiotlb_tbl_map_single()分配彈跳緩衝區空間以滿足alloc_align_mask要求時,它可能會跨零個或多個槽分配預填充空間。但是當使用彈跳緩衝區地址呼叫swiotlb_tbl_unmap_single()時,支配分配的alloc_align_mask值(以及任何填充槽的分配)是未知的。“pad_slots”欄位記錄填充槽的數量,以便swiotlb_tbl_unmap_single()可以釋放它們。“pad_slots”值僅記錄在分配給彈跳緩衝區的第一個非填充槽中。
受限池¶
swiotlb機制也用於“受限池”,這些池是獨立於預設swiotlb池的記憶體池,專門用於特定裝置的DMA使用。受限池在硬體保護能力有限的系統(例如缺乏IOMMU的系統)上提供了一定級別的DMA記憶體保護。這種用法由裝置樹條目指定,並要求設定CONFIG_DMA_RESTRICTED_POOL。每個受限池都基於其自己的io_tlb_mem資料結構,該結構獨立於主swiotlb io_tlb_mem。
受限池添加了swiotlb_alloc()和swiotlb_free() API,這些API是從dma_alloc_*()和dma_free_*() API呼叫的。swiotlb_alloc/free() API直接從/向受限池分配/釋放槽,並且不透過swiotlb_tbl_map/unmap_single()。