HyperNews Linux KHG 讨论页面

1. 参与者

TLB。
就 Linux 刷新架构而言,这更多的是一个虚拟实体,而不是一个严格的模型。它唯一的特性是
  1. 它以某种方式跟踪进程/内核映射,无论是在软件还是硬件中。
  2. 当内核更改进程/内核映射时,可能需要通知特定于架构的代码。
缓存。
这个实体本质上是刷新架构所认为的“内存状态”。一般来说,它具有以下属性
  1. 它将始终保存数据的副本,这些副本将被本地处理器视为最新的。
  2. 它的正常运行可能与 TLB 和进程/内核页面映射在某种程度上相关,也就是说它们可能相互依赖。
  3. 在虚拟缓存配置中,如果一个物理页同时映射到两个虚拟页,并且由于用于索引缓存线的地址位,同一数据块可能最终在缓存中驻留两次,从而导致不一致问题。
  4. 设备和 DMA 可能能够或可能不能够看到驻留在本地处理器缓存中的数据块的最新副本。
  5. 目前,假设多处理器环境中的一致性由缓存/内存子系统维护。也就是说,当一个处理器在内存总线上请求数据,而另一个处理器拥有更新的副本时,无论通过何种方式,请求者都将获得另一个处理器拥有的更新副本。
(注意:实际上可能存在没有硬件缓存一致性机制的 SMP 架构,当前的刷新架构目前不处理这种情况。如果在某个时候,Linux 移植到某个系统时出现这个问题,我将添加必要的钩子。但这不会很漂亮。)

2. 刷新架构关心的内容

  1. 在任何时候,内存管理硬件对一组进程/内核映射的视图都将与内核页表的视图保持一致。
  2. 如果内存管理内核代码通过内核空间别名修改底层物理页的数据,从而修改用户进程页,则用户控制线程将在允许继续执行之前看到正确的数据,而与缓存架构和/或语义无关。
  3. 一般来说,当地址空间状态发生更改时(代表通用内核内存管理代码),将调用适当的刷新架构钩子,完整地描述该状态更改。

3. 刷新架构不关心的内容

  1. DMA/驱动程序一致性。这包括 DMA 映射(在 MMU 映射的意义上)和缓存/DMA 数据一致性。这些类型的问题与刷新架构无关,请参阅下文了解应如何处理它们。
  2. 关于信号分派代码对进程指令空间进行的修改,指令/数据缓存拆分一致性。同样,请参阅下文了解应如何以另一种方式处理此问题。

4. 刷新架构的接口以及如何实现它们

一般来说,下面描述的所有例程都将按以下顺序调用
	flush_cache_foo(...);
	modify_address_space();
	flush_tlb_foo(...);
这里的逻辑是
  1. 在给定的架构中,当不存在该数据的映射时,缓存数据存在可能是非法的,因此刷新必须在更改之前发生。
  2. 给定的 MMU/TLB 架构可能会执行内核页表的硬件表遍历。因此,TLB 刷新是在页表更改后完成的,以便之后硬件只能将页表信息的新副本加载到 TLB 中。

void flush_cache_all(void);
void flush_tlb_all(void);
这些例程用于通知特定于架构的代码,内核地址空间映射已更改,这意味着每个进程的映射都已有效地更改。

一个实现应该

  1. 消除在此时有效的当flush_cache_all被调用时的所有缓存条目。这适用于虚拟缓存架构。如果缓存本质上是写回式的,则此例程应在使每个条目无效之前将缓存数据提交到内存。对于物理缓存,无需执行任何操作,因为物理映射与地址空间转换无关。
  2. 对于flush_tlb_all,内核地址空间的所有 TLB 映射都应通过任何必要的方式与 OS 页表保持一致。请注意,对于具有“MMU/TLB 上下文”概念的架构,可能需要在每个“活动”MMU/TLB 上下文中执行此同步。

void flush_cache_mm(struct mm_struct *mm);
void flush_tlb_mm(struct mm_struct *mm);
这些例程通知系统,由mm_struct传递的整个地址空间正在更改。请特别注意以下两点
  1. 这个mm_struct是 mmu/tlb 实际范围的单元,就刷新架构而言。特别是,一个mm_struct可能映射到一个或多个任务,也可能不映射到任何任务!
  2. 此“地址空间”更改被认为仅发生在用户空间中。因此,如果为了提高效率,代码可以避免刷新内核 tlb/缓存条目,那是安全的。
一个实现应该
  1. 对于flush_cache_mm,无论虚拟缓存中可能存在哪些用于由mm_struct描述的地址空间的条目,都将被无效化。
  2. 对于flush_tlb_mm,tlb/mmu 硬件将被置于一种状态,在该状态下,它将看到(现在最新的)内核页表条目,这些条目用于由mm_struct.

flush_cache_range(struct mm_struct *mm, unsigned long start,
                  unsigned long end);
flush_tlb_range(struct mm_struct *mm, unsigned long start,
                unsigned long end);
传递的地址空间。在由mm_struct传递的地址空间中,用户地址的特定范围发生了更改。上面关于flush_*_mm()的两个注释,关于mm_struct传递的 mm_struct 也适用于此处。

一个实现应该

  1. 对于flush_cache_range,在虚拟缓存系统上,对于由mm_struct描述的地址空间的条目,都将被无效化。
  2. 对于flush_tlb_range,需要执行任何必要的操作,以使 MMU/TLB 硬件不包含过时的转换。这意味着,无论内核页表中的哪些转换,在由mm_struct描述的地址空间中的范围开始到结束,都将是内存管理硬件从现在开始将看到的,通过任何方式。

void flush_cache_page(struct vm_area_struct *vma, unsigned long address);
void flush_tlb_page(struct vm_area_struct *vma, unsigned long address);
address处的单个页面,在由vm_area_struct传递的地址空间内的用户空间中,正在发生更改。如果需要,实现可以获得与此地址空间关联的mm_structmm_struct,通过vma->vm_mm。传递 VMA 是为了方便实现可以检查vma->vm_flags。这样,在指令和数据空间未统一的实现中,可以检查是否在VM_EXEC。传递 VMA 是为了方便实现可以检查中设置了

vma->vm_flagsflush_*_mm()的两个注释,关于mm_struct,例如,可能避免刷新指令空间。,通过上面关于

一个实现应该

  1. 对于flush_cache_rangeflush_*_mm()address,在虚拟缓存系统上,对于 VMA 描述的地址空间中
  2. 对于flush_tlb_rangeflush_tlb_pageaddress,需要执行任何必要的操作,以使 MMU/TLB 硬件不包含过时的转换。这意味着,无论内核页表中的哪些转换,对于 VMA 传递的地址空间中

void flush_page_to_ram(unsigned long page);
这是一个丑小鸭。但它的语义在如此多的架构上是必要的,以至于我需要将其添加到 Linux 的刷新架构中。

简而言之,当(例如)内核服务于 COW 故障时,它使用内核空间中所有物理内存的别名映射来执行将有问题页面复制到新页面的操作。对于本质上是写回式的虚拟索引缓存,这会带来问题。在这种情况下,内核会触及内核空间中的两个物理页。这里描述的代码序列本质上看起来像

do_wp_page()
{
	[ ... ]
		copy_cow_page(old_page,new_page);
		flush_page_to_ram(old_page);
		flush_page_to_ram(new_page);
		flush_cache_page(vma, address);
		modify_address_space();
		free_page(old_page);
		flush_tlb_page(vma, address);
	[ ... ]
}
(为了举例说明,一些实际代码已被简化。)

考虑一个虚拟索引的写回式缓存。在页面复制到内核空间别名的时间点,原始页面的用户空间视图可能在缓存中(在用户的地址,即故障发生的位置)。页面复制可以将此数据(对于旧页面)带入缓存。它还将数据(在新页面的内核别名映射处)复制到缓存中,并且对于写回式缓存,此数据将在缓存中是脏的或已修改的。

在这种情况下,主内存将看不到数据的最新副本。缓存是愚蠢的,因此对于我们提供给用户的新页面,如果不强制将内核别名处的缓存数据刷新到主内存,则进程将看到页面的旧内容(即 COW 处理在上面完成的复制之前的任何垃圾)。

刚刚描述的具体示例

考虑一个进程,该进程以只读方式与另一个任务(或多个任务)在用户空间中的虚拟地址 0x2000 处共享一个页面。为了示例目的,我们假设此虚拟地址映射到物理页面 0x14000。

		Virtual Pages
	task 1	--------------
	        | 0x00000000 |
		--------------
	        | 0x00001000 |			Physical Pages
		--------------			--------------
		| 0x00002000 | --\		| 0x00000000 |
		--------------    \		--------------
                                   \		| ...        |
	task 2  --------------      \		--------------
		| 0x00000000 |      |---->	| 0x00014000 |
		--------------      /		--------------
		| 0x00001000 |     /		| ...        |
		--------------    /		--------------
		| 0x00002000 | --/
		--------------
如果任务 2 尝试写入地址 0x2000 处的只读页面,我们将收到故障,并最终到达do_wp_page().

内核将为任务 2 获取一个新页面,假设这是物理页面 0x26000,并且我们还假设物理页面 0x14000 和 0x26000 的内核别名映射可以同时驻留在两个唯一的缓存行中,这基于此缓存的行索引方案。

页面内容从物理页面 0x14000 的内核映射复制到物理页面 0x26000 的内核映射。

此时,在写回式虚拟索引缓存架构上,我们存在潜在的不一致性。复制到物理页面 0x26000 中的新数据此时不一定在主内存中,实际上它可能全部仅在缓存中,位于物理地址的内核别名处。此外,原始(旧)页面的(未修改的,即干净的)数据在物理页面 0x14000 的内核别名处的缓存中,这可能会在以后产生不一致性,因此为了安全起见,最好也消除此数据的缓存副本。

假设我们没有写回页面 0x26000 的数据,而是让它停留在那里。我们将返回到任务 2(它现在在虚拟地址 0x2000 处映射了此新页面),他将完成他的写入,然后他将读取此新页面中的其他一些数据(即,期望之前存在的内容)。此时,如果数据保留在内核别名处,用于新物理页面的缓存中,则用户将获得复制之前主内存中的任何内容。这可能会导致灾难性的结果。

因此,架构应

在虚拟索引缓存架构上,执行任何必要的操作,以使主内存与内核空间页面的缓存副本保持一致。

注意:实际上,对于非写回式虚拟缓存,此例程也必须使行无效。要了解为什么这确实是必要的,请使用任务 1 和 2 重演上述示例,但这次在 COW 故障发生之前fork()再派生一个任务 3,考虑如果以下序列完全连续发生,内核和用户空间中缓存的内容

  1. 任务 1 读取地址 0x2000 处的页面
  2. 任务 2 COW 故障地址 0x2000 处的页面
  3. 任务 2 执行他对地址 0x2000 处新页面的写入
  4. 任务 3 COW 故障地址 0x2000 处的页面
即使在非写回式虚拟索引缓存上,如果flush_page_to_ram不使缓存中内核别名化的物理页面无效,任务 3 也可能在 COW 故障后看到不一致的数据。

void update_mmu_cache(struct vm_area_struct *vma,
                      unsigned long address, pte_t pte);
虽然不严格属于刷新架构的一部分,但在某些架构上,为了使事情正常运行并保持系统一致,需要在此处执行一些关键操作和检查。

特别是,对于虚拟索引缓存,此例程必须检查当前页面故障添加的新映射是否向用户空间添加了“坏别名”。

“坏别名”定义为两个或多个映射(其中至少一个可写)到两个或多个虚拟页面,这些虚拟页面都转换为完全相同的物理页面,并且由于缓存的索引算法,也可以驻留在唯一且互斥的缓存行中。

如果检测到此类“坏别名”,则实现需要以某种方式解决此不一致性,一种解决方案是遍历所有映射并更改页表,以使这些页面在硬件允许的情况下“不可缓存”。

对此的检查非常简单,实现基本上需要做的就是

if((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED))
        check_for_potential_bad_aliases();
因此,对于常见情况(共享可写映射非常罕见),对于具有虚拟索引缓存的系统,只需要进行一次比较。

5. 对 SMP 的影响

根据架构的不同,可能需要对刷新架构进行某些修改,以使其在 SMP 系统上工作。

主要关注的问题是,上述刷新操作之一是否导致整个系统全局看到刷新,还是仅保证本地处理器看到刷新。

在后一种情况下,需要一种交叉调用机制。当前 Linux 下支持的两个 SMP 系统(Intel 和 Sparc)使用处理器间中断来“广播”刷新操作,并在必要时使其在所有处理器上本地运行。

例如,在 sun4m Sparc 系统上,系统中的所有处理器都必须执行刷新请求,以保证整个系统的一致性。但是,在 sun4d Sparc 机器上,在本地处理器上执行的 TLB 刷新由硬件在系统总线上广播,因此不需要交叉调用。

6. 对基于上下文的 MMU/CACHE 架构的影响

MMU 和缓存上下文设施概念背后的整个思想是允许许多地址空间共享 cpu 上的缓存/mmu 资源。

为了充分利用这种设施,并仍然保持上述一致性,实现者需要进行一些额外的考虑。

所涉及的问题因实现而异,至少这是作者的经验。但特别是一些问题可能是

  1. 内核空间映射与用户空间映射的关系,就上下文而言。在某些系统上,内核映射具有“全局”属性,因为硬件在进行具有此属性的转换时,不关心上下文信息。因此,对内核缓存/mmu 映射的一次刷新(在任何上下文中)可能就足够了。
    但是,在其他实现中,内核可能共享与特定地址空间关联的上下文密钥。在这种情况下,可能需要在所有当前有效的上下文中遍历,并在每个上下文中为内核地址空间刷新执行完整的刷新。
  2. 按上下文刷新的成本可能成为一个关键问题,特别是对于 TLB 而言。例如,如果需要在较大范围的地址(或整个地址空间)上进行 tlb 刷新,则为了提高效率,为该进程分配和分配新的 mmu 上下文可能更明智。

7. 如何处理刷新架构不处理的内容,以及示例

刚刚描述的刷新架构没有对设备/DMA 与缓存数据的一致性进行任何修改。它也没有为 DMA 和设备所需的任何映射策略提供任何规定,如果这在 Linux 移植到的特定机器上是必需的。这些问题与刷新架构无关。

这些问题在设备驱动程序级别处理得最干净。在作者使用一组通用的 Sparc 设备驱动程序的经验之后,他确信这一点,这些驱动程序需要在同一内核中的多个缓存/mmu 和总线架构上正确运行。

事实上,这种实现更有效,因为驱动程序确切地知道 DMA 何时需要看到一致的数据,或者 DMA 何时会产生必须解决的不一致性。任何试图通过添加到通用内核内存管理代码中的钩子来达到这种效率水平的尝试都将是复杂的,甚至是非常不干净的。

例如,考虑在 Sparc 上如何处理 DMA 缓冲区。当设备驱动程序必须执行与单个缓冲区或多个缓冲区分散列表之间的 DMA 时,它使用一组抽象例程

 char *(*mmu_get_scsi_one)(char *, unsigned long, struct linux_sbus *sbus);
 void  (*mmu_get_scsi_sgl)(struct mmu_sglist *, int, struct linux_sbus *sbus);
 void  (*mmu_release_scsi_one)(char *, unsigned long, struct linux_sbus *sbus);
 void  (*mmu_release_scsi_sgl)(struct mmu_sglist *, int, struct linux_sbus *sbus);
 void  (*mmu_map_dma_area)(unsigned long addr, int len);
本质上是mmu_get_*例程被传递一个指针或一组指针和大小规范,指向内核空间中将发生 DMA 的区域,它们返回一个 DMA 可寻址的地址(即,可以加载到 DMA 控制器中进行传输的地址)。当驱动程序完成 DMA 并且传输完成后,必须使用 DMA 可寻址的地址调用mmu_release_*例程,以便可以释放资源(如果必要)并且可以执行缓存刷新(如果必要)。

最后的例程适用于需要长时间拥有 DMA 内存块的驱动程序,例如网络驱动程序将使用此例程来处理池传输和接收缓冲区。

最后一个参数是 Sparc 特定的实体,如果 DMA 映射是基于每个总线设置的,则允许机器级代码执行映射。

8. 未解决的问题

似乎存在一些非常愚蠢的缓存架构,当别名放入缓存时(即使是安全的别名,其中没有一个别名缓存条目是可写的!),它们会引起麻烦。值得注意的是 MIPS R4000,当发生这种情况时,它会给出异常,这些异常可能在当前实现中发生 COW 处理时发生。在大多数像这样做出愚蠢事情的芯片上,异常处理程序可以刷新被抱怨的缓存中的条目,一切都会好起来的。作者主要担心的是 COW 处理期间这些异常的成本以及这将对系统性能产生的影响。也许有必要进行新的刷新,这将在 COW 故障处理中的页面复制之前执行,这本质上是在不这样做会导致刚刚描述的麻烦时刷新用户空间页面。

最近一直在热烈讨论为非常智能的网络硬件添加页面翻转功能。可能有必要扩展刷新架构,以提供网络代码的这些更改所需的接口和设施。

并且无论如何,刷新架构始终会受到改进和更改,以处理新的问题或新的硬件,这些问题或硬件会带来到目前为止未知的难题。

David S. Miller
davem@caip.rutgers.edu