user_pcid是 x86 架构中用于将逻辑 ASID 转换为用户态 PCID(uPCID)的辅助函数。它与kern_pcid配对使用,专门服务于 KPTI(页表隔离)场景下的用户态页表切换。
核心作用:在 kPCID 基础上设置切换位
static inline u16 user_pcid(u16 asid) { u16 ret = kern_pcid(asid); // 先得到内核态 PCID (1~6) #ifdef CONFIG_MITIGATION_PAGE_TABLE_ISOLATION ret |= 1 << X86_CR3_PTI_PCID_USER_BIT; // 设置 bit 11,得到用户态 PCID #endif return ret; }代码逻辑解析
1. 基础转换
u16 ret = kern_pcid(asid);
先调用kern_pcid完成逻辑 ASID → kPCID 的转换。kern_pcid的核心操作是asid + 1,将逻辑 ASID(0~5)映射到 kPCID(1~6)。PCID 0 被保留给swapper_pg_dir等特殊用途。
2. KPTI 下的用户态标识
#ifdef CONFIG_MITIGATION_PAGE_TABLE_ISOLATION ret |= 1 << X86_CR3_PTI_PCID_USER_BIT; #endif
如果内核启用了 KPTI(页表隔离),则将结果与1 << X86_CR3_PTI_PCID_USER_BIT进行或运算。X86_CR3_PTI_PCID_USER_BIT通常是bit 11,1 << 11 = 2048。这样,uPCID 的范围就是2049~2054。
为什么需要区分 kPCID 和 uPCID?
在 KPTI 机制下,每个进程实际上有两套页表:
内核页表:包含完整的内核和用户空间映射。
用户页表:只包含极少的必要内核映射和完整的用户空间映射。
当 CPU 从用户态陷入内核时,需要从用户页表切换到内核页表;返回用户态时再切回。如果两套页表使用同一个 PCID,那么每次切换都会导致 TLB 中该 PCID 的条目被混淆——内核态的 TLB 条目和用户态的 TLB 条目无法区分。
通过给用户页表分配一个独立的 PCID(设置 bit 11 的 uPCID),内核页表和用户页表就拥有了不同的 PCID 标签。CR3 切换时,CPU 只会命中与当前 PCID 匹配的 TLB 条目,从而避免了 KPTI 切换时刷新整个 TLB,显著降低了 KPTI 的性能开销。
三个标识符的完整映射
| 名称 | 范围 | 计算公式 | 用途 |
|---|---|---|---|
| ASID | 0~5 | 逻辑标识 | 内核内部索引 |
| kPCID | 1~6 | asid + 1 | 写入 CR3,内核态页表 |
| uPCID | 2049~2054 | kPCID | (1 << 11) | 写入 CR3,用户态页表(KPTI) |
这三个标识符共同构成了 Linux PCID 管理的核心:用极小的 ASID 空间(6 个槽位)配合位标志,在有限的 12 位 PCID 中,为每个 CPU 缓存最近使用的少量地址空间,同时与 KPTI 的页表切换需求完美协同。
kern_pcid的调用者
kern_pcid和user_pcid是纯计算函数,它们本身不调用其他函数,而是被上层函数在构建 CR3 值和执行 TLB 刷新时调用。
它最核心的用途是构建内核态 CR3 值。在build_cr3和build_cr3_noflush中,kern_pcid(asid)被用来将逻辑 ASID 转换为硬件 PCID 位:
static inline unsigned long build_cr3(pgd_t *pgd, u16 asid, unsigned long lam) { return __sme_pa(pgd) | kern_pcid(asid) | lam; }build_cr3本身又被load_new_mm_cr3和__get_current_cr3_fast调用,用于在进程切换时加载 CR3 。
此外,在 TLB 刷新的底层实现中,kern_pcid也被用来指定要刷新哪个 PCID 的条目,例如invlpgb_flush_single_pcid_nosync(kern_pcid(asid))。
user_pcid的调用者
user_pcid主要用于KPTI 场景下的 TLB 刷新。当内核需要使某个用户地址空间的 TLB 条目失效时,必须同时刷新对应的内核 PCID 和用户 PCID:
invlpgb_flush_user_nr_nosync(kern_pcid(asid), addr, nr, pmd); if (static_cpu_has(X86_FEATURE_PTI)) invlpgb_flush_user_nr_nosync(user_pcid(asid), addr, nr, pmd);
这段代码来自 AMD 广播 TLB 失效(INVLPGB)的补丁,它在刷新用户地址的 TLB 时,先刷新内核 PCID,如果启用了 KPTI,再刷新用户 PCID 。
在invalidate_user_asid中的间接使用
invalidate_user_asid函数不直接调用user_pcid,但它通过kern_pcid来标记用户 ASID 需要刷新:
static inline void invalidate_user_asid(u16 asid) { __set_bit(kern_pcid(asid), (unsigned long *)this_cpu_ptr(&cpu_tlbstate.user_pcid_flush_mask)); }这里用kern_pcid(asid)作为位索引,在user_pcid_flush_mask位图中标记“这个 ASID 的用户 PCID 需要在下一次切换时刷新” 。这个位图的存在正是因为用户 PCID 和内核 PCID 需要分别管理——内核空间的 TLB 失效只影响kern_pcid,用户空间的失效则需要额外标记user_pcid。
总结
kern_pcid和user_pcid处于 PCID 管理的最底层,它们不调用任何函数,只做简单的位运算。它们的调用者分为两类:构建 CR3 值(如build_cr3)和执行 TLB 刷新(如 INVLPGB 路径和invalidate_user_asid)。这种分层设计确保了 PCID 的转换逻辑集中在一处,避免在各处重复计算。