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()。