#loongarch架构介绍# [五] TLB维护

时间:2022-05-09 00:40:36

作者:蒋卫峰 李涛

前言

上一篇文章中介绍了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表项格式: #loongarch架构介绍# [五] TLB维护

其中PS(page size)域仅存在于MTLB。

2. 虚拟内存系统与硬件维护

虚拟地址空间的引入为程序提供了方便,但同时也带来了其他问题。

其中,有一种称为homonym的问题,其指的是单个虚拟地址指向多个物理地址的情况。例如,在多个进程中使用了相同的虚拟地址,但这些虚拟地址实际上指向了不同的物理地址。当进程切换、虚拟地址空间切换、页表修改等情况时,硬件上的缓存数据不一定会自动进行同步,此时就会有数据不一致的问题。

因此,操作系统在软件上需要手动去维护相关的硬件数据一致性:

  • TLB维护:TLB中缓存的页表项就有可能因为软件上对页表的修改,出现数据不一致的问题。操作系统需要进行invalidate(或者称为flush)TLB的操作,将相应的表项无效。

  • cache维护:某些类型的cache也可能会因页表的修改,出现数据不一致的问题。对于这些cache,操作系统同样需要进行clean或invalidate操作。

2.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维护

同时loongarch中TLB相关维护指令也支持根据ASID,只无效部分ASID匹配的表项。见后文介绍。

2.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相关信息不够详细

更多原创内容请关注:深开鸿技术团队

入门到精通、技巧到案例,系统化分享OpenHarmony开发技术,欢迎投稿和订阅,让我们一起携手前行共建生态。

想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com/#bkwz​