Linux 下的缓存和 TLB 刷新

作者:

David S. Miller <davem@redhat.com>

本文档描述了 Linux VM 子系统调用的缓存/TLB 刷新接口。它列举了每个接口,描述了其预期目的,以及调用接口后预期的副作用。

下面描述的副作用是针对单处理器实现的,以及在该单个处理器上会发生什么。SMP 情况是一个简单的扩展,即您只需扩展定义,以便特定接口的副作用发生在系统中的所有处理器上。不要让这吓到您,认为 SMP 缓存/TLB 刷新效率低下,这实际上是一个可以进行许多优化的地方。例如,如果可以证明用户地址空间从未在某个 CPU 上执行(参见 mm_cpumask()),则无需在该 CPU 上对该地址空间执行刷新。

首先是 TLB 刷新接口,因为它们最简单。“TLB”在 Linux 下被抽象为 CPU 用于缓存从软件页表获得的虚拟到物理地址转换的东西。这意味着如果软件页表发生变化,这个“TLB”缓存中可能存在过时的转换。因此,当软件页表发生变化时,内核将在页表变化发生_之后_调用以下刷新方法之一。

  1. void flush_tlb_all(void)

    所有刷新中最严重的。在此接口运行后,任何以前的页表修改都将对 CPU 可见。

    这通常在内核页表更改时调用,因为此类转换本质上是“全局的”。

  2. void flush_tlb_mm(struct mm_struct *mm)

    此接口从 TLB 中刷新整个用户地址空间。运行后,此接口必须确保地址空间“mm”的任何以前的页表修改都将对 CPU 可见。也就是说,运行后,TLB 中将不存在“mm”的任何条目。

    此接口用于处理整个地址空间页表操作,例如 fork 和 exec 期间发生的操作。

  3. 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(见下文)。

  4. 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”中)。

    运行后,此接口必须确保地址空间“vma->vm_mm”中用户虚拟地址“addr”的任何以前的页表修改都将对 CPU 可见。也就是说,运行后,TLB 中将不存在“vma->vm_mm”中虚拟地址“addr”的条目。

    这主要在缺页处理期间使用。

  5. 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 处理器的物理索引物理标记缓存不需要实现这些接口,因为缓存是完全同步的并且不依赖于转换信息。

以下是例程,逐一介绍:

  1. void flush_cache_mm(struct mm_struct *mm)

    此接口将整个用户地址空间从缓存中刷新。也就是说,运行后,将没有与“mm”关联的缓存行。

    此接口用于处理整个地址空间页表操作,例如在退出和执行期间发生的操作。

  2. void flush_cache_dup_mm(struct mm_struct *mm)

    此接口将整个用户地址空间从缓存中刷新。也就是说,运行后,将没有与“mm”关联的缓存行。

    此接口用于处理整个地址空间页表操作,例如在 fork 期间发生的操作。

    此选项与 flush_cache_mm 分开,以允许对 VIPT 缓存进行一些优化。

  3. 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(见下文)。

  4. 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 可获得物理地址)。应从缓存中删除的就是此映射。

    运行后,缓存中将不存在“vma->vm_mm”的虚拟地址“addr”转换为“pfn”的条目。

    这主要在缺页处理期间使用。

  5. void flush_cache_kmaps(void)

    只有当平台使用高内存时,才需要实现此例程。它将在所有 kmap 失效之前被调用。

    运行后,内核虚拟地址范围 PKMAP_ADDR(0) 到 PKMAP_ADDR(LAST_PKMAP) 的缓存中将没有条目。

    此路由应在 asm/highmem.h 中实现

  6. 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 缓存问题,目前需要一套完全不同的接口才能妥善处理。最大的问题是处理器数据缓存中的虚拟别名(virtual aliasing)。

您的移植是否容易受到 D-cache 中虚拟别名的影响?好吧,如果您的 D-cache 是虚拟索引的,大小大于 PAGE_SIZE,并且不阻止同一物理地址的多个缓存行同时存在,那么您就有这个问题。

如果您的 D-cache 有这个问题,首先请在 asm/shmparam.h 中正确定义 SHMLBA,它本质上应该是您的虚拟寻址 D-cache 的大小(如果大小可变,则为最大可能大小)。此设置将强制 SYSv IPC 层只允许用户进程将共享内存 mmap 到此值倍数的地址。

注意

这并不能解决共享 mmap 的问题,请查看 sparc64 移植以了解一种解决方案(特别是 SPARC_FLAG_MMAPSHARED)。

接下来,您必须解决所有其他情况下的 D-cache 别名问题。请记住,对于映射到某些用户地址空间中的给定页面,总是至少存在一个附加映射,即内核在其从 PAGE_OFFSET 开始的线性映射中的映射。因此,一旦第一个用户将给定物理页面映射到其地址空间中,D-cache 别名问题就可能立即存在,因为内核已经将此页面映射到其虚拟地址。

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-cache 别名问题。

例如,移植可以在复制期间将“from”和“to”临时映射到内核虚拟地址。这两个页面的虚拟地址选择方式是,内核的加载/存储指令发生到与页面的用户映射具有相同“颜色”的虚拟地址。例如,Sparc64 使用了这种技术。

“addr”参数告诉用户最终将此页面映射到的虚拟地址,而“page”参数提供指向目标页面的 struct page 的指针。

如果 D-cache 别名不是问题,这两个例程可以直接调用 memcpy/memset,无需做更多事情。

void flush_dcache_folio(struct folio *folio)

在以下情况下必须调用此例程:

  1. 内核已写入页面缓存页面和/或高内存中的页面。

  2. 内核即将从页面缓存页面读取,并且可能存在此页面的用户空间共享/可写映射。请注意,{get,pin}_user_pages{_fast} 已经对用户地址空间中找到的任何页面调用了 flush_dcache_folio,因此驱动程序代码很少需要考虑这一点。

注意

此例程只需要为可能映射到用户进程地址空间的页面缓存页面调用。因此,例如,处理页面缓存中 vfs 符号链接的 VFS 层代码根本不需要调用此接口。

“内核写入页面缓存页面”这一短语特指内核执行存储指令,在页面的内核虚拟映射处弄脏该页面中的数据。在这里刷新很重要,以处理 D-cache 别名,确保这些内核存储对于该页面的用户空间映射可见。

推论情况同样重要,如果存在对该文件有共享+可写映射的用户,我们必须确保内核读取这些页面时会看到用户最近的存储。

如果 D-cache 别名不是问题,则此例程在该架构上可以简单地定义为 nop。

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() 故意不适用于匿名页面。默认实现是一个 nop(并且对于所有一致性架构都应保持如此)。对于非一致性架构,它应刷新 vmaddr 处页面的缓存。

void flush_icache_range(unsigned long start, unsigned long end)

当内核存储到它将执行的地址中时(例如加载模块时),会调用此函数。

如果 I-cache 不监听存储,则此例程需要刷新它。

void flush_icache_page(struct vm_area_struct *vma, struct 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) invalidate

使 vmap 区域中给定虚拟地址范围的缓存失效,这可防止处理器在对物理页面进行 I/O 时通过推测性读取数据而使缓存过期。这仅适用于数据读取到 vmap 区域。