gpt4 book ai didi

x86 - 英特尔处理器的 TLB ASID 标签中有多少位?以及如何处理 'ASID overflow' ?

转载 作者:行者123 更新时间:2023-12-04 14:38:30 33 4
gpt4 key购买 nike

根据一些操作系统教科书,为了更快的上下文切换,人们在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,甚至对于 Intel 处理器也是如此,因此我也将使用这个术语。

一般来说,管理ASID空间的方法真的有很多种,例如:

  • 当需要创建新进程时,为该进程分配一个专用的ASID。如果 ASID 空间已用完,则拒绝创建进程并失败。这简单而有效,但可能会严重限制进程的数量。
  • 当 ASID 空间耗尽时,不是将进程数限制为 ASID 的可用性,而是表现得好像 ASID 不受支持。也就是说,在所有进程的进程上下文切换上刷新整个 TLB。实际上,这是一种糟糕的方法,因为随着进程的创建和终止,您最终可能会在禁用和启用 ASID 之间切换。这种方法会导致潜在的高性能损失。
  • 允许多个进程使用相同的 ASID。在这种情况下,在使用相同 ASID 的进程之间切换时需要小心,因为使用该 ASID 标记的 TLB 条目仍然需要全部刷新。
  • 在前面的所有方法中,每个进程都有一个ASID,因此表示进程的OS数据结构需要有一个存储ASID的字段。另一种方法是将当前分配的 ASID 存储在单独的结构中。 ASID 在进程需要执行时动态分配给进程。不事件的进程不会分配给它们的 ASID。与以前的方法相比,这有两个优点。首先,ASID 空间的使用效率更高,因为大多数休眠进程不会不必要地消耗 ASID。其次,所有当前分配的 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 空间:
  • 规范的 ASID 空间:这些包括 ASID 0 到 6 ( TLB_NR_DYN_ASIDS )。这些值存储在 next_asid 字段中,并用作 ctxs 数组的索引。
  • 内核 ASID (kPCID) 空间:这些包括 ASID 1 到 7 (TLB_NR_DYN_ASIDS + 1)。这些值实际上存储在 CR3.PCID 中。
  • 用户 ASID (uPCID) 空间:这些包括 ASID 2048 + 1 到 2048 + 7 (2048 + TLB_NR_DYN_ASIDS + 1)。这些值实际上存储在 CR3.PCID 中。

  • 每个进程都有一个规范的 ASID。这是 Linux 本身使用的值。每个规范的 ASID 都与一个 kPCID 和一个 uPCID 相关联,它们是实际存储在 CR3.PCID 中的值。每个进程有两个 ASID 的原因是支持页表隔离 (PTI),从而减轻 Meltdown 漏洞。实际上,有了PTI,每个进程都有两个虚拟地址空间,每个都有自己的ASID,但是这两个ASID有一个固定的算术关系,如上图所示。因此,即使 Intel 处理器支持每个内核 4096 个 ASID,Linux 每个内核仅使用 12 个。我将进入 ctxs 数组,请耐心等待。

    Linux 在上下文切换时动态地为进程分配 ASID,而不是在创建时。同一进程可能会在不同的内核上获得不同的 ASID,并且只要该进程的线程被安排在内核上运行,其 ASID 可能会动态更改。这是在 switch_mm_irqs_off 函数中完成的,每当调度程序从一个内核上的一个线程切换到另一个线程时,该函数就会被调用,即使这两个线程属于同一个进程。有两种情况需要考虑:
  • 用户线程被中断或执行了系统调用。在这种情况下,系统切换到内核模式来处理中断或系统调用。由于用户线程刚刚运行,它的进程必须有一个已经分配的 ASID。如果操作系统稍后决定继续执行同一线程或同一进程的另一个线程,那么它将继续使用相同的 ASID。这个案子很无聊。
  • 操作系统决定调度另一个进程的线程在内核上运行。因此操作系统必须为进程分配一个 ASID。这个案例非常有趣,将在本答案的其余部分详细讨论。

  • 在这种情况下,内核执行以下函数调用:
    choose_new_asid(next, next_tlb_gen, &new_asid, &need_flush);

    第一个参数 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;
    }

    从逻辑上讲,该函数的工作原理如下。如果处理器不支持 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 都没有分配给进程,那么我们需要分配一个新的。对 this_cpu_add_return 的调用只是将 next_asid 中的值加 1。这给了我们一个 kPCID 值。然后减去 1,我们得到规范的 ASID。如果我们已经超过了最大规范 ASID 值 ( TLB_NR_DYN_ASIDS ),那么我们将回绕到规范 ASID 0 并将相应的 kPCID(即 1)写入 next_asid 。发生这种情况时,意味着其他一些进程被分配了相同的规范 ASID,因此我们肯定希望刷新与核心上的该 ASID 关联的 TLB 条目。然后当 choose_new_asid 返回到 switch_mm_irqs_off 时, ctxs 数组和 CR3 会相应地更新。写入 CR3 将使内核自动刷新与该 ASID 关联的 TLB 条目。如果其 ASID 被重新分配给另一个进程的进程仍然存在,那么下一次它的一个线程运行时,它将在该核心上分配一个新的 ASID。整个过程发生在每个内核上。否则,如果该进程死了,那么在 future 的某个时刻,它的 ASID 将被回收。

    Linux 每个内核正好使用 6 个 ASID 的原因是它使 tlb_state 类型的大小小到足以容纳两个 64 字节缓存行。通常,在 Linux 系统上可能有数十个进程同时处于事件状态。然而,它们中的大多数通常处于休眠状态。因此 Linux 管理 ASID 空间的方式实际上非常有效。尽管看到有关 TLB_NR_DYN_ASIDS 的值对性能影响的实验评估会很有趣。但我不知道有任何此类已发表的研究。

    关于x86 - 英特尔处理器的 TLB ASID 标签中有多少位?以及如何处理 'ASID overflow' ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52813239/

    33 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com