英文

何时需要在页表锁内进行通知?

当清除 pte/pmd 时,我们可以选择通过(*_clear_flush 调用的通知版本,即 mmu_notifier_invalidate_range)在页表锁下通知该事件。但并非所有情况下都需要此通知。

对于二级 TLB(非 CPU TLB),如 IOMMU TLB 或设备 TLB(当设备使用类似 ATS/PASID 的机制,使 IOMMU 遍历 CPU 页表以访问进程虚拟地址空间时),在清除 pte/pmd 时,只有在以下两种情况下才需要在持有页表锁的情况下通知这些二级 TLB:

  1. 在 mmu_notifier_invalidate_range_end() 之前,页面后备地址已被释放。

  2. 页表项已更新,指向新的页面 (COW, 零页上的写故障, __replace_page(), ...)。

情况 A 很明显,您不希望设备冒险写入可能被完全不同的任务使用的页面。

情况 B 更微妙。为了正确性,它要求发生以下序列:

  • 获取页表锁。

  • 清除页表项并通知 ([pmd/pte]p_huge_clear_flush_notify())。

  • 设置页表项以指向新页面。

如果在设置新的 pte/pmd 值之前,清除页表项后没有通知,则可能会破坏设备内存模型,例如 C11 或 C++11。

考虑以下场景(设备使用类似于 ATS/PASID 的功能):

两个地址 addrA 和 addrB,使得 |addrA - addrB| >= PAGE_SIZE,我们假设它们对于 COW 是写保护的(情况 B 的其他情况也适用)。

[Time N] --------------------------------------------------------------------
CPU-thread-0  {try to write to addrA}
CPU-thread-1  {try to write to addrB}
CPU-thread-2  {}
CPU-thread-3  {}
DEV-thread-0  {read addrA and populate device TLB}
DEV-thread-2  {read addrB and populate device TLB}
[Time N+1] ------------------------------------------------------------------
CPU-thread-0  {COW_step0: {mmu_notifier_invalidate_range_start(addrA)}}
CPU-thread-1  {COW_step0: {mmu_notifier_invalidate_range_start(addrB)}}
CPU-thread-2  {}
CPU-thread-3  {}
DEV-thread-0  {}
DEV-thread-2  {}
[Time N+2] ------------------------------------------------------------------
CPU-thread-0  {COW_step1: {update page table to point to new page for addrA}}
CPU-thread-1  {COW_step1: {update page table to point to new page for addrB}}
CPU-thread-2  {}
CPU-thread-3  {}
DEV-thread-0  {}
DEV-thread-2  {}
[Time N+3] ------------------------------------------------------------------
CPU-thread-0  {preempted}
CPU-thread-1  {preempted}
CPU-thread-2  {write to addrA which is a write to new page}
CPU-thread-3  {}
DEV-thread-0  {}
DEV-thread-2  {}
[Time N+3] ------------------------------------------------------------------
CPU-thread-0  {preempted}
CPU-thread-1  {preempted}
CPU-thread-2  {}
CPU-thread-3  {write to addrB which is a write to new page}
DEV-thread-0  {}
DEV-thread-2  {}
[Time N+4] ------------------------------------------------------------------
CPU-thread-0  {preempted}
CPU-thread-1  {COW_step3: {mmu_notifier_invalidate_range_end(addrB)}}
CPU-thread-2  {}
CPU-thread-3  {}
DEV-thread-0  {}
DEV-thread-2  {}
[Time N+5] ------------------------------------------------------------------
CPU-thread-0  {preempted}
CPU-thread-1  {}
CPU-thread-2  {}
CPU-thread-3  {}
DEV-thread-0  {read addrA from old page}
DEV-thread-2  {read addrB from new page}

因此,因为在时间 N+2 时,清除页表项没有与使二级 TLB 失效的通知配对,设备在看到 addrA 的新值之前看到了 addrB 的新值。这破坏了设备的完全内存顺序。

当将 pte 更改为写保护或指向具有相同内容(KSM)的新写保护页面时,可以将 mmu_notifier_invalidate_range 调用延迟到页表锁外的 mmu_notifier_invalidate_range_end()。即使执行页表更新的线程在释放页表锁后但在调用 mmu_notifier_invalidate_range_end() 之前被抢占,这也是正确的。