Sal*_*hLZ 4 x86 operating-system intel virtual-memory tlb
根据一些操作系统的教科书,为了更快地进行上下文切换,人们在TLB标签字段中为每个进程添加了ASID,因此我们不需要在上下文切换中刷新整个TLB。
我听说有些ARM处理器和MIPS处理器在TLB中确实具有ASID。但是我不确定Intel x86处理器是否具有ASID。
同时,似乎ASID通常具有比PID(32位)少的位(例如8位)。那么,如果在上述8位ASID情况下内存中的进程比2 ^ 8多,那么系统如何处理“ ASID溢出”?
英特尔将ASID称为过程上下文标识符(PCID)。在所有支持PCID的Intel处理器上,PCID的大小为12位。它们构成CR3寄存器的位11:0。默认情况下,在处理器重置时,CR4.PCIDE(CR4的第17位)被清除并且CR3.PCID为零,因此,如果操作系统要使用PCID,则必须先设置CR4.PCIDE才能启用该功能。仅当设置了CR4.PCIDE时,才允许写入大于零的PCID值。也就是说,当设置CR4.PCIDE时,也可以将零写入CR3.PCID。因此,可以同时使用的PCID的最大数量为2 ^ 12 = 4096。
我将讨论Linux内核如何分配PCID。Linux内核本身甚至对英特尔处理器也使用术语ASID,因此我也将使用该术语。
通常,实际上有很多方法可以管理ASID空间,例如:
Linux使用最后一种方法,我将在后面详细讨论。
Linux仅记住每个内核上使用的最后6个ASID。这由TLB_NR_DYN_ASIDS宏指定。系统为类型为tlb_state的每个核心创建一个数据结构,该数据结构定义如下:
struct tlb_context {
u64 ctx_id;
u64 tlb_gen;
};
struct tlb_state {
.
.
.
u16 next_asid;
struct tlb_context ctxs[TLB_NR_DYN_ASIDS];
};
DECLARE_PER_CPU_SHARED_ALIGNED(struct tlb_state, cpu_tlbstate);
Run Code Online (Sandbox Code Playgroud)
该类型包括其他字段,但为简洁起见,我仅显示了两个。Linux定义了以下ASID空间:
TLB_NR_DYN_ASIDS)。这些值存储在next_asid字段中,并用作ctxs数组的索引。TLB_NR_DYN_ASIDS+ 1)。这些值实际上存储在CR3.PCID中。TLB_NR_DYN_ASIDS+++ 1)的ASID 。这些值实际上存储在CR3.PCID中。每个进程都有一个规范的ASID。这是Linux本身使用的值。每个规范的ASID与一个kPCID和一个uPCID相关联,它们是实际存储在CR3.PCID中的值。每个进程具有两个ASID的原因是为了支持页表隔离(PTI),从而减轻了Meltdown漏洞。实际上,使用PTI,每个进程都有两个虚拟地址空间,每个都有自己的ASID,但是两个ASID具有固定的算术关系,如上所示。因此,即使英特尔处理器每个内核支持4096个ASID,Linux还是每个内核仅使用12个。我进入ctxs数组,请耐心一点。
Linux在上下文切换(而不是创建)上为进程动态分配ASID。同一进程可能在不同内核上获得不同的ASID,并且每当该进程的线程计划在内核上运行时,其ASID可能会动态更改。这是在switch_mm_irqs_off函数中完成的,只要调度程序从一个线程切换到内核上的另一个线程,即使两个线程属于同一进程,该函数都会被调用。有两种情况需要考虑:
在这种情况下,内核执行以下函数调用:
choose_new_asid(next, next_tlb_gen, &new_asid, &need_flush);
Run Code Online (Sandbox Code Playgroud)
第一个参数next指向调度程序选择要恢复的线程所属的进程的内存描述符。该对象包含许多东西。但是我们在这里关心的是ctx_id,这是一个64位值,对于每个现有进程都是唯一的。将next_tlb_gen被用来确定一个TLB失效是否需要与否,我将在稍后讨论。该函数返回new_asid保留分配给该进程的ASID,need_flush并说明是否需要TLB无效。函数的返回类型为void。
static void choose_new_asid(struct mm_struct *next, u64 next_tlb_gen,
u16 *new_asid, bool *need_flush)
{
u16 asid;
if (!static_cpu_has(X86_FEATURE_PCID)) {
*new_asid = 0;
*need_flush = true;
return;
}
if (this_cpu_read(cpu_tlbstate.invalidate_other))
clear_asid_other();
for (asid = 0; asid < TLB_NR_DYN_ASIDS; asid++) {
if (this_cpu_read(cpu_tlbstate.ctxs[asid].ctx_id) !=
next->context.ctx_id)
continue;
*new_asid = asid;
*need_flush = (this_cpu_read(cpu_tlbstate.ctxs[asid].tlb_gen) <
next_tlb_gen);
return;
}
/*
* We don't currently own an ASID slot on this CPU.
* Allocate a slot.
*/
*new_asid = this_cpu_add_return(cpu_tlbstate.next_asid, 1) - 1;
if (*new_asid >= TLB_NR_DYN_ASIDS) {
*new_asid = 0;
this_cpu_write(cpu_tlbstate.next_asid, 1);
}
*need_flush = true;
}
Run Code Online (Sandbox Code Playgroud)
从逻辑上讲,该功能的工作原理如下。如果处理器不支持PCID,则所有进程的ASID值均为零,并且始终需要进行TLB刷新。invalidate_other由于它不相关,因此我将跳过该检查。接下来,循环遍历所有6个规范的ASID,并将它们用作的索引ctxs。cpu_tlbstate.ctxs[asid].ctx_id当前具有上下文标识符的进程被分配了ASID值asid。因此,循环将检查进程是否仍为其分配了ASID。在这种情况下,将使用相同的ASID并need_flush根据进行更新next_tlb_gen。即使未回收ASID,我们也可能需要刷新与ASID相关的TLB条目的原因是由于惰性TLB无效机制所致,这超出了您的问题范围。
如果没有将当前使用的ASID分配给该进程,则我们需要分配一个新的ASID。要将呼叫this_cpu_add_return简单地增加值next_asid1。这给了我们一个kPCID值。然后,当减去1时,我们得到规范的ASID。如果我们超过了最大的规范ASID值(TLB_NR_DYN_ASIDS),那么我们将规范ASID换为零,并将对应的kPCID(即1)写入next_asid。发生这种情况时,这意味着为其他进程分配了相同的规范ASID,因此我们肯定要在内核上刷新与该ASID关联的TLB条目。然后,当choose_new_asid返回时switch_mm_irqs_off,ctxs阵列和CR3也会相应更新。写入CR3将使内核自动刷新与该ASID相关的TLB条目。如果将ASID重新分配给另一个进程的进程仍处于活动状态,则下一次运行其线程时,将在该内核上为其分配新的ASID。这整个过程是每个核心发生的。否则,如果该进程已终止,那么将来的某个时候,其ASID将被回收。
Linux在每个内核中仅使用6个ASID的原因是,它使tlb_state类型的大小很小,足以容纳两个64字节的缓存行。通常,在Linux系统上可以同时存在数十个同时运行的进程。但是,大多数都通常处于休眠状态。因此,Linux管理ASID空间的方式实际上非常有效。尽管有趣的是对价值TLB_NR_DYN_ASIDS对性能的影响进行实验评估。但是我不知道有任何这样发表的研究。