ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PCID Process Context Identifier

PCID Process Context Identifier PCIDProcess Context Identifier进程上下文标识符是 x86 架构中与 ASID 等价的概念其核心目的是让 TLB 能够同时容纳多个地址空间的转换条目从而在进程切换时避免刷新整个 TLB。核心作用避免进程切换时的 TLB 全刷在没有 PCID 的时代每次进程切换都必须重新加载 CR3 寄存器这会导致TLB 被完全清空。新进程运行时所有地址转换都需要重新走页表造成大量 TLB Miss。PCID 为每个 TLB 条目附加一个12 位的标签0-4095。当 CR3 被加载时CPU 只使用与当前 PCID 匹配的条目。这样不同进程的 TLB 条目可以共存于 TLB 中进程切换时只需切换 PCID无需清空 TLB。Linux 的“非正统”实现Linux 对 x86 PCID 的使用方式非常特殊。由于 PCID 只有 12 位最多 4095 个可用值而系统可能同时运行数千个进程给每个进程分配唯一 PCID 是不可行的。因此Linux 采用了一种Per-CPU 的“浅缓存”策略TLB_NR_DYN_ASIDS 6每个 CPU 只维护一个6 个条目的小数组缓存最近使用过的 6 个mm地址空间。ASIDLinux 内部的逻辑标识范围[0, 5]仅作为数组索引。kPCID真正写入 CR3 的 PCID 值等于ASID 1PCID 0 被保留给swapper_pg_dir等特殊用途。uPCID用于 KPTI 的用户页表 PCID等于kPCID 2048因为每个进程需要两个 PCID内核态和用户态各一个。当进程切换时内核线性扫描这个 6 条目数组。如果命中了最近运行过的mm就直接复用其 PCID跳过 TLB 刷新如果未命中则淘汰最久未使用的条目并刷新该 PCID 对应的所有 TLB 条目然后分配新的 PCID。硬件层面的考量根据 Dave Hansen 在 LKML 的讨论Intel CPU 的 TLB 条目中实际只存储约 3 位的 PCID 别名而非完整的 12 位。CPU 内部维护一个映射表将架构上的 12 位 PCID 映射到硬件中的 3 位别名。这意味着即使软件层面使用了 4095 个 PCID硬件层面的实际并发容量也远小于此进一步验证了 Linux “浅缓存”策略的合理性。与 KPTI 的协同PCID 与 KPTI页表隔离配合使用时CR3 加载会设置“no-flush”位bit 63告诉 CPU 不要刷新 TLB。这样当从内核页表切换到用户页表或反之时TLB 中属于另一套页表的条目得以保留显著减少了 KPTI 带来的性能损失。与INVPCID的对比Linux 在 TLB 刷新时面临一个选择使用INVPCID指令精确失效单个 PCID还是通过重写 CR3 触发全刷。实测数据显示在 KPTI 场景下使用INVPCID编译内核比使用延迟 CR3 写入快约 0.58%。虽然差距不大但INVPCID避免了不必要的 TLB 重填因此成为内核的首选方案。PCID实现Linux 对 PCID 的实现是一种“非正统”的浅缓存策略核心是在6 个 Per-CPU 槽位中缓存最近使用的地址空间通过巧妙的命名和位操作与 KPTI 协同工作。核心数据结构cpu_tlbstate.ctxs每个 CPU 维护一个struct tlb_state其中关键的是TLB_NR_DYN_ASIDS 6个上下文槽位struct tlb_context { u64 ctx_id; // 对应的 mm 指针或 0 u64 tlb_gen; // TLB 代际计数器 }; struct tlb_context ctxs[TLB_NR_DYN_ASIDS];选择 6 是因为它“足够用”且能让整个tlb_state结构体恰好容纳在两个缓存行内避免额外的 L1 缓存缺失。三个标识符的命名与换算为了避免混淆Linux 为同一概念在不同层面起了三个名字名称范围含义ASID[0, 5]Linux 内部的逻辑标识是ctxs[]数组的索引kPCID[1, 6]写入 CR3 的内核态 PCID等于ASID 1PCID 0 被保留uPCID[2049, 2054]KPTI 用户态 PCID等于kPCID 2048PCID 0 是特殊的用于swapper_pg_dir和不感知 PCID 的 CR3 写入者如 EFI。KPTI 为每个mm分配两个 PCID内核态和用户态各一个用 bit 11X86_CR3_PTI_PCID_USER_BIT来区分。切换与淘汰流程当 CPU 要加载一个新的mm时内核线性扫描这 6 个槽位命中直接复用已有的 PCID跳过 TLB 刷新。未命中选择一个槽位覆盖。覆盖前必须刷新原来占用该 PCID 的 TLB 条目。维护user_pcid_flush_mask位掩码来追踪哪些用户 PCID 需要在下一次切换时刷新这避免了不必要的全局刷新开销。硬件层面的考量根据 Dave Hansen 在 LKML 的讨论Intel CPU 的 TLB 条目中实际只存储约 3 位的 PCID 别名而非完整的 12 位。这意味着即使软件层面使用了 4095 个 PCID硬件层面的实际并发容量也远小于此进一步验证了 Linux“浅缓存”策略的合理性。性能收益PCID 支持在 Linux 4.14 才正式主线化。在 KPTI 场景下PCID 能显著减少dTLB_load_misses。基准测试显示在 512 页工作负载下未使用 PCID 的 KPTI 补丁有 207M 次 dTLB 缺失而使用 PCID 的版本只有 190M 次性能接近无 KPTI 的 11M 次。
返回列表