何时需要在页表锁内进行通知?¶
当清除 pte/pmd 时,我们可以选择通过(*_clear_flush 的通知版本调用 mmu_notifier_invalidate_range)在页表锁下通知该事件。 但并非在所有情况下都必须进行通知。
对于辅助 TLB(非 CPU TLB),如 IOMMU TLB 或设备 TLB(当设备使用诸如 ATS/PASID 之类的东西来使 IOMMU 遍历 CPU 页表以访问进程虚拟地址空间时)。 只有在以下两种情况下需要在持有页表锁的同时通知这些辅助 TLB 以清除 pte/pmd
页面后备地址在 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() 之前被抢占,也是如此。