Linux 下的缓存和 TLB 刷新¶
- 作者:
David S. Miller <davem@redhat.com>
本文档描述了 Linux VM 子系统调用的缓存/tlb 刷新接口。它枚举了每个接口,描述了其预期目的,以及在调用该接口后预期的副作用。
下面描述的副作用是针对单处理器实现声明的,以及在该单处理器上会发生什么。SMP 情况是一个简单的扩展,您只需扩展定义,使特定接口的副作用发生在系统中的所有处理器上。不要因此而认为 SMP 缓存/tlb 刷新效率低下,事实上,这方面可以进行许多优化。例如,如果可以证明用户地址空间从未在某个 cpu 上执行过(请参阅 mm_cpumask()),则无需在该 cpu 上为此地址空间执行刷新。
首先是 TLB 刷新接口,因为它们最简单。“TLB”在 Linux 下被抽象为 cpu 用来缓存从软件页表获得的虚拟地址到物理地址的转换的东西。这意味着如果软件页表发生更改,则此“TLB”缓存中可能存在过时的转换。因此,当软件页表发生更改时,内核将在页表更改发生_之后_调用以下刷新方法之一
void flush_tlb_all(void)
所有刷新中最严重的刷新。在此接口运行后,cpu 将可以看到任何先前的页表修改。
当内核页表更改时,通常会调用此函数,因为此类转换本质上是“全局”的。
void flush_tlb_mm(struct mm_struct *mm)
此接口从 TLB 中刷新整个用户地址空间。运行后,此接口必须确保地址空间“mm”的任何先前的页表修改对于 cpu 可见。也就是说,运行后,“mm”的 TLB 中将不存在任何条目。
此接口用于处理整个地址空间页表操作,例如 fork 和 exec 期间发生的操作。
void flush_tlb_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)
这里我们正在从 TLB 中刷新特定范围的(用户)虚拟地址转换。运行后,此接口必须确保地址空间 ‘vma->vm_mm’ 中 ‘start’ 到 ‘end-1’ 范围内的任何先前的页表修改对于 cpu 可见。也就是说,运行后,TLB 中将不存在 ‘mm’ 中 ‘start’ 到 ‘end-1’ 范围内的虚拟地址的条目。
“vma”是用于该区域的后备存储。主要用于 munmap() 类型操作。
提供此接口的目的是希望端口可以找到一种合适的高效方法,从 TLB 中删除多个页面大小的转换,而不是让内核为每个可能修改的条目调用 flush_tlb_page(见下文)。
void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)
这次我们需要从 TLB 中删除 PAGE_SIZE 大小的转换。“vma”是 Linux 用来跟踪进程 mmap 区域的后备结构,地址空间可以通过 vma->vm_mm 获取。此外,可以测试 (vma->vm_flags & VM_EXEC) 以查看此区域是否可执行(因此可能在拆分 tlb 类型设置的“指令 TLB”中)。
运行后,此接口必须确保用户虚拟地址“addr”的地址空间 ‘vma->vm_mm’ 的任何先前的页表修改对于 cpu 可见。也就是说,运行后,TLB 中将不存在虚拟地址“addr”的 ‘vma->vm_mm’ 的条目。
这主要在故障处理期间使用。
void update_mmu_cache_range(struct vm_fault *vmf, struct vm_area_struct *vma, unsigned long address, pte_t *ptep, unsigned int nr)
在每次页面错误结束时,都会调用此例程,以告知体系结构特定代码,现在地址空间 “vma->vm_mm” 中虚拟地址 “address” 处存在“nr”个连续页面的软件页表中的转换。
此例程也在传递 NULL “vmf” 的各种其他位置调用。
端口可以以任何它选择的方式使用此信息。例如,它可以使用此事件为软件管理的 TLB 配置预加载 TLB 转换。sparc64 端口目前正在这样做。
接下来,我们有缓存刷新接口。一般来说,当 Linux 将现有的虚拟到物理映射更改为新值时,该序列将采用以下形式之一
1) flush_cache_mm(mm);
change_all_page_tables_of(mm);
flush_tlb_mm(mm);
2) flush_cache_range(vma, start, end);
change_range_of_page_tables(mm, start, end);
flush_tlb_range(vma, start, end);
3) flush_cache_page(vma, addr, pfn);
set_pte(pte_pointer, new_pte_val);
flush_tlb_page(vma, addr);
缓存级刷新将始终是第一个,因为这使我们能够正确处理其缓存严格并要求存在虚拟地址的虚拟到物理转换的系统,当该虚拟地址从缓存刷新时。HyperSparc cpu 就是这样一种具有此属性的 cpu。
下面的缓存刷新例程只需处理缓存刷新到特定 cpu 所需的程度即可。大多数情况下,这些例程必须为具有虚拟索引缓存的 cpu 实现,当虚拟到物理转换被更改或删除时,必须刷新这些缓存。因此,例如,IA32 处理器的物理索引物理标记缓存无需实现这些接口,因为缓存是完全同步的并且不依赖于转换信息。
以下是逐个例程
void flush_cache_mm(struct mm_struct *mm)
此接口从缓存中刷新整个用户地址空间。也就是说,运行后,将不存在与“mm”关联的缓存行。
此接口用于处理整个地址空间页表操作,例如退出和 exec 期间发生的操作。
void flush_cache_dup_mm(struct mm_struct *mm)
此接口从缓存中刷新整个用户地址空间。也就是说,运行后,将不存在与“mm”关联的缓存行。
此接口用于处理整个地址空间页表操作,例如 fork 期间发生的操作。
此选项与 flush_cache_mm 分开,以便为 VIPT 缓存进行一些优化。
void flush_cache_range(struct vm_area_struct *vma, unsigned long start, unsigned long end)
这里我们正在从缓存中刷新特定范围的(用户)虚拟地址。运行后,缓存中将不存在 ‘vma->vm_mm’ 中 ‘start’ 到 ‘end-1’ 范围内的虚拟地址的条目。
“vma”是用于该区域的后备存储。主要用于 munmap() 类型操作。
提供此接口的目的是希望端口可以找到一种合适的高效方法,从缓存中删除多个页面大小的区域,而不是让内核为每个可能修改的条目调用 flush_cache_page(见下文)。
void flush_cache_page(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn)
这次我们需要从缓存中删除 PAGE_SIZE 大小的范围。“vma”是 Linux 用来跟踪进程 mmap 区域的后备结构,地址空间可以通过 vma->vm_mm 获取。此外,可以测试 (vma->vm_flags & VM_EXEC) 以查看此区域是否可执行(因此可能在“哈佛”类型缓存布局的“指令缓存”中)。
“pfn”指示“addr”转换到的物理页帧(将此值向左移动 PAGE_SHIFT 即可获得物理地址)。应该从缓存中删除此映射。
运行后,缓存中将不存在虚拟地址“addr”的 ‘vma->vm_mm’ 的条目,该虚拟地址“addr”转换为“pfn”。
这主要在故障处理期间使用。
void flush_cache_kmaps(void)
仅当平台使用 highmem 时才需要实现此例程。它将在所有 kmaps 失效之前调用。
运行后,缓存中将不存在内核虚拟地址范围 PKMAP_ADDR(0) 到 PKMAP_ADDR(LAST_PKMAP) 的条目。
此路由应在 asm/highmem.h 中实现
void flush_cache_vmap(unsigned long start, unsigned long end)
void flush_cache_vunmap(unsigned long start, unsigned long end)
这两个接口用于刷新缓存中特定范围的(内核)虚拟地址。运行后,缓存中将不再有内核地址空间中 ‘start’ 到 ‘end-1’ 范围内的虚拟地址的条目。
这两个例程中的第一个是在 vmap_range() 安装页表项后调用的。第二个是在 vunmap_range() 删除页表项之前调用的。
还存在另一类 CPU 缓存问题,目前需要一整套不同的接口来正确处理。最大的问题是处理器数据缓存中的虚拟别名问题。
你的端口容易受到其 D 缓存中的虚拟别名影响吗?嗯,如果你的 D 缓存是虚拟索引的,其大小大于 PAGE_SIZE,并且不阻止同一物理地址的多个缓存行同时存在,那么你就有这个问题。
如果你的 D 缓存存在此问题,请首先正确定义 asm/shmparam.h SHMLBA,它本质上应该是你的虚拟寻址 D 缓存的大小(如果大小可变,则为最大可能的大小)。此设置将强制 SYSv IPC 层仅允许用户进程在地址是此值的倍数时映射共享内存。
注意
这并不能解决共享的 mmap,请查看 sparc64 端口以了解解决此问题的一种方法(特别是 SPARC_FLAG_MMAPSHARED)。
接下来,你必须解决所有其他情况下的 D 缓存别名问题。请记住,对于映射到某个用户地址空间的给定页面,始终至少存在另一个映射,即内核从 PAGE_OFFSET 开始的线性映射。因此,一旦第一个用户将其虚拟地址映射到给定的物理页面,就意味着 D 缓存别名问题有存在的可能性,因为内核已经将此页面映射到其虚拟地址。
void copy_user_page(void *to, void *from, unsigned long addr, struct page *page)
void clear_user_page(void *to, unsigned long addr, struct page *page)
这两个例程将数据存储在用户匿名页面或 COW 页面中。它允许端口有效地避免用户空间和内核之间的 D 缓存别名问题。
例如,端口可以在复制期间将 ‘from’ 和 ‘to’ 临时映射到内核虚拟地址。这两个页面的虚拟地址的选择方式是,内核加载/存储指令发生在与用户页面映射的“颜色”相同的虚拟地址上。例如,Sparc64 使用了这种技术。
‘addr’ 参数告知用户最终将此页面映射到的虚拟地址,而 ‘page’ 参数提供指向目标 struct page 的指针。
如果 D 缓存别名不是问题,这两个例程可以直接调用 memcpy/memset,而无需执行其他操作。
void flush_dcache_folio(struct folio *folio)
必须在以下情况下调用此例程:
内核写入了页面缓存页面和/或高内存中的页面
内核即将从页面缓存页面读取,并且此页面的用户空间共享/可写映射可能存在。请注意,{get,pin}_user_pages{_fast} 已经对用户地址空间中找到的任何页面调用 flush_dcache_folio,因此驱动程序代码很少需要考虑这一点。
注意
此例程只需针对可能映射到用户进程地址空间中的页面缓存页面调用。因此,例如,在页面缓存中处理 vfs 符号链接的 VFS 层代码根本不需要调用此接口。
短语“内核写入页面缓存页面”具体是指内核执行存储指令,这些指令会使该页面在内核虚拟映射中弄脏数据。这里进行刷新以处理 D 缓存别名很重要,以确保这些内核存储对于该页面的用户空间映射可见。
推论情况同样重要,如果存在对此文件具有共享 + 可写映射的用户,我们必须确保内核读取这些页面会看到用户最近完成的存储。
如果 D 缓存别名不是问题,此例程可以简单地定义为该架构上的空操作。
在 folio->flags (PG_arch_1) 中保留了一个位作为“架构私有”。内核保证,对于页面缓存页面,当此类页面首次进入页面缓存时,它将清除此位。
这使得这些接口的实现效率更高。如果当前没有用户进程映射此页面,则可以“延迟”(可能是无限期地)实际刷新。有关如何执行此操作的示例,请参阅 sparc64 的 flush_dcache_folio 和 update_mmu_cache_range 实现。
想法是,首先在 flush_dcache_folio() 时,如果
folio_flush_mapping()
返回一个映射,并且该映射上的 mapping_mapped() 返回 %false,则只需标记架构私有页面标志位。稍后,在 update_mmu_cache_range() 中,会检查此标志位,如果设置了该标志位,则会进行刷新并清除该标志位。重要
如果你延迟刷新,通常重要的是,实际刷新发生在与 cpu 将存储到页面中使其变脏的 CPU 相同。同样,请参阅 sparc64 以了解如何处理这种情况的示例。
void copy_to_user_page(struct vm_area_struct *vma, struct page *page, unsigned long user_vaddr, void *dst, void *src, int len)
void copy_from_user_page(struct vm_area_struct *vma, struct page *page, unsigned long user_vaddr, void *dst, void *src, int len)
当内核需要在任意用户页面中复制任意数据时(例如,用于 ptrace()),它将使用这两个例程。
此处应进行任何必要的缓存刷新或其他需要发生的连贯性操作。如果处理器的指令缓存不监听 CPU 存储,则很可能你需要为 copy_to_user_page() 刷新指令缓存。
void flush_anon_page(struct vm_area_struct *vma, struct page *page, unsigned long vmaddr)
当内核需要访问匿名页面的内容时,它会调用此函数(目前仅 get_user_pages())。注意:flush_dcache_folio() 特意不适用于匿名页面。默认实现为空操作(对于所有连贯的架构都应保持如此)。对于不连贯的架构,它应该刷新 vmaddr 处的页面缓存。
void flush_icache_range(unsigned long start, unsigned long end)
当内核将存储到它将执行的地址中时(例如在加载模块时),会调用此函数。
如果 icache 不监听存储,则此例程将需要刷新它。
void flush_icache_page(struct vm_area_struct *vma, struct page *page)
flush_icache_page 的所有功能都可以在 flush_dcache_folio 和 update_mmu_cache_range 中实现。将来,我们希望完全删除此接口。
最后一类 API 用于内核内故意别名的地址范围的 I/O。此类别名通过使用 vmap/vmalloc API 设置。由于内核 I/O 通过物理页面进行,因此 I/O 子系统假定用户映射和内核偏移映射是唯一的别名。对于 vmap 别名来说并非如此,因此内核中任何尝试对 vmap 区域进行 I/O 的操作都必须手动管理连贯性。它必须通过在执行 I/O 之前刷新 vmap 范围并在 I/O 返回后使其无效来实现这一点。
void flush_kernel_vmap_range(void *vaddr, int size)
刷新 vmap 区域中给定虚拟地址范围的内核缓存。这是为了确保内核在 vmap 范围中修改的任何数据对物理页面可见。该设计的目的是使此区域可以安全地执行 I/O。请注意,此 API 不 还会刷新该区域的偏移映射别名。
void invalidate_kernel_vmap_range(void *vaddr, int size) invalidates
vmap 区域中给定虚拟地址范围的缓存,这可以防止处理器在对物理页面执行 I/O 时通过推测读取数据来使缓存过时。这仅对于读取到 vmap 区域中的数据是必需的。