文章目录
- [1. 前言](#1. 前言)
- [2. TLB ASID 的硬件支持](#2. TLB ASID 的硬件支持)
-
- [2.1 概念](#2.1 概念)
- [2.2 TLB 查找](#2.2 TLB 查找)
- [3. Linux 下 TLB ASID 管理](#3. Linux 下 TLB ASID 管理)
- [4. 参考资料](#4. 参考资料)
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. TLB ASID 的硬件支持
2.1 概念
-
什么是
TLB?
TLB是Translation Lookaside Buffers的缩写,MMU 将虚拟地址(VA)翻译为物理地址(PA)时,要经过页表遍历(page table walk)过程,每访问一级页表就要一次内存访问,相对来说,这个延迟还是相对较大的。为了提高性能,硬件上引入了 TLB cache 缓存,首次访问一个 VA 后,将 MMU 转换的 PA 以对应的 VA 为 tag 缓存到 TLB 缓存,下次再访问同一 VA,就可以通过以 VA 为 tag 从 TLB 搜索提取对应的 TLB entry 了,不用再经历漫长的page table walk过程。当然,TLB 的容量有限,只能缓存有限个数 VA 翻译的 PA 地址,这就需要做一定的管理,在 TLB 耗尽时,通过某种算法,用新的 PA 将一些旧的缓存替换掉。 -
什么是 TLB ASID?
ASID是Address Space Identifier的缩写,标识属于特定进程的TLB entries。那为什么需要它?前面说过,TLB 容量有限,在系统中属于珍贵资源;另外,在系统进程的切换过程中,会进行页表切换,而每个进程的页表不一样,那意味着,当前进程的 TLB 缓存的地址翻译内容,对新进程将失效,所以页表切换需要进行整个 TLB cache 的 flush,这会带来不小的性能损失。于是硬件上引入了nG(not Global)标志位 和ASID,来对该问题进行优化:nG=0标识 TLB entries 属于 ASID 标识的进程,nG=1标识 TLB entries 属于全局的内核空间地址(所有进程共享内核空间的页表映射)。这样,在切换进程的页表时候,只需要为新进程分配一个 ASID 来标识自己的 TLB entries,在 ASID 或 TLB cache entries 消耗完之前,都不需要刷 TLB cache 了。
2.2 TLB 查找
nG 标志位同时位于最后一级页表的表项和 TLB entries 中,然后首次内存访问某个地址时,记录到 TLB entries 中。而当前进程的 ASID,会在进程页表切换时设置到 TTBRx 寄存器中,同时也会在首次访问某个 VA 地址时,记录到 TLB entries 中。当然,TLB entries 也记录访问 VA 地址的 VA tag。这样,在后续访问某一 VA 地址时,首先比较 VA 地址 TLB entries VA tag,如果不匹配,则表示 TLB miss,要继续进行 page table walk 来翻译 PA;如果相同,则继续查看 TLB entry 的 nG 位,如果为 nG=0,则意味着是内核空间地址的 TLB entry,所有进程共享,即命中了 TLB,返回 TLB entry 保存的 PA 即可;如果 nG=1,则表示是某进程特定的 TLB entry,通过 ASID 标识,则继续比较 TLB entry 的 ASID 和 TTBRx 寄存器存储的当前进程的 ASID,如果值相同,则标识命中,否则标识 TLB miss,否则要继续进行 page table walk 来翻译 PA。
3. Linux 下 TLB ASID 管理
以 ARMv7 架构 + Linux 4.14.x 内核简略的分析下 TLB ASID 的管理细节,来看代码:
c
typedef struct {
#ifdef CONFIG_CPU_HAS_ASID
atomic64_t id; /* ASID: generation | HW ASID */
#else
...
#endif
...
} mm_context_t;
c
void check_and_switch_context(struct mm_struct *mm, struct task_struct *tsk)
{
unsigned long flags;
unsigned int cpu = smp_processor_id();
u64 asid;
...
asid = atomic64_read(&mm->context.id); /* 读取新进程的 ASID */
/*
* a. !((asid ^ atomic64_read(&asid_generation)) >> ASID_BITS
* 新进程 asid 属于/分配自 当前 generation
* b. atomic64_xchg(&per_cpu(active_asids, cpu), asid)
* a.成立的条件下, 顺便设定 当前 CPU 激活的 (进程的) asid,
* 然后就可以进入页表的切换过程了.
*/
if (!((asid ^ atomic64_read(&asid_generation)) >> ASID_BITS)
&& atomic64_xchg(&per_cpu(active_asids, cpu), asid))
goto switch_mm_fastpath; /* ASID 无需更新,直接进入页表切换过程 */
raw_spin_lock_irqsave(&cpu_asid_lock, flags);
/* Check that our ASID belongs to the current generation. */
asid = atomic64_read(&mm->context.id);
/* 新进程 asid 不 属于/分配自 当前 generation, 要重新从当前 generation 为新进程分配 asid */
if ((asid ^ atomic64_read(&asid_generation)) >> ASID_BITS) {
asid = new_context(mm, cpu); /* ASID 管理操作 */
atomic64_set(&mm->context.id, asid); /* 为 进程 分配的 ASID 记录到 进程的 mm_struct */
}
/* 当前 generation ASID 耗尽,需要刷掉所有的 TLB cache */
if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending)) {
local_flush_bp_all();
local_flush_tlb_all();
}
atomic64_set(&per_cpu(active_asids, cpu), asid); /* 当前 CPU 激活的 (进程的) asid */
cpumask_set_cpu(cpu, mm_cpumask(mm));
raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);
}
static u64 new_context(struct mm_struct *mm, unsigned int cpu)
{
static u32 cur_idx = 1;
u64 asid = atomic64_read(&mm->context.id);
u64 generation = atomic64_read(&asid_generation);
/*
* 在创建新的进程的时候会分配一个新的 mm, 其(mm->context.id)初始化为 0.
* 如果 asid 不等于 0, 那么说明这个 mm 之前就已经分配过 software asid
* (generation + hw asid)了.
*/
if (asid != 0) {
u64 newasid = generation | (asid & ~ASID_MASK);
/*
* If our current ASID was active during a rollover, we
* can continue to use it and this was just a false alarm.
*/
if (check_update_reserved_asid(asid, newasid))
return newasid;
/*
* We had a valid ASID in a previous life, so try to re-use
* it if possible.,
*/
/* 如果新 generation 中旧的 asid 还未被分配出去, 重用它 */
asid &= ~ASID_MASK;
if (!__test_and_set_bit(asid, asid_map))
return newasid; /* 返回更新了 generation 的 asid */
}
/*
* 如果 asid 等于 0, 说明我们的确是需要分配一个新的 HW asid,
* 这时候首先要找一个空闲的 HW asid, 如果能够找到, 那么直接返
* 回 software asid (当前 generation + 新分配的 hw asid); 否则,
* 表示 asid 消耗完了,生成新的 generation 并重新进行分配???
*/
asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, cur_idx);
if (asid == NUM_USER_ASIDS) { /* ASID 消耗完了,需重新分配 */
generation = atomic64_add_return(ASID_FIRST_VERSION,
&asid_generation); /* 递增 ASID generation */
flush_context(cpu);
asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, 1);
}
__set_bit(asid, asid_map); /* 更新 ASID 分配位图: 标记 asid 已经被分配 */
cur_idx = asid;
cpumask_clear(mm_cpumask(mm));
return asid | generation; /* 返回新分配的 asid */
}
static void flush_context(unsigned int cpu)
{
int i;
u64 asid;
/* Update the list of reserved ASIDs and the ASID bitmap. */
bitmap_clear(asid_map, 0, NUM_USER_ASIDS);
for_each_possible_cpu(i) {
asid = atomic64_xchg(&per_cpu(active_asids, i), 0);
/*
* If this CPU has already been through a
* rollover, but hasn't run another task in
* the meantime, we must preserve its reserved
* ASID, as this is the only trace we have of
* the process it is still running.
*/
if (asid == 0)
asid = per_cpu(reserved_asids, i);
__set_bit(asid & ~ASID_MASK, asid_map);
per_cpu(reserved_asids, i) = asid;
}
/* Queue a TLB invalidate and flush the I-cache if necessary. */
cpumask_setall(&tlb_flush_pending); /* 当前 generation 的 asid 耗尽, 更新 generation, 标记要刷 TLB */
if (icache_is_vivt_asid_tagged())
__flush_icache_all();
}
4. 参考资料
1\] DDI0406C_d_armv7ar_arm.pdf \[2\] [TLB原理](https://zhuanlan.zhihu.com/p/108425561 "TLB原理")