仓库源文

.. Kenneth Lee 版权所有 2020

:Authors: Kenneth Lee :Version: 1.0

多核MMU和ASID管理逻辑


本文是回答一位同事问到的,关于有多个核运行一个或者多个进程的时候,MMU和ASID怎么 应用的问题。

复杂的问题总是来自简单问题的组合,所以,我们还是从最基本的概念开始建模。

MMU是CPU的地址翻译器,每个CPU一个,示意如下:

    .. figure:: _static/多核MMU的ASID管理.jpg

你从全系统看,pa只有一份,而每个cpu都有自己的一份va,翻译方法由页表指定,放在物 理内存里面,TLB充当这个页表内存的Cache,把常用的翻译项内置在MMU中。这是硬件角度 提供的模型。

好,现在看软件怎么用。假设我创建一个进程,我把它部署到左边的CPU上。我要设定这个 进程的页表空间,它就是这样的:

    .. figure:: _static/多核MMU的ASID管理2.jpg

如果你在另一个CPU上再创建一个进程,就是把左边的事情再做一次,这个我们就不画图了 。

如果你现在要把左边这个CPU的进程切换出去,交给另一个进程,就会这样:

    .. figure:: _static/多核MMU的ASID管理3.jpg

进程1暂时放一边,页表换成进程2的页表就行。但这个过程成本很高,因为你首先得把TLB 里面属于进程1页表的缓冲清掉,才能保证不会影响进程2的地址空间。

为了解决这个问题,我们把每个翻译条目都加上一个进程ID,简称ASID。在CPU的系统寄存 器中设置上这个ASID,这样进程1用进程1的asid,进程2用进程2的asid,两者都在TLB中, 但进程2占据CPU的时候,不会使用进程1的项,等切换回进程1的时候,原来的东西还在, 也不需要重新加载,这提高了效率。

我们当然想最好asid和软件的pid是一样的,但一般做不到,因为软件的PID通常是一个标 准字长,而asid必须嵌入页表项里,没法放太大,所以,它常常只有16位,甚至8位之类的 ,需要一个稀疏映射表才能把两者关联起来。

现在假设我在左边CPU的进程中再创建一个线程,而且把这个线程调度到第二个CPU上,这 个结果是这样的:

两个CPU共享同一个进程,它们就需要共享同一个页表,但它们需要共享同一个asid吗?答 案是:不需要。因为asid本来就不大,明明可以分开用,只要达到每个CPU的调度上限就可 以了,你现在让我公用?如果有一个进程永远不调度到我这边,我不是亏(白分配)了?

所以,靠谱的实现(比如Linux Kernel)中,asid仅在本CPU有效,扩展到IOMMU,也仅仅 对那个设备有效,不是全局的。所以,对于每个进程的asid,都是per_cpu结构,每个CPU 都有一个实例。

    | 有趣的而是,RISC-V的20190608-Priv-MSU-Ratified版本里面
    | 有这样一条修改记录:
    | Software is strongly recommended to allocate ASIDs globally,
    | so that a future extension can globalize ASIDs for imporved
    | performance and hardware flexibility。
    | 做这个统一对软件来说未来表面上肯定是利好的,
    | 因为很多方案多了一个假设可以用。但综合性能是否能够做上去,
    | 还真要用上一段时间才知道。

这时,如果其中一个CPU进行调度,把时间让给另一个进程,结果就会是这样的:

    .. figure:: _static/多核MMU的ASID管理5.jpg

TLB里面谁的页表都可以有,反正有asid区分,有一个线程被挂起,以后属于哪个CPU等调 度的时候另说,剩下就是谁占着那个CPU,谁在那个CPU上的asid生效,自然就会查那个 asid的翻译项,如果没有,就从真正的内存页表里面读进去了。

如果进程实在太多,在某个CPU上没法给他分配一个实在的了,这个好办,只要分配一个不 是当前的,然后把新分配的这个asid的内容从TLB里面全部抹掉就可以了。代码在各个平台 的上下文切换逻辑中,比如ARM64,代码在arch/arm64/mm/context.c中。但其实概念空间 逻辑都是一样的。

Linux在实现的时候用了大量的Lazy算法,所以,其实asid都不是在进程创建的时候生成的 ,而是在调度前发现没有了,就临时生成的,这对于新手来说,看代码会比较困扰的,但 还是那句话,习惯就好。