根据一些操作系统的教科书,对于更快的上下文切换,人们在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);
该类型包括其他字段,但为简洁起见,我仅显示了两个。 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);
第一个参数
next
指向调度程序选择要恢复的线程所属的进程的内存描述符。该对象包含许多东西。但是,我们在这里关心的一件事是ctx_id
,它是一个64位值,对于每个现有进程都是唯一的。 next_tlb_gen
用于确定是否需要TLB无效,我将在稍后讨论。该函数返回new_asid
和need_flush
,void
保留分配给该进程的ASID,invalidate_other
表示是否需要TLB无效。该函数的返回类型为ctxs
。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;
}
从逻辑上讲,该函数的工作方式如下。如果处理器不支持PCID,则所有进程的ASID值均为零,并且始终需要进行TLB刷新。由于它不相关,因此我将跳过
cpu_tlbstate.ctxs[asid].ctx_id
检查。接下来,循环遍历所有6个规范的ASID,并将它们用作asid
的索引。当前为上下文标识符为need_flush
的进程分配了ASID值next_tlb_gen
。因此,循环将检查进程是否仍为其分配了ASID。在这种情况下,将使用相同的ASID并根据this_cpu_add_return
更新next_asid
。即使未回收ASID,我们可能仍需要刷新与ASID相关的TLB条目的原因是由于惰性TLB无效机制所致,这超出了您的问题范围。如果没有将当前使用的ASID分配给该进程,那么我们需要分配一个新的ASID。调用
TLB_NR_DYN_ASIDS
只会将next_asid
中的值增加1。这将为我们提供一个kPCID值。然后,当减去1时,我们得到规范的ASID。如果我们超过了最大的规范ASID值(choose_new_asid
),那么我们将规范ASID换为零,并将相应的kPCID(即1)写入switch_mm_irqs_off
。发生这种情况时,这意味着为其他进程分配了相同的规范ASID,因此我们绝对希望在核心上刷新与该ASID相关联的TLB条目。然后,当ctxs
返回到tlb_state
时,TLB_NR_DYN_ASIDS
数组和CR3会相应更新。写入CR3将使内核自动刷新与该ASID相关的TLB条目。如果将其ASID重新分配给另一个进程的进程仍处于事件状态,则下一次其线程运行时,将在该内核上为其分配一个新的ASID。这整个过程是每个核心发生的。否则,如果该进程已终止,则在将来的某个时候,其ASID将被回收。Linux在每个内核中仅使用6个ASID的原因是,它使ojit_code类型的大小小到足以容纳两个64字节高速缓存行。通常,在Linux系统上可以同时存在数十个同时运行的进程。但是,大多数都通常处于休眠状态。因此,Linux管理ASID空间的方式实际上非常有效。尽管有趣的是,对ojit_code的值对性能的影响进行了实验评估。但是我不知道有任何这样的发表的研究。
关于x86 - 用于英特尔处理器的TLB ASID标签中有多少位?以及如何处理 'ASID overflow'呢?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/52813239/