何时需要在页表锁内进行通知?¶
当清除 pte/pmd 时,我们可以选择通过(*_clear_flush 调用的通知版本,即 mmu_notifier_invalidate_range)在页表锁下通知该事件。但并非所有情况下都需要此通知。
对于二级 TLB(非 CPU TLB),如 IOMMU TLB 或设备 TLB(当设备使用类似 ATS/PASID 的机制,使 IOMMU 遍历 CPU 页表以访问进程虚拟地址空间时),在清除 pte/pmd 时,只有在以下两种情况下才需要在持有页表锁的情况下通知这些二级 TLB:
在 mmu_notifier_invalidate_range_end() 之前,页面后备地址已被释放。
页表项已更新,指向新的页面 (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() 之前被抢占,这也是正确的。