SWIOTLB概述
上一篇文章已经提到,IOMMU的核心功能就是,实现在low buffer和high buffer之间的sync,也就是内存内容的复制操作。
读者可能会想,内存的复制,在内核中,不就是调用memcpy()函数来实现的吗?没错,这就是本文要介绍的IOMMU的软件实现方式——SWIOTLB。之所以说是软件实现,是因为sync操作在底层正是调用memcpy()函数,这完全是软件实现的。
SWIOTLB的作用在于,使得寻址能力较低、无法直接寻址到内核所分配的DMA buffer的那些设备,也能够进行DMA操作。记住这句话——它将贯穿全文。由此,我们对本文开头图片稍作修改,制作了一个SWIOTLB的实现版本。
在目前主流的Linux操作系统中,SWIOTLB发挥作用的场合并不多见。这主要是由于以下原因:
- 现代的外部设备,通常都是32位或64位设备。64位设备毫无疑问可以直接寻址整个物理内存空间;而32位设备能够直接寻址的范围也达到了4G。如果操作系统运行内存不大于4G,则所有内存都可以被这些设备直接寻址到,此时设备的DMA操作,就无需SWIOTLB的辅助。
- 相比硬件IOMMU,SWIOTLB存在memcpy()操作,需要CPU的参与,降低了效率,这是软件实现的固有弊端。
- 后面的文章将会提到,如果启动参数中同时启用SWIOTLB和硬件IOMMU(即Intel IOMMU),那么当Linux系统启动完成后,SWIOTLB将会被禁用,而仅保留硬件IOMMU。
DMA的两种映射方式与SWIOTLB的关系
DMA映射方式包含两种,一是DMA Coherent Mapping(一致性DMA映射),二是DMA Streaming Mapping(流式DMA映射)。关于这两种映射方式的区别,网上有很多详尽的资料,故本文并不展开介绍。
读者应当注意的是,在Linux 4.0及更高的版本,只有DMA Streaming Mapping有可能触发SWIOTLB机制,而DMA Coherent Mapping与SWIOTLB没有任何联系。之所以要强调Linux 4.0及更高版本,是因为,在Linux 4.0之前的版本,DMA Coherent Mapping也会借助SWIOTLB来实现,而这一情况从Linux 4.0起就不复存在了。
SWIOTLB部分术语解释
为了让读者更好地理解本文及其他与SWIOTLB/DMA Streaming Mapping有关的文章,笔者认为还是需要解释一下一些在SWIOTLB中经常出现的术语的含义。
map:直译为映射。本质上,map是DMA Streaming Mapping中一系列函数的统称,它们反映了DMA Streaming Mapping的核心机制(下文称为"map函数")。map函数的典型代表是dma_map_single()和dma_map_sg(),其基本流程是:
- 设备调用map函数。设备调用时,会提供一个或一组位于高内存地址的页(以下称为“原始页”),作为需要被map的页。map函数需要返回被map页的DMA地址。
- map函数根据设备的寻址能力,并结合SWIOTLB启动参数,决定是否采取实际的映射行为。
- 如果设备能够直接寻址到原始页,则map函数不进行实际映射,而是直接返回原始页的DMA地址(在x86体系中,DMA地址就等于物理地址,所以实际上返回的是原始页的物理地址)。
- 如果设备不能直接寻址到原始页,则map函数将执行实际映射——在SWIOTLB buffer中申请一个页(low buffer),而后将原始页(high buffer)的内容复制过来,最后将SWIOTLB中申请的页的物理地址,作为DMA地址,返回给设备。之后,low buffer和high buffer之间需要进行sync操作以保证一致性。因此,这种实际映射会降低设备DMA效率。
- 如果启动参数指定了"swiotlb=force"(后文将会讲到启用SWIOTLB的配置),那么正如其名,即使设备能够直接寻址到原始页,map函数也会强制执行实际映射。
sync:直译为同步。在SWIOTLB机制中,如果CPU/设备向high buffer/low buffer写入内容,那么两段buffer的内容就会不一致,此时便需要进行sync,将被写入的buffer的内容,复制到另一个buffer。sync的方式很简单,就是使用memcpy()函数。
bounce buffer:bounce原意为“弹跳”,这个词形象地描述了low buffer与high buffer之间数据sync的行为,因而可以看作对SWIOTLB机制更为直观的描述。可以这么说:bounce buffer就是map + sync,二者共同构成了SWIOTLB机制。
SWIOTLB实现原理
SWIOTLB的实现原理,并不难理解。
在内核启动过程中,会使用memblock分配器,从较低的内存中,预留出一段连续物理内存(默认为64MB),用于SWIOTLB(以下称为SWIOTLB Buffer)。之所以要从低地址内存中分配SWIOTLB Buffer,原因很简单,是为了保证那些位数较低、寻址范围较小的设备,也能够寻址到SWIOTLB Buffer,从而实现sync。
对于SWIOTLB Buffer,内核会默认按照2KB的粒度,进行切分(至于为何不采用标准页大小4KB,笔者也不甚了解)。每一段2KB的连续物理内存,称为一个slab。
默认情况下,slab数目 = 64MB / 2KB = 32K = 32768。
在预留出SWIOTLB Buffer后,接下来将会初始化SWIOTLB的核心管理数据结构,它是一个名为io_tlb的数组:
static unsigned int *io_tlb_list;
io_tlb_list的每个元素,对应的正是一个slab。
io_tlb_list[i]代表:从SWIOTLB Buffer的第i个slab开始,有多少个连续slab是可用(空闲)的。这个值存在上限,用常量IO_TLB_SEGSIZE表示。IO_TLB_SEGSIZE默认值为128。 这个值是由x86 PCIe规范所确定的——x86 PCIe硬件设计决定了,每次DMA读写操作中,有效数据长度不得超过128字节。
以下图为例。io_tlb_list[0] - [1]均为0,代表第0、1个slab已经被占用。io_tlb_list[2] = 5,代表从SWIOTLB Buffer的第2个slab开始,有连续5个slab是可用的,即下标为2-6的slab是可用的。由此可以推断,io_tlb_list[2]-[6]的值分别为4、3、2、1。io_tlb_list[7]一定为0,即第7个slab必然已被占用;否则,io_tlb_list[2]应至少为6。
io_tlb_list[i]的最大值为IO_TLB_SEGSIZE,即128。我们可以把每128个连续slab看成一组。假定一个极端场景:SWIOTLB Buffer中所有slab都是空闲的。那么,此时io_tlb_list各元素的值将会是:
io_tlb_list[i] = IO_TLB_SEGSIZE - i % IO_TLB_SEGSIZE;
例如:
io_tlb_list[0] = 128
io_tlb_list[1] = 127
io_tlb_list[127] = 1
io_tlb_list[128] = 128
IO_TLB_SEGSIZE限制了DMA Streaming Mapping的最大可申请内存 —— 128 * 2KB = 256KB,也就是说,每次DMA Streaming Mapping申请,不得超过256KB的内存。如果确实需要申请超过256KB的内存呢?那么请使用DMA Coherent Mapping,这才是适用于较大内存申请的DMA方式。
当内核接收到DMA Streaming Mapping请求时,如果判定需要map(是否需要map,取决于启动参数和设备寻址能力,详见最后一部分展示的函数dma_direct_map_page()代码),则会将请求的size(以字节为单位)换算为slab数目n(向上取整)。而后,内核从上一次查找时停下的位置开始,沿下标递增方向查找(若已经到达数组末尾,则回到数组开头,继续查找)。若找到一个下标k,使得io_tlb_list[k] >= n成立,则表明找到需要分配的区段,查找停止。之后,内核将会更新io_tlb_list中对应下标元素的值。最后,返回这连续n个slab的起始地址,之后允许设备在该区段上进行DMA操作。如果遍历完io_tlb_list,仍未找到符合条件的k(遍历完的条件是,先到达数组末尾,而后从头查找,最后又回到了初始查找的下标),则拒绝此次DMA请求。
仍以上图场景为例。假设此时查找起始位置为下标2。内核接收到一个需要sync的DMA Streaming Mapping请求,其长度被换算为4个slab。由于io_tlb_list[2] = 5,5 > 4,因此找到需要分配的起始slab。此时,内核会将io_tlb_list[2]-[5]均置为0,表明它们已被分配。下次查找的起始下标变为6。最后,将下标为2的slab起始地址返回给设备,设备可以占用下标[2, 5]区间的slab(即low buffer),用于DMA。
如果其他条件不变,长度被换算为6个slab呢?显然,io_tlb_list[2] = 5,5 < 6,因而下标为2的slab无法满足需求,需要继续查找。下一个查找的下标,并不是3,而是直接跳到2 + 5 + 1 = 8。之后重复此步骤。
启用SWIOTLB的配置
Linux内核默认是禁用SWIOTLB的。如需启用,则需要分别修改.config文件和启动参数文件(在主流的Linux发行版本中,启动参数文件就是grub文件)。
.config文件
在.config文件中进行如下配置:
CONFIG_SWIOTLB=y
启动参数文件
在启动参数文件中,推荐使用:
iommu=soft intel_iommu=off # Recommended
以下方式也可,但不推荐:
swiotlb=force # Not recommended!
原因前文已经解释过——第二种方式会强制进行SWIOTLB map,即使设备能够直接寻址到DMA地址也是如此。这一配置会从整体上降低操作系统的DMA效率——因为绝大部分现代设备具备较强的寻址能力,无需实际映射,强制映射将会降低它们的DMA效率。因此,建议读者在对SWIOTLB机制尚未透彻理解的情况下,使用第一种方式,而不要使用第二种方式。
读取SWIOTLB启动参数
SWIOTLB的启动参数格式为:
swiotlb=[nslabs],[force|noforce]
解析SWIOTLB启动参数的函数是setup_io_tlb_npages():
static int __init
setup_io_tlb_npages(char *str)
{
if (isdigit(*str)) {
io_tlb_nslabs = simple_strtoul(str, &str, 0);
/* avoid tail segment of size < IO_TLB_SEGSIZE */
io_tlb_nslabs = ALIGN(io_tlb_nslabs, IO_TLB_SEGSIZE);
}
if (*str == ',')
++str;
if (!strcmp(str, "force")) {
swiotlb_force = SWIOTLB_FORCE;
} else if (!strcmp(str, "noforce")) {
swiotlb_force = SWIOTLB_NO_FORCE;
io_tlb_nslabs = 1;
}
return 0;
}
swiotlb的第一个值是反直觉的——它代表SWIOTLB Buffer中slab的数目,而非SWIOTLB Buffer的起始地址或长度。由于每个slab长度为2KB,因此,指定slab的数目,也就相当于指定SWIOTLB Buffer的长度。
如果不指定slab数目,那么在后续的函数swiotlb_init()中,SWIOTLB Buffer长度会被设置为默认的64MB。而后,默认slab数目 = 64MB / 2KB = 32K。
预留SWIOTLB Buffer
终于要讲到SWIOTLB的初始化函数swiotlb_init()。
事实上,swiotlb_init()函数并不一定会被调用。在调用此函数之前,内核还会做很多准备工作,包括根据启动参数,以及机器硬件环境,设置一些与SWIOTLB相关的全局变量,它们最终将决定swiotlb_init()函数是否会被调用。这些内容并不会在本文中介绍,而是被放到本系列的后续文章中——前文已经提到,SWIOTLB和Intel IOMMU并不会同时存留。笔者希望先向读者介绍它们各自的原理,之后再讨论内核在初始化过程中,选择保留SWIOTLB或Intel IOMMU的原因。
因此,在本文中,我们假定swiotlb_init()函数会被执行。
前文已经提到,内核在启动过程中,会使用memblock分配器,从较低的内存中,预留出SWIOTLB Buffer。这正是swiotlb_init()函数的主要工作。
void __init
swiotlb_init(int verbose)
{
/* SWIOTLB Buffer默认大小为64MB */
size_t default_size = IO_TLB_DEFAULT_SIZE;
unsigned char *vstart;
unsigned long bytes;
if (!io_tlb_nslabs) {
io_tlb_nslabs = (default_size >> IO_TLB_SHIFT);
io_tlb_nslabs = ALIGN(io_tlb_nslabs, IO_TLB_SEGSIZE);
}
/*
* io_tlb_nslabs代表bounce buffer中包含的slab个数。
* 每个slab长度为2KB(IO_TLB_SHIFT = 11,2 ^ 11 = 2K)。
*/
bytes = io_tlb_nslabs << IO_TLB_SHIFT;
/* Get IO TLB memory from the low pages */
vstart = memblock_alloc_low(PAGE_ALIGN(bytes), PAGE_SIZE);
if (vstart && !swiotlb_init_with_tbl(vstart, io_tlb_nslabs, verbose))
return;
if (io_tlb_start)
memblock_free_early(io_tlb_start,
PAGE_ALIGN(io_tlb_nslabs << IO_TLB_SHIFT));
pr_warn("Cannot allocate buffer");
no_iotlb_memory = true;
}
注意上述代码中的:
vstart = memblock_alloc_low(PAGE_ALIGN(bytes), PAGE_SIZE);
它要求内核需要从低内存地址预留SWIOTLB Buffer,以保证即使是寻址能力较为有限的设备,也能够直接访问SWIOTLB Buffer。
初始化SWIOTLB管理数据结构
函数swiotlb_init()会调用swiotlb_init_with_tbl(),后者初始化SWIOTLB管理数据结构,主要包括两个数组:一是io_tlb_list,前文已经详细介绍其功能;二是io_tlb_orig_addr,该数组的作用是保存sync的物理地址——io_tlb_orig_addr[i]保存第i个slab所映射的高地址,即本文开头的图片中high buffer的起始地址。
有了io_tlb_orig_addr,内核就可以根据SWIOTLB Buffer的slab下标,很方便地找到需要进行sync的两段内存地址。
举个例子:假设设备向下标为2的slab写入了数据,现在需要sync。则:
起始地址src = SWIOTLB Buffer起始地址 + 2 * 2K
目标地址dst = io_tlb_orig_addr[2]
后续将这两个地址传给memcpy()函数即可。
int __init swiotlb_init_with_tbl(char *tlb, unsigned long nslabs, int verbose)
{
unsigned long i, bytes;
size_t alloc_size;
bytes = nslabs << IO_TLB_SHIFT;
io_tlb_nslabs = nslabs;
io_tlb_start = __pa(tlb); /* io_tlb_list数组起始地址 */
io_tlb_end = io_tlb_start + bytes; /* io_tlb_list数组结束地址 */
/*
* Allocate and initialize the free list array. This array is used
* to find contiguous free memory regions of size up to IO_TLB_SEGSIZE
* between io_tlb_start and io_tlb_end.
*/
alloc_size = PAGE_ALIGN(io_tlb_nslabs * sizeof(int));
io_tlb_list = memblock_alloc(alloc_size, PAGE_SIZE);
if (!io_tlb_list)
panic("%s: Failed to allocate %zu bytes align=0x%lx\n",
__func__, alloc_size, PAGE_SIZE);
alloc_size = PAGE_ALIGN(io_tlb_nslabs * sizeof(phys_addr_t));
io_tlb_orig_addr = memblock_alloc(alloc_size, PAGE_SIZE);
if (!io_tlb_orig_addr)
panic("%s: Failed to allocate %zu bytes align=0x%lx\n",
__func__, alloc_size, PAGE_SIZE);
/*
* io_tlb_list[i]表示,从第i个slab开始,有多少个连续的slab是可用(可分配)的。
* 最多只允许分配128个连续slab。因此,io_tlb_list[i]的合法值是0 ~ 128之间的整数。
*
* io_tlb_orig_addr记录原始物理地址与slab index的映射关系。
*/
for (i = 0; i < io_tlb_nslabs; i++) {
io_tlb_list[i] = IO_TLB_SEGSIZE - OFFSET(i, IO_TLB_SEGSIZE);
io_tlb_orig_addr[i] = INVALID_PHYS_ADDR;
}
io_tlb_index = 0;
if (verbose)
swiotlb_print_info();
swiotlb_set_max_segment(io_tlb_nslabs << IO_TLB_SHIFT);
return 0;
}
SWIOTLB的触发时机——DMA Streaming与SWIOTLB的函数调用树
DMA Streaming共有两条路径,它们在底层最终都会调用swiotlb_map函数,从而进入SWIOTLB的路径。
第一条路径是dma_map_single,每次映射一个页:
dma_map_single ->
dma_map_single_attrs ->
dma_map_page_attrs ->
dma_direct_map_page ->
swiotlb_map ->
swiotlb_tbl_map_single ->
swiotlb_bounce ->
memcpy
第二条路径是dma_map_sg,sg是"scatter-gather"的缩写,表示每次映射一系列的页。
dma_map_sg ->
dma_map_sg_attrs ->
dma_direct_map_sg ->
dma_direct_map_page ->
swiotlb_map ->
swiotlb_tbl_map_single ->
swiotlb_bounce ->
memcpy
这里面比较重要的函数有dma_direct_map_page()和swiotlb_tlb_map_single()。
dma_direct_map_page()是高层函数,其目的是接收一个高地址的物理页,并将其映射到SWIOTLB Buffer的一个slab中。以下展示该函数的代码,其中关键在于调用swiotlb_map()前的判断条件,笔者已经用注释写明。
dma_addr_t dma_direct_map_page(struct device *dev, struct page *page,
unsigned long offset, size_t size, enum dma_data_direction dir,
unsigned long attrs)
{
phys_addr_t phys = page_to_phys(page) + offset;
dma_addr_t dma_addr = phys_to_dma(dev, phys);
/*
* 如果启动参数指定了swiotlb=force,或者设备无法直接寻址到物理页所对应的dma地址,
* 那么就会调用swiotlb_map(),触发SWIOTLB映射流程
*
* 这里需要强调的是,如果启动参数指定了swiotlb=force,那么
* swiotlb_map()会被无条件调用,即使设备能够直接寻址到原来的DMA地址。
* 这里印证了前文所述。
*/
if (unlikely(!dma_direct_possible(dev, dma_addr, size)) &&
!swiotlb_map(dev, &phys, &dma_addr, size, dir, attrs)) {
report_addr(dev, dma_addr, size);
return DMA_MAPPING_ERROR;
}
if (!dev_is_dma_coherent(dev) && !(attrs & DMA_ATTR_SKIP_CPU_SYNC))
arch_sync_dma_for_device(phys, size, dir);
return dma_addr;
}
EXPORT_SYMBOL(dma_direct_map_page);
swiotlb_tbl_map_single()则是寻找该slab的过程,原理前文已经详细解释过。这里只展示查找slab的算法对应的代码,读者可结合前文提到的SWIOTLB原理,来理解代码。
phys_addr_t swiotlb_tbl_map_single(struct device *hwdev,
dma_addr_t tbl_dma_addr,
phys_addr_t orig_addr,
size_t mapping_size,
size_t alloc_size,
enum dma_data_direction dir,
unsigned long attrs)
{
unsigned long flags;
phys_addr_t tlb_addr;
unsigned int nslots, stride, index, wrap;
int i;
unsigned long mask;
unsigned long offset_slots;
unsigned long max_slots;
unsigned long tmp_io_tlb_used;
/* ...... */
/*
* Carefully handle integer overflow which can occur when mask == ~0UL.
*/
max_slots = mask + 1
? ALIGN(mask + 1, 1 << IO_TLB_SHIFT) >> IO_TLB_SHIFT
: 1UL << (BITS_PER_LONG - IO_TLB_SHIFT);
/*
* For mappings greater than or equal to a page, we limit the stride
* (and hence alignment) to a page size.
*/
nslots = ALIGN(alloc_size, 1 << IO_TLB_SHIFT) >> IO_TLB_SHIFT;
/* ...... */
/*
* Find suitable number of IO TLB entries size that will fit this
* request and allocate a buffer from that IO TLB pool.
*/
/* ... */
wrap = index;
do {
while (iommu_is_span_boundary(index, nslots, offset_slots,
max_slots)) {
index += stride;
if (index >= io_tlb_nslabs)
index = 0;
if (index == wrap)
goto not_found;
}
/*
* If we find a slot that indicates we have 'nslots' number of
* contiguous buffers, we allocate the buffers from that slot
* and mark the entries as '0' indicating unavailable.
*/
if (io_tlb_list[index] >= nslots) {
int count = 0;
for (i = index; i < (int) (index + nslots); i++)
io_tlb_list[i] = 0;
for (i = index - 1; (OFFSET(i, IO_TLB_SEGSIZE) != IO_TLB_SEGSIZE - 1) && io_tlb_list[i]; i--)
io_tlb_list[i] = ++count;
tlb_addr = io_tlb_start + (index << IO_TLB_SHIFT);
/*
* Update the indices to avoid searching in the next
* round.
*/
io_tlb_index = ((index + nslots) < io_tlb_nslabs
? (index + nslots) : 0);
goto found;
}
index += stride;
if (index >= io_tlb_nslabs)
index = 0;
} while (index != wrap);
not_found:
/* ...... */
found:
io_tlb_used += nslots;
spin_unlock_irqrestore(&io_tlb_lock, flags);
/*
* Save away the mapping from the original address to the DMA address.
* This is needed when we sync the memory. Then we sync the buffer if
* needed.
*/
for (i = 0; i < nslots; i++)
io_tlb_orig_addr[index+i] = orig_addr + (i << IO_TLB_SHIFT);
if (!(attrs & DMA_ATTR_SKIP_CPU_SYNC) &&
(dir == DMA_TO_DEVICE || dir == DMA_BIDIRECTIONAL))
swiotlb_bounce(orig_addr, tlb_addr, mapping_size, DMA_TO_DEVICE);
return tlb_addr;
}
最后,展示一下swiotlb_bounce()函数代码。在删除了冗长而又无用的分支后(见代码注释),可以看到,这个函数是直接调用了memcpy()。注意,memcpy()需要CPU参与,这降低了效率,也与DMA的初衷背道而驰(CPU:还得我亲自出马?那我要这DMA有何用)。
正因为SWIOTLB会降低效率,因此,它只被用于少数的DMA场景中——具体来说,只有在DMA Streaming Mapping,并且设备无法直接寻址到内核分配的DMA地址时,SWIOTLB才会派上用场。
/*
* Bounce: copy the swiotlb buffer from or back to the original dma location
*/
static void swiotlb_bounce(phys_addr_t orig_addr, phys_addr_t tlb_addr,
size_t size, enum dma_data_direction dir)
{
unsigned long pfn = PFN_DOWN(orig_addr);
unsigned char *vaddr = phys_to_virt(tlb_addr);
/* 目前主流的64位机器已经没有highmem,因此完全可以忽略此分支 */
if (PageHighMem(pfn_to_page(pfn))) {
/* ...... */
} else if (dir == DMA_TO_DEVICE) {
memcpy(vaddr, phys_to_virt(orig_addr), size);
} else {
memcpy(phys_to_virt(orig_addr), vaddr, size);
}
}
更多推荐
Linux x86-64 IOMMU详解(二)——SWIOTLB(软件IOMMU)
发布评论