本文介绍了TLB维护操作和相关指令,并结合Linux中代码进行了分析。这篇文章之后,本系列文章暂时告一段落。主要是目前loongarch相关的资料有限,描述二进制翻译扩展等扩展内容的loongarch手册第二、三卷也还没有出。
想了解更多关于开源的内容,请访问:
51CTO 开源基础软件社区
https://ost.51cto.com
前言
上一篇文章中介绍了loongarch中TLB相关的异常处理,这一篇文章继续介绍TLB相关的维护操作。
1、TLB硬件组织机构
首先补充loongarch中TLB相关的硬件背景知识。
loongarch架构中,TLB分为两个部分:一个是所有表项的页大小都相同的Singular-Page-Size TLB,简称STLB;一个是支持不同表项的页大小可以不同的Multiple-Page-Size TLB,简称MTLB。在进行虚实地址转换时,STLB和MTLB同时查找。同时软件上需保证不会出现MTLB和STLB同时命中的情况。
STLB和MTLB的表项格式基本一致,区别仅在于MTLB每个表项中均包含了一个页大小的信息,而STLB每个表项中无此信息。
回顾上一篇文章中介绍了的TLB表项格式:
其中PS(page size)域仅存在于MTLB。
2、虚拟内存系统与硬件维护
虚拟地址空间的引入为程序提供了方便,但同时也带来了其他问题。
其中,有一种称为homonym的问题,其指的是单个虚拟地址指向多个物理地址的情况。例如,在多个进程中使用了相同的虚拟地址,但这些虚拟地址实际上指向了不同的物理地址。当进程切换、虚拟地址空间切换、页表修改等情况时,硬件上的缓存数据不一定会自动进行同步,此时就会有数据不一致的问题。
因此,操作系统在软件上需要手动去维护相关的硬件数据一致性:
- TLB维护:TLB中缓存的页表项就有可能因为软件上对页表的修改,出现数据不一致的问题。操作系统需要进行invalidate(或者称为flush)TLB的操作,将相应的表项无效。
- cache维护:某些类型的cache也可能会因页表的修改,出现数据不一致的问题。对于这些cache,操作系统同样需要进行clean或invalidate操作。
(1)TLB维护与ASID
在一些早期的架构中,invalidate TLB操作实际上是将整个TLB中的数据无效,因为TLB因homonym问题无法判断其缓存的表项属于哪个进程。
但实际上,刷新整个TLB的数据是一件相当浪费性能的事,既会影响上下文切换时的速度,也会降低TLB加速的作用。因此,后来的架构一般都会在TLB中加入其他标识以识别不同的进程或者虚拟地址空间,这样TLB不需要每次切换上下文都进行invalidate操作。
ASID(Address Space ID)就是这样的标识。loongarch中支持ASID,见上文中TLB表项图,其中就有ASID域。每个TLB表项都有ASID,ASID由操作系统在软件上进行分配,一般一个虚拟地址空间有一个唯一的ASID,这样就有效减少了TLB invalidate操作的次数。
loongarch中,CSR.ASID寄存器可以控制当前TLB使用的ASID,如下图:
同时loongarch中TLB相关维护指令也支持根据ASID,只无效部分ASID匹配的表项。见后文介绍。
(2)cache维护
相较于cache维护,TLB相关的维护是本文着重介绍的内容。主要是因为目前loongarch中cache相关的资料较少。
一般来说,在上下文切换、页表修改等情况时,是否需要在软件上对cache进行维护,与具体的架构和cache类型有关。以ARM架构为例,其中VIVT类型的cache在涉及页表切换等操作时需要进行维护。
3、TLB相关维护指令
- tlbclr:tlbclr指令根据TLB相关CSR中的信息无效TLB中的内容。
- 当CSR.TLBIDX.Index落在MTLB范围内时,执行tlbclr,将MTLB中所有G=0且ASID等于CSR.ASID.ASID的表项无效。其中G、ASID为表项中的域。
- 当CSR.TLBIDX.Index落在STLB范围内时,执行tlbclr,将STLB中CSR.TLBIDX.Index对应的且G=0且ASID等于CSR.ASID.ASID的表项无效。
- tlbflush:tlbflush指令同样根据TLB相关CSR中的信息无效TLB中的内容,但作用范围较tlbclr指令更广。
- 当CSR.TLBIDX.Index落在MTLB范围内时,执行tlbflush,将MTLB中所有的表项无效。
- 当CSR.TLBIDX.Index落在STLB范围内时,执行tlbflush,将STLB中CSR.TLBIDX.Index对应的表项无效。
- invtlb op, rj, rk:invtlb指令同样用于无效TLB中的内容,但相较于tlbclr和tlbflush指令更加灵活。
- op表示操作类型,下面是loongarch手册中列出的op类型:
- op=0:清除所有表项。
- op=1:清除所有表项。效果和op=0完全一致。
- op=2:清除所有G=1的表项。
- op=3:清除所有G=0的表项。
- op=4:清除所有G=0,且ASID等于寄存器指定ASID的表项。
- op=5:清除所有G=0,ASID等于寄存器指定ASID,且VA等于寄存器指定VA的表项。
- op=6:清除所有G=1或ASID等于寄存器指定ASID,且VA等于寄存器指定VA的表项。
- 通用寄存器rj中存放ASID信息。当op对应的操作不需要ASID时,rj应设置为r0。
- 通用寄存器rk中存放VA虚拟地址信息。当op对应的操作不需要VA时,rk应设置为r0。
下面用linux中loongarch下TLB flush相关API对invtlb指令举例说明。
注:目前loongarch手册中的op操作类型似乎不全。
// invtlb指令中的op
enum invtlb_ops {
/* Invalid all tlb */
INVTLB_ALL = 0x0,
/* Invalid current tlb */
INVTLB_CURRENT_ALL = 0x1,
/* Invalid all global=1 lines in current tlb */
INVTLB_CURRENT_GTRUE = 0x2,
/* Invalid all global=0 lines in current tlb */
INVTLB_CURRENT_GFALSE = 0x3,
/* Invalid global=0 and matched asid lines in current tlb */
INVTLB_GFALSE_AND_ASID = 0x4,
/* Invalid addr with global=0 and matched asid in current tlb */
INVTLB_ADDR_GFALSE_AND_ASID = 0x5,
/* Invalid addr with global=1 or matched asid in current tlb */
INVTLB_ADDR_GTRUE_OR_ASID = 0x6,
/* Invalid matched gid in guest tlb */
INVGTLB_GID = 0x9,
/* Invalid global=1, matched gid in guest tlb */
INVGTLB_GID_GTRUE = 0xa,
/* Invalid global=0, matched gid in guest tlb */
INVGTLB_GID_GFALSE = 0xb,
/* Invalid global=0, matched gid and asid in guest tlb */
INVGTLB_GID_GFALSE_ASID = 0xc,
/* Invalid global=0 , matched gid, asid and addr in guest tlb */
INVGTLB_GID_GFALSE_ASID_ADDR = 0xd,
/* Invalid global=1 , matched gid, asid and addr in guest tlb */
INVGTLB_GID_GTRUE_ASID_ADDR = 0xe,
/* Invalid all gid gva-->gpa guest tlb */
INVGTLB_ALLGID_GVA_TO_GPA = 0x10,
/* Invalid all gid gpa-->hpa tlb */
INVTLB_ALLGID_GPA_TO_HPA = 0x11,
/* Invalid all gid tlb, including gva-->gpa and gpa-->hpa */
INVTLB_ALLGID = 0x12,
/* Invalid matched gid gva-->gpa guest tlb */
INVGTLB_GID_GVA_TO_GPA = 0x13,
/* Invalid matched gid gpa-->hpa tlb */
INVTLB_GID_GPA_TO_HPA = 0x14,
/* Invalid matched gid tlb,including gva-->gpa and gpa-->hpa */
INVTLB_GID_ALL = 0x15,
/* Invalid matched gid and addr gpa-->hpa tlb */
INVTLB_GID_ADDR = 0x16,
};
/*
* invtlb op info addr
* (0x1 << 26) | (0x24 << 20) | (0x13 << 15) |
* (addr << 10) | (info << 5) | op
*/
// 基于机器码封装了invtlb op info addr格式的指令
static inline void invtlb(u32 op, u32 info, u64 addr)
{
__asm__ __volatile__(
"parse_r addr,%0\n\t"
"parse_r info,%1\n\t"
".word ((0x6498000) | (addr << 10) | (info << 5) | %2)\n\t"
:
: "r"(addr), "r"(info), "i"(op)
:
);
}
// invtlb op 0 0指令
static inline void invtlb_all(u32 op, u32 info, u64 addr)
{
__asm__ __volatile__(
".word ((0x6498000) | (0 << 10) | (0 << 5) | %0)\n\t"
:
: "i"(op)
:
);
}
4、上下文切换和TLB维护
本节结合linux中上下文切换部分代码对TLB invalidate操作进行分析。
以下为linux中context_switch上下文切换函数的流程:
context_switch(struct rq *rq, struct task_struct *prev,
| struct task_struct *next)
| // 更新时间片、最近进队时间等调度信息以及其他准备工作
|-> prepare_task_switch(rq, prev, next);
|
| // 虚拟地址空间切换相关
|-> mm = next->mm;
| oldmm = prev->active_mm;
| if (!mm) {
| next->active_mm = oldmm;
| atomic_inc(&oldmm->mm_count); // 增加oldmm的引用计数
| // 其他架构(x86)相关,这里不关注
| enter_lazy_tlb(oldmm, next);
| } else
| // 切换到用户进程,需切换进程虚拟空间
| switch_mm(oldmm, mm, next);
|
|-> ...
|
| // 切换任务上下文
|-> switch_to(prev, next, prev);
| // 切换后再次被调度时向下执行
|
| // 用barrier机制同步,
| // 保证switch_to和finish_task_switch的执行顺序
|-> barrier();
| // 使用gcc内联汇编:::"memory"语法实现,
| // 这样编译器不会优化此语句前后的访存顺序
|-> #define barrier() __asm__ __volatile__("": : :"memory")
|
| // 再次被调度时,进行清理工作
|-> finish_task_switch(this_rq(), prev);
在进行任务上下文切换函数switch_to之前,如果涉及进程虚拟地址空间改变,则需要切换mmu上下文。上面switch_mm函数的作用就是切换mmu上下文。
loongarch架构代码中switch_mm及相关函数的分析如下:
switch_mm(struct mm_struct *prev, struct mm_struct *next,
| struct task_struct *tsk)
|-> switch_mm_irqs_off(prev, next, tsk);
|-> unsigned int cpu = smp_processor_id();
|
| /* Check if our ASID is of an older version and thus invalid */
| // 如果asid不同,则需重新分配
|-> if (!asid_valid(next, cpu))
| get_new_mmu_context(next, cpu);
|
| // 写入asid到寄存器CSR.ASID
|-> write_csr_asid(cpu_asid(cpu, next));
|
| // 切换页表
|-> if (next != &init_mm)
| csr_writeq((unsigned long)next->pgd, LOONGARCH_CSR_PGDL);
| else
| csr_writeq((unsigned long)invalid_pg_dir, LOONGARCH_CSR_PGDL);
|
|-> ...
#define cpu_context(cpu, mm) ((mm)->context.asid[cpu])
#define asid_cache(cpu) (cpu_data[cpu].asid_cache)
static inline int asid_valid(struct mm_struct *mm, unsigned int cpu)
{
// 如果mm中的asid和当前asid_cache不同,则返回无效
if ((cpu_context(cpu, mm) ^ asid_cache(cpu)) & asid_version_mask(cpu))
return 0;
return 1;
}
static inline void
get_new_mmu_context(struct mm_struct *mm, unsigned long cpu)
{
| // 循环递增地分配新的asid,为旧的asid_cache + 1
|-> u64 asid = asid_cache(cpu);
| if (!((++asid) & cpu_asid_mask(&cpu_data[cpu])))
| // 当asid溢出时开始新的分配循环,此时需刷新TLB中所有用户部分
| local_flush_tlb_user(); /* start new asid cycle */
|
| // 将mm中asid和当前asid_cache设为新分配的asid
|-> cpu_context(cpu, mm) = asid_cache(cpu) = asid;
}
其中,switch_mm函数主要完成两个任务:
- 维护TLB:如前文所述,loongarch中上下文切换时需要维护TLB数据一致性。在上面的代码中,是结合ASID进行实现:
- 每次检测ASID是否变化,如果变化则说明虚拟地址空间需要进行切换,CSR.ASID寄存器需要重新设置
- 上面代码实现中是通过循环递增的方式分配新的ASID,当ASID溢出时需要使用invalidate TLB操作来保证数据一致性
- 切换页表:不同的用户虚拟地址空间有不同的页表,需通过设置相关寄存器进行切换。页表相关的配置可参考前面的文章。
上面的asid_valid函数检测ASID是否变化,get_new_mmu_context函数负责重新分配ASID和ASID溢出时调用local_flush_tlb_user函数进行invalidate TLB操作。
local_flush_tlb_user函数分析如下:
local_flush_tlb_user(void)
| // 刷新所有global=0的TLB表项
|-> invtlb_all(INVTLB_CURRENT_GFALSE, 0, 0);
另外,上面代码中ASID管理部分可以进一步改进,因为每次检测到ASID变化后,mm结构体被设置了一个新分配的ASID,这样实际上未能利用mm结构体中原来的ASID和TLB中对应缓存数据。
总结
本文介绍了TLB维护操作和相关指令,并结合linux中代码进行了分析。这篇文章之后,本系列文章暂时告一段落。主要是目前loongarch相关的资料有限,描述二进制翻译扩展等扩展内容的loongarch手册第二、三卷也还没有出。
最后,在查询loongarch资料(主要是基于龙芯手册第一卷1.02和linux中loongarch部分源码)的过程中也发现了一些不足点或者是不够详细的地方,这里一并列出:
- 指令的介绍有信息遗漏:如第一篇文章中列出的一部分手册上没有,但在代码中出现的move等指令;又如前文中的invtlb指令中的op操作类型相比代码中缺少一些
- 内存一致性模型,内存访存类型的信息不够详细:第二篇文章中提到了这点,如手册中出现的一致可缓存等术语没有解释
- 中断机制相关信息不够详细
- cache相关信息不够详细
想了解更多关于开源的内容,请访问:
51CTO 开源基础软件社区
https://ost.51cto.com