Linux 内存管理浅析

来源:互联网 发布:mac地址修改 编辑:程序博客网 时间:2024/06/17 20:24

(6). TLB Miss异常处理

这一部分我打算介绍的是软件处理部分,并不局限于TLB Miss异常处理,还包括MMU的配置,内核进程线性映射区的设置,用户进程地址的切换等。

一. MMU 软件配置和内核线性区映射配置

对于MMU的配置,其实就是对TLB的配置。前面说过,对于TLB,软件能通过写入MAS寄存器进行操作。我不想具体介绍这些寄存器,具体可以查询文档。关于MAS寄存器的操作定义,在:arch\powerpc\include\asm\mmu-book3e.h。

在PACA结构体中,有一个struct tlb_core_data,它定义了几个变量分别表示TLB1 中第一个entry,entry数和下一个entry。

struct tlb_core_data {    /*     * Per-core spinlock for e6500 TLB handlers (no tlbsrx.)     * Must be the first struct element.     */    u8 lock;    /* For software way selection, as on Freescale TLB1 */    u8 esel_next, esel_max, esel_first;};

软件能够操作TLB0和TLB1。在内核实际操作中,只是配置了TLB1。当TLB1不够用时,如果新的PMD需要更新到TLB1,那么我们要能够安全的替换TLB1中已使用的entry。这里的变量就是用于这样的替换算法的。

PACA在PowerPC中用来保存CPU的私有数据的结构体,每个CPU都有一份私有拷贝。在多核系统中,像中断处理、线程执行这些是在不同的CPU上并行执行的。那么每个CPU就需要一个私有数据拷贝以记录像中断栈空间这样的私有数据。这个结构体定义在:arch\powerpc\include\asm\paca.h。

我们前面说过,PGD表的地址非常重要,每个进程都有自己的一份拷贝。在TLB Miss发生后,内核需要查找页表。那么在异常发生后,内核需要知道是哪个进程访问虚拟地址发生了TLB Miss。在PACA中,有一个成员:pgd_t *pgd,记录了当前CPU的当前进程的PGD表地址。异常发生后,软件能够直接读取这个地址,快速的查找页表。

因为MMU的初始化配置和内核的线性映射区配置是同步完成,下面我将它们放在一起介绍。我先将函数执行过程列出来,这些函数定义在:arch\powerpc\mm\fsl_booke_mmu.c,arch\powerpc\mm\tlb_nohash.c

这里写图片描述


  • setup_page_sizes:
    这个函数通过查询MMU寄存器SPRN_TLBxPS获得TLB0和TBL1能够支持的最大page size。我们知道TLB0是固定4KB页面,则TLB1决定了MMU能够支持的最大页面SIZE。从笔者平台打印出的信息看,e6500最大支持1GB的页面映射。这个会对后面的内核线性区页面映射有影响。

  • setup_mmu_htw:
    根据平台不同,设置TLB Miss异常处理函数。注意这里的处理函数,会用来代替在异常例程初始化部分的TLB Miss异常处理。

  • early_init_this_mmu:
    根据平台不同,配置MAS4寄存器的值。发生TLB Miss异常时,相应的MAS寄存器会将MAS4中配置的值作为默认值。例如,IND标志位。这样异常发生时,软件就不需要再设置这些寄存器。

    • map_mem_in_cams,map_mem_in_cams_addr:
      这里设置内核线性映射区。虚拟地址起始地址是Page_offsit,物理地址起始地址是memstart_addr,实际配置是0。注意这里用到了一个参数linear_map_top。它开始被设置成最大物理内存结束地址,作为参数调用map_mem_in_cams。在这个函数里,我们看到,根据最大MMU支持的页面SIZE,配置了内核线性映射区的TLB entry。在e6500平台上,因为最大支持1GB,所以如果内存有4G,那么就需要4个TLB1 entry来存放内核线性映射区的PTE。在函数最后,将这4个TLB1 entry保留,并设置IPROT标志位,那么这4个TLB1 entry将不会被flush,并且不会被替换掉。最后,返回新的已经映射的线性区SIZE作为变量linear_map_top新值。

我们知道,MMU会根据TLB1 IND entry中匹配的PTE地址,自己在内存中查找PTE,自动更新到TLB0。这一步过程称之为Hardware page tablewalk。在MMU更新PTE时,会根据TLB1 IND entry中的一些标志位来设定TLB0 entry。其中TID就是。在PMD和PTE中是没有相应的PID bit位的。PID是在TLB Miss异常发生时,自动根据PID寄存器设置到相应的MAS寄存器,当我们用tlbwe命令进行设置操作时,PID会自动的更新到TLB1 相应IND entry。


二. 内核TLB Miss异常处理.

下面我们先看下软件部分是怎么处理TLB Miss异常的(e6500代码部分在arch\powerpc\mm\tlb_low_64e.s)。

/* * This is the guts of the TLB miss handler for e6500 and derivatives. * We are entered with: * * r16 = page of faulting address (low bit 0 if data, 1 if instruction) * r15 = crap (free to use) * r14 = page table base * r13 = PACA * r11 = tlb_per_core ptr * r10 = crap (free to use) */tlb_miss_common_e6500:    crmove  cr2*4+2,cr0*4+2     /* cr2.eq != 0 if kernel address */....    /* Now we build the MAS for a 2M indirect page:     *     * MAS 0   :    ESEL needs to be filled by software round-robin     * MAS 1   :    Fully set up     *               - PID already updated by caller if necessary     *               - TSIZE for now is base ind page size always     *               - TID already cleared if necessary     * MAS 2   :    Default not 2M-aligned, need to be redone     * MAS 3+7 :    Needs to be done     */    ori r14,r14,(BOOK3E_PAGESZ_4K << MAS3_SPSIZE_SHIFT)    mtspr   SPRN_MAS7_MAS3,r14    clrrdi  r15,r16,21      /* make EA 2M-aligned */    mtspr   SPRN_MAS2,r15    lbz r15,TCD_ESEL_NEXT(r11)    lbz r16,TCD_ESEL_MAX(r11)    lbz r14,TCD_ESEL_FIRST(r11)    rlwimi  r10,r15,16,0x00ff0000   /* insert esel_next into MAS0 */    addi    r15,r15,1       /* increment esel_next */    mtspr   SPRN_MAS0,r10    cmpw    r15,r16    iseleq  r15,r14,r15     /* if next == last use first */    stb r15,TCD_ESEL_NEXT(r11)    tlbwe

这里我只列出了部分代码。下面大概说下处理过程。

从前面的介绍可知,对于内核线性映射区来说,不可能发生TLB Miss异常。但除了线性区外,内核还有像vmalloc这样的页面映射区。因此内核态时还是有可能发生TLB Miss异常的。

因此在进入函数tlb_miss_common_e6500之前,需要先判断当前CPU是在内核态还是用户态。据此,R14保存的可能是内核进程的PGD地址或用户进程的PGD地址。这两个地址都可以从当前CPU的PACA里获得。

  • 首先再一次检查当前是否有匹配的TLB entry,如果有就直接返回。

  • 选择next entry作为新的TLB entry。如果原来entry有效,需要先invalid这个entry,这里用到了一个指令tlbilxva。这个指令意思是根据在MAS寄存器中配置的PID和有效地址等进行查找并invalid所有匹配的TLB entry(包括L1 TLB和L2 TLB)。

  • 查找页表。如果在查找过程中,有任何一级页表无效,表示这个虚拟地址还没有与物理页面建立映射关系,那么就会触发一个页面异常。

  • 将找到的PMD更新进TLB1 entry,并更新entry next。这里可以看到,并没有更新IND,PSIZE等信息,这是由初始化时配置的MAS4寄存器在Miss异常发生时默认载入的。另外还有TID,也是自动根据PID寄存器的值更新的。这里有个问题,PID,我们在配置的时候(后面切换的时候会说到)都是用户进程的PID,内核进程实际上是不切换PID寄存器的。那么我们怎么配置内核进程的地址映射TLB1 entry呢?这里还是判断当前是否是内核态,如果是,则清空TID,相当于设置TID为0。TID为0,对于MMU来说,比较特殊。在MMU查询TLB entry时,如果entry TID为0,则不比较TID。什么意思呢?就是说如果在TLB中有两个虚拟地址相同的entry,当一个TID为1,而另一个为0,也就是说这是两个不同进程的虚拟地址。那么当PID为1的虚拟地址送到MMU需要转换时,实际上他会匹配到TID为1 和TID为0的这两个entry。这样就发生了冲突,实际上是不容许的。那么为什么要设置内核进程的PID和TID为0呢。后面我们会看到,在用户进程切换时,需要将被切换进程的TLB entry全部invalid。这样做的目的一是为了节约TLB entry,而是因为PID有限,如果两个进程共用一个PID,那么同时在TLB中,会发生地址冲突。我们知道,切换到内核进程或从内核进程切换到用户进程是非常频繁的,那这样的每次地址空间切换其实是非常花时间的。为了节约这样的时间,实际上,内核在切换用户进程和内核进程时,是不切换当前用户进程的MMU的,即不将当前用户进程的TLB无效。而且也不切换当前进程的PID,就是说,当CPU在内核态访问内存时,它的虚拟地址中的PID是用户进程的PID,而不是内核进程的PID 0,这就是当内核态发生TLB Miss时,我们需要clear TID的原因。那这样如果地址相同,不是会同时命中用户进程的TLB entry和内核进程的TLB entry吗?实际是,我们划分了用户进程和内核进程的虚拟地址空间。在链接器进行程序链接时,代码或数据的访问地址就确定了只能在各自的虚拟地址空间内,所以说这是没问题的。啊哈,如果你在用户进程中绑定了一个内核空间的虚拟地址,说我就是想去访问这段空间,会发生什么呢?还记得我们说过的页面访问权限控制吗?对了,你在用户态访问不了绑定到内核态的物理地址的。

  • 当软件处理完异常返回时,SRR0寄存器会保存当前发生异常的虚拟地址,MMU会继续查找过程。

从这个过程看到,软件只是将PMD更新进TLB1。MMU需要完成PTE的更新过程。同时,我们也看到,如果发生了Miss异常,意味着只有PTE为4K固定页面的PTE能够被MMU自动更新进TLB0。


三. 进程切换时,MMU上下文切换.

我们前面说过,mm_struct结构体,表示了进程的地址空间。每个进程都有自己的地址空间,意味着,在切换进程时,我们需要在TLB中做些特殊处理。我们也说过,MMU的查找TLB匹配需要TID=PID,TID我们前面说过,是MMU根据PID寄存器自动配置的。那么在进程地址空间切换时,我们就需要设置PID寄存器对应到当前进程。那么进程的PID是从哪儿来的呢?

在这个结构体中,有一个mmu_context的成员,记录了MMU的相关配置。
arch\powerpc\include\asm\mmu-book3e.h

typedef struct {    unsigned int    id;    unsigned int    active;    unsigned long   vdso_base;} mm_context_t;

我去掉了一些无关代码。这里有一个ID变量,表示的就是进程地址空间的PID。

在内核中,每一个进程理论上都会有一个这样的实体来表示MMU的上下文,保存自己的PID。但在e6500平台上,我们会看到,只定义了256个这样的变量,也即只有256个PID可以使用。这里为什么要只定义256个呢。这实际上取决于TLB entry数的。在同一个TLB中,会同时保存这256个PID的entry。就是说,可以同时有256个进程地址映射驻留在TLB中,而不用每次进程切换时,TLB中地址映射也需要切换。但实际上是不够的。这里我们会看到,如果新的进程被切换执行,如果PID不够,则会找一个已分配出去的PID,在这里称之为MMU context steal。下面我们看下这些函数的作用:arch\powerpc\mm\mmu_context_nohash.c.

  • mmu_context_init:
    这个函数初始化了几个全局变量:context_map,以bit位的形式保存了PID。如果相应bit位置位,则表明相应PID已经分配出去。context_mm,保存了已经分配了PID的mm_struct数据指针。以PID为下标可以查询到对应的mm_struct实体。stale_map,以bit位的形式保存了窃取的PID。当PID不够时,新的进程,需要窃取一个已经分配出去的PID,会暂时保存到这个变量,用来表示当前的PID是从别的进程窃取而来。因为多核CPU的原因,可能多个CPU同时执行相同进程的不同线程,那么这些CPU的MMU TLB中就会有相同的PID的entry。如果这个PID被窃取了,那么所有的这些CPU中的TLB都需要将这个PID的entry无效。所以这里stale_map是一个以CPU ID为下标的数组。因此,当进程切换时,如果当前进程PID不是窃取来的,那么其实是不需要无效相应PID TLB entry的。那么这样就加速了进程切换过程。

    这里还要说下的是,窃取的PID不能是正在使用的PID。什么是正在使用的PID呢?当CPU执行相应的进程的线程时,当前进程的PID就是CPU正在使用的PID。如果其他CPU正在使用这个PID进行内存访问,你把它的PID窃取来了,那其他CPU的内存访问就会出现问题。mm_context_t中active变量就是用来记录是否有CPU正在使用当前PID(或当前mm_struct)。

  • steal_context_smp:
    如果当前进程没有被分配PID,并且没有可用的PID,那么会从已分配的进程窃取一个PID。大致过程就是找next_context变量记录的PID,如果这PID正在被使用,那么继续找下一个。找到后,会将使用这个PID的进程的mm_struct的PID置为无效,并将这个PID保存到stale_map变量。

  • switch_mmu_context:
    当进程切换时,如果切换到用户进程,会调用这个函数。这个函数会判断当前进程是有有效的PID,如果没有,则窃取一个。如果PID是窃取来,需要在当前CPU的TLB中无效相应的entry。如果不是窃取来的,则省略这步。最后,切换PID寄存器值为当前PID。注意这里,切换到内核进程,是不需要切换PID的,也即这个函数不会被调用。原因前面说过。

原创粉丝点击