flush_cache_foo(...); modify_address_space(); flush_tlb_foo(...);这里的逻辑是
void flush_cache_all(void); void flush_tlb_all(void);这些例程用于通知特定于架构的代码,内核地址空间映射已更改,这意味着每个进程的映射都已有效地更改。
一个实现应该
void flush_cache_mm(struct mm_struct *mm); void flush_tlb_mm(struct mm_struct *mm);这些例程通知系统,由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 也适用于此处。
一个实现应该
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,例如,可能避免刷新指令空间。,通过上面关于
一个实现应该
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,考虑如果以下序列完全连续发生,内核和用户空间中缓存的内容
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();因此,对于常见情况(共享可写映射非常罕见),对于具有虚拟索引缓存的系统,只需要进行一次比较。
主要关注的问题是,上述刷新操作之一是否导致整个系统全局看到刷新,还是仅保证本地处理器看到刷新。
在后一种情况下,需要一种交叉调用机制。当前 Linux 下支持的两个 SMP 系统(Intel 和 Sparc)使用处理器间中断来“广播”刷新操作,并在必要时使其在所有处理器上本地运行。
例如,在 sun4m Sparc 系统上,系统中的所有处理器都必须执行刷新请求,以保证整个系统的一致性。但是,在 sun4d Sparc 机器上,在本地处理器上执行的 TLB 刷新由硬件在系统总线上广播,因此不需要交叉调用。
为了充分利用这种设施,并仍然保持上述一致性,实现者需要进行一些额外的考虑。
所涉及的问题因实现而异,至少这是作者的经验。但特别是一些问题可能是
这些问题在设备驱动程序级别处理得最干净。在作者使用一组通用的 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 映射是基于每个总线设置的,则允许机器级代码执行映射。
最近一直在热烈讨论为非常智能的网络硬件添加页面翻转功能。可能有必要扩展刷新架构,以提供网络代码的这些更改所需的接口和设施。
并且无论如何,刷新架构始终会受到改进和更改,以处理新的问题或新的硬件,这些问题或硬件会带来到目前为止未知的难题。
David S. Miller davem@caip.rutgers.edu