天天看點

結合Linux的應用場景看MIPS32架構之記憶體管理

由于本系列文檔在介紹過程中,參考了很多MIPS官方,以及北京君正(Ingenic)的xburst系列處理器的資料,目的僅僅是為拓展MIPS架構以及Linux進自己的綿薄之力,如果有侵權行為時,請告知本人處理,謝謝

1 MIPS32的記憶體管理

1.1 引子

談論一個話題,總得有個頭兒,我們從哪裡開始呢?MIPS官方文檔和<<See MIPS Run Linux>>介紹MIPS架構是按照一個子產品一個子產品進行介紹,每一個子產品獨立成一個部分,這個是最快的也是最合理的介紹方式,不過沒有作業系統的原理作為基礎,看過這些文檔或者書籍還是不能對MIPS架構有深入的了解。這裡我不打算按照這種方式把官方文檔再翻譯一遍,而是換種思路來聊聊MIPS32的CPU架構.

大部分人程式設計生涯的入門都是從C語言開始的,在沒有接觸過計算機體系結構之前的C語言僅僅是停留在語言的層面,對于程式設計語言是如何控制計算機的沒有深入的了解(至于其他進階語言,比如Java,Python等等,對于計算機運作程式的原理就更加偏遠了)。是以我們就從最簡單的一個C語言代碼開始談起,看看下面一段代碼

  1 int c;

  2 

  3 int main(int argc, char *argv[])

  4 {

  5         int a, b;

  6 

  7         a = 5;

  8         b = 2;

  9         c = a * b;

 10 

 11         return 0;

 12 }

上面的代碼很簡單,隻要你學過程式設計語言(無論是不是C語言),這段代碼你肯定能看懂,你一定知道c的值是a*b=2*5=10, 它太簡單了,不過還是有幾個問題需要提前考慮一下:

<1> 學過C語言的都知道,C程式的入口位址是main函數,可是現實真的是這樣嗎?

<2> 有沒有考慮過,你所知道的CPU是如何執行這段代碼的呢?

<3> CPU是執行機器指令的,那麼CPU内部的PC,MMU(TLB),CACHE等等是如何共同協作來完成這段代碼的執行?

由于我們是基于Linux來介紹,是以這裡先把這段代碼編譯成ELF檔案,然後再反彙編出對應的彙編指令

 339 004005e0 <main>:

 340   4005e0:       27bdffe8        addiu   sp,sp,-24

 341   4005e4:       afbe0014        sw      s8,20(sp)

 342   4005e8:       03a0f021        move    s8,sp

 343   4005ec:       afc40018        sw      a0,24(s8)

 344   4005f0:       afc5001c        sw      a1,28(s8)

 345   4005f4:       24020005        li      v0,5

 346   4005f8:       afc20008        sw      v0,8(s8)

 347   4005fc:       24020002        li      v0,2

 348   400600:       afc2000c        sw      v0,12(s8)

 349   400604:       8fc30008        lw      v1,8(s8)

 350   400608:       8fc2000c        lw      v0,12(s8)

 351   40060c:       70621802        mul     v1,v1,v0

 352   400610:       3c020041        lui     v0,0x41

 353   400614:       ac43080c        sw      v1,2060(v0)

 354   400618:       00001021        move    v0,zero

 355   40061c:       03c0e821        move    sp,s8

 356   400620:       8fbe0014        lw      s8,20(sp)

 357   400624:       27bd0018        addiu   sp,sp,24

 358   400628:       03e00008        jr      ra

 359   40062c:       00000000        nop

DUMP檔案拿到後,我們先不介紹MIPS架構的指令集以及寄存器等資訊,而是看看MIPS32的處理器是如何執行這段指令的,比如340行的位址0x4005e0處的指令27bdffe8.

CPU在開始執行這個程式之前,需要提前做很多準備性的工作,這些工作是由作業系統來完成,這些工作包括哪些内容呢?首先需要由父程序(比如shell)通過fork系統調用fork出一個子程序出來,然後在子程序中調用execve系統調用(execve這個系統調用很複雜,我們這裡的重點是看MIPS32的CPU如何執行代碼,這裡對于這個系統調用不多介紹)去執行這段程式對應的ELF檔案.execve系統調用的工作對于了解CPU是如何執行這段代碼至關重要,首先思考兩個簡單的問題:Q1:在OS還沒有将PC改為這個程式的入口處前(或者說execv()系統調用傳回到使用者空間前)實際的(DDR)RAM中是怎樣的,有沒有将這段代碼對應的ELF中的text段讀入到其中?Q2:這個代碼段是由誰來加載的?

對于Linux記憶體管理有一定了解的人都清楚上面的答案,下面我們從MIPS體系結構的角度來讨論一下兩個問題的具體答案。

execve的過程在這裡不進行探讨,但是它的目的必須要說明白,execve執行完畢後會将這個程式對應的ELF檔案load到記憶體中,準确的說應該是根據這個ELF在記憶體中做好了映射(或者說是布局),做好映射後的圖大家應該都知道,如圖1

結合Linux的應用場景看MIPS32架構之記憶體管理

圖1 應用程式虛拟記憶體映射圖

execve系統調用并沒有把ELF中的所有段中的内容(比如代碼段中的指令部分)都讀入到實體記憶體中,因為這麼做可能會浪費時間、浪費資源,Linux kernel的做法是将ELF中的代碼段,init段(也是代碼),隻讀資料段會在read only segments中做好映射(mapping)(這裡的mapping是指将ELF檔案中相應的段和虛拟記憶體進行關聯,通路這片虛拟記憶體,就會讀取相應的檔案中的資料,類似與我們在使用者空間使用的mmap系統調用一樣),全局的初始化資料段會在Data Segments中做好mapping,未初始化的資料段會在bss segments中做mapping(當然這部分會在execve中初始化0),堆空間開始時沒有做mapping,而是在後期使用過程中不斷擴大的,如果ELF檔案是動态編譯的,那麼會将動态連結庫映射到memory mapping segment區域,不過這個工作不是execve系統調用完成的,而是由使用者空間的linker完成的,而棧頂以及argv,environ變量區域會在execve中指定好.

如果足夠仔細,你可能會發現上面這個圖和你之前在其他資料上的應用程式記憶體映射圖有不同地方,不同的地方主要展現在位址空間的大小,比如說,在X86上kernel空間是高1G,使用者空間是低3G,而這裡kernel空間是高2G,使用者空間是低2G,這是什麼原因呢?這個是由MIPS32的記憶體映射所決定的.

1.2 MIPS32 R2的記憶體映射

首先介紹一下MIPS32R2的運作模式,MIPS32R2主要有兩種運作模式,一種是kernel模式,一種是user模式(還有一種debug模式,這裡我們不過多介紹).不同的模式下虛拟記憶體和實體記憶體的映射關系是不同的,如下圖2

結合Linux的應用場景看MIPS32架構之記憶體管理

圖2 MIPS32虛拟記憶體映射圖

通過圖2我們可以知道,在user mode下,CPU能夠通路的記憶體範圍就是在0x0到0x7fffffff之間的2G虛拟記憶體空間,可能有人會問,如果在這種模式下,要通路高2G的空間,也就是0x80000000以上的虛拟記憶體怎麼辦,這時候CPU是通路不到這片空間,如果強制通路那CPU也得有恰當的方式來處理這種”不情之請”,CPU通過”抛出”一個位址錯誤異常(異常放到後面去介紹)來處理這個操作.低2G的虛拟記憶體空間這部分的通路特點是經過cache(可選擇是否經過)和TLB的,通過圖上我們可以看到”mapped”字樣,就是這個意思.也就是說,當CPU通路這片空間時,需要經過TLB進行轉換翻譯後才能知道對應的實體位址在什麼地方.這段空間是在應用程式運作時所通路的的空間,

在圖2中可以看到,kernel mode下,CPU能夠通路的虛拟位址空間是從0x0到0xffffffff整個4G的空間,不過這4G空間又分成了幾個小段:

(1)低2G的虛拟記憶體空間(0x0-0x7fffffff)稱為kuseg區,這片空間是”mapped”,通路時需要經過TLB和cache,其實這段空間在核心中copy_to_user或者copy_from_user時會通路到.

(2)從0x80000000到0x9fffffff對應的稱為kseg0區域,這個區域的特性是”unmapped cacheable”,”unmapped ”這個就是說通路這段空間時,能夠直接找到對應實體位址,是不用經過TLB的,其對應的實體位址直接将最高3bit或者1bit清0得到(高3bit和高1bit清0效果是一緻的),也就是0x0-0x1fffffff.而”cacheable”這個是說通路這片空間是可以經過cache的,對MIPS有些了解的人可能納悶”為什麼說可以經過cache”,這是由于通路這片空間可以通過配置CP0的config寄存器來決定是否經過cache,以及經過cache的方式,比如write back或者write through等.這片空間是作業系統核心所用的空間,啟動過程中,bootloader也是運作在這片空間中.

(3)從0xa0000000到0xbfffffff對應的稱為kseg1區域,這個區域是”unmapped uncached”,”unmapped”的含義以及介紹過了,這片區域對應的實體位址也是經過将高3bit清0得到,也是0x0-0x1fffffff,如果細心點可能發現這和kseg0對應的實體位址空間完全一緻啊,是的,kseg1和kseg0對應的實體位址完全一緻,但是有一點不同,也就是另一個屬性”uncached”,這個是說通路這片虛拟記憶體空間時,直接通路實體記憶體空間,是不經過cache的,而kseg0是”cacheable”,可以經過cached.可能有人會有疑問,為什麼要這麼設計(kseg0和kseg1對應的實體記憶體空間一緻,而通路的屬性不同)呢?這個問題不太容易回到,但是有一條是肯定的,考慮到IO空間也是統一編址,需要有一段空間是不能經過cache的.這片空間主要在通路IO空間時使用.

(4)從0xc0000000-0xffffffff的空間是Kseg2和kseg3,每個空間各占512M,這片空間的屬性是”mapped”,和kuseg區域一緻,也就是通路這片空間的位址時需要經過TLB轉換,什麼時候會用到這片空間呢?當核心中使用vmalloc時,配置設定的空間就是在這裡.

下面看一下kernel模式下的虛拟記憶體到實體記憶體的映射圖,如圖3

結合Linux的應用場景看MIPS32架構之記憶體管理

圖3 MIPS32的虛拟核心到實體記憶體的映射

可以看到,0x80000000-0x8fffffff間的kseg0區域映射到物實體記憶體的0x0到0xfffffff空間,這個空間是256M,是核心,bootloader等代碼運作的空間,而0xa0000000-0xafffffff間的kseg1,映射的實體記憶體空間也是0x0-0xfffffff的256M空間,由于和上面的重合,是以一般情況下我們很少用到這片空間.0xb0000000-0xbfffffff間的kseg1映射到了實體記憶體的0x10000000-0x1fffffff的256M空間,一般實體記憶體的0x10000000開始的空間我們映射到了IO空間,是以如果要通路IO空間時我們需要通路這段空間,而0x90000000-0x9fffffff間的空間雖然也映射到了這片實體空間内,但是由于這段空間是經過cache的,是以我們基本不會用到這片空間.

可能有人會有疑問,如果DDR的實體記憶體大于256M,核心想使用這片空間怎麼辦?在MIPS32中高于256M的實體記憶體我們稱為高端記憶體(High memory),如果想使用高端記憶體,需要配置highmemory的配置選項,通過vmalloc申請(注意,如果沒有配置highmemory的話,那麼通過vmalloc申請記憶體所占的虛拟空間仍然是kseg2,也就是0xc0000000,但是實際的實體空間仍然在低256M).

再額外補充一個問題:實體位址和RAM的關系是怎樣的呢?實際上DDR僅僅用了實體位址的一部分,實體位址還包括的IO空間(各種外部控制器(UART控制器)的寄存器位址)等,是以DDR的位址配置設定可能是分段的。

經過上面的介紹後,應該對于圖1中的位址和X86不一緻的原因以及MIPS32的記憶體管理有個大概了解了.下面我們開始看看CPU是如何從位址4005e0處取出指令27bdffe8并執行的.

根據上面的分析可以知道,0x4005e0這個位址位于useg區域,是需要經過TLB轉換翻譯,經過cache緩存的.現在假設CPU的PC寄存器中的内容已經是0x4005e0,那麼CPU是如何從這個位址中取出指令并執行的呢?期間經過了哪些需要軟體人員關注的環節呢?

注意:MIPS 的PC和ARM的不同點:MIPS的PC是存在的,但是對于軟體人員來說不可見,不可直接操作

下面的列出的圖5大概描述了MIPS32 R2取一條指令需要經過的步驟

結合Linux的應用場景看MIPS32架構之記憶體管理

圖5 MIPS32 取值過程圖

按照前面的例子對這幅圖檔進行介紹,首先CPU結合目前的運作在user模式,判斷0x4005e0這個位址是經過屬于useg區域,需要經過TLB轉換的,是以取值單元經過第一步去檢視TLB中是否有0x4005e0這個虛拟位址的比對項,如果存在有效的比對項,那麼拿到實體位址後,就會去cache中檢視是否已經有這個實體位址對應的有效資料(對應圖中的步驟2),如果有,那麼取值單元就會取到該位址對應的指令(對應圖中的步驟3),如果cache中沒有,那麼就會在DDR中取出這條指令(對應圖中的4-2步驟),以及同時會緩存這條指令以及位址附近的資料到cache中(對應了圖中的4-1步驟),如果是第一次執行,這個時候TLB中沒有這個位址對應的entry的,該如何處理呢?這時候需要将虛拟位址和實體位址間的映射關系填到TLB中,CPU是通過TLB refill異常來完成這個工作的,在MIPS32體系結構中,這個需要軟體人員來處理,而不像ARM或者X86那樣硬體自動完成.那麼對于這個工作會引來下面的疑問:

Q1:TLB refill到底refill了什麼?是怎麼完成的?

Q2:refill之後的虛拟位址映射的實體位址一定是有效的嗎?如果不一定,那麼假如是無效的又該如何處理?

對于上面的兩個問題,我們先來看看MIPS32 R2的MMU機制.

1.3 TLB的結構

首先看一下TLB的結構

結合Linux的應用場景看MIPS32架構之記憶體管理

圖6 MIPS32 TLB的結構圖

這裡的例子中,TLB共有32個entry,可以把這個”entry“形象的了解為每一個entry對應的是一行,一個entry中可分為兩大部分:Tag array和Data array。可能有人會問TLB不是處理位址轉換的嗎?怎麼還扯上Data了?其實這裡的”Data“并不是問題中的那個”Data“,TLB實際上就是頁表的一種硬體CACHE的實作形式。這裡Tag array中又包含了4個部分:PageMask,VPN2,G,和ASID,圖7較長的描述了這部分。而Data array中8部分:PFN0,C0,D0,V0,PFN1,C1,D1,V1,圖8較長的描述了這部分.

結合Linux的應用場景看MIPS32架構之記憶體管理

圖7  TLB Tag域

<1> Pagemask指的是協處理器CP0的register5,select0寄存器,[24:13]表示的是該寄存器的13到24bit,該部分用于表示頁的大小,核心中一般是4k,是以我們在系統啟動過程中,該部分會被設定為0,當需要一些大頁映射的場景時,比如需要用到hugepages,那麼就需要修改這部分,占用了12bit.

<2> VPN2[31:13]表示的是虛拟位址的第13bit到31bit,其中的2的含義是說這個VPN會對應兩個實體頁,占用了19bit.

<3> G表示的是對于所有的程序這個映射關系都是有用的,全局性的,占用了1bit.

<4> ASID用于表示這個映射關系是僅僅針對特定的程序的,占用了8個bit,可能有人會有疑問,在核心中程序的數目可是可以有很多的,遠遠超過了8個bit所能容納的資料,這是如何做到每個程序都有不同的映射關系的呢?實際上很簡單,開始ASID從一個數值(kernel中不是0開始的)開始增長,随着新程序被排程,這個數值也不斷增加,當增加到255時,再排程新的程序時,就把TLB中的表項全清除,然後這個ASID就可以從開始遞增了.不過kernel中這部分代碼是有bug的,并且存在了很多年,一直到目前為止都沒有修正,這是由于bug的觸發很難複現,bug的原因是由于對于kernel中的ASID溢出後處理不完善導緻的不同的位址空間的程序可能會非法通路到同樣的實體位址上去,或者說是一個私有的實體位址可能會被不同的位址空間的程序所共享。

結合Linux的應用場景看MIPS32架構之記憶體管理

圖8 TLB Data 域

<1> PFN0[31:12]PFN1[31:12],這裡代表的是實體頁位址的第12bit到31bit,之是以有兩個是由于MIPS32 R2采用了一個虛拟頁映射兩個實體頁的的方法,以減少TLB的容量,這是如何做到的呢?還記得前面的VPN2的位數是從13bit到31bit共19bit,而這裡是從12bit開始,是以這個12bit就是作為奇偶頁的選擇bit,如果12bit是0,那麼就選擇PFN0,否則選擇PFN1,既然是從第12bit開始,那麼剩下的12bit的實體位址呢?這個是從虛拟位址中直接拿過來就可以了,不需要經過轉換,是以這部分我們成之為頁内位址.

<2> C0[2:0]C1[2:0]用于這個映射的實體頁是否經過cache以及經過cache的方式,寫穿(write through)還是寫回(write back),寫穿代表當要往這個位址寫入資料時,直接寫到RAM中(如果cache中存在同一個位址,也會寫到cache中),就如同”穿”過了cache一樣,這種效率較低,但是不存在資料的一緻性問題;而寫回代表當要往這個位址寫入資料時,寫入到cache中就”回去”了,而不再将資料寫入到RAM中,cache和RAM中資料的一緻性問題交由cache處理,這種方式存在效率較高,但是可能會引發一緻性問題,這種方式往往和write allocate(寫配置設定)配合使用,即當需要往位址寫資料時,如果這個位址在cache中不存在,那麼先把這個位址中的資料讀入到cache中,然後在往cache中寫入資料.對于實體頁的這個配置在寫使用者空間驅動時是至關重要的.

<3> D0和D1代表這個映射關系下的實體頁是否可寫,如果這個bit是0,那麼就表示是不可寫的,這時候會觸發TLB modified異常,什麼時候用這個異常呢?在核心中的寫時複制就用了這個機制,程序在fork時,會将父程序的頁表中的映射關系都标記為不可寫,然後複制到子程序,對應到這裡就是将D0或者D1标記為0,當父程序或者子程序在對這些标記為隻讀的頁進行寫操作時就會觸發這個TLB modified異常,在異常進行中配置設定新的實體頁将原來實體頁上的資料copy到這個新的實體頁上,并将新的實體頁和這個虛拟頁進行映射.

<4> V0,V1用于表示這個映射關系是否是有效的,如果是無效的,會觸發TLB invalid 異常,在我們調用mmap映射檔案時,malloc或者vmalloc配置設定記憶體,就用到了這個特性,當調用前面提到的接口配置設定記憶體或進行檔案映射時,隻是在程序的位址空間中配置設定了一片虛拟空間或者将檔案和這個虛拟空間進行了映射,但是開始并沒有直接配置設定實體頁和這些虛拟空間進行映射,這時候頁表中的映射關系還是invalid的,當通路這片虛拟空間時,在這裡發現V bit是0,也就是說這個虛拟到實體的映射關系是invalid,是以觸發了TLB invalid異常,這時候在kernel的異常處理函數中會配置設定新的實體頁和通路的虛拟頁進行映射,如果是檔案映射,還需要将檔案中的内容讀入到實體頁中.

下面看看MIPS32 R2中如何才能準确操作TLB.

1.4 操作TLB的方式

軟體控制硬體的方式需要操作的接口也就是寄存器以及指令,操作TLB也需要寄存器和指令.在MIPS32中控制cache\TLB等子產品是通過協處理器CP0,協處理器CP0對于MIPS處理器至關重要,不過我們這裡不先介紹.看看TLB相關的寄存器.

與TLB相關的寄存器有很多Index, Random, EntryLo0, EntryLo1, Context, PageMask, Wired, BadVAddr, EntryHi等,與TLB相關的指令有TLBP, TLBR, TLBWI, TLBWR. 下面分别介紹一下

先說下指令,

TLBR指令用于從TLB的所有entry中讀取出由index寄存器所列出的entry出來,讀出來的内容放到EntryLo和EntryHi以及Pagemask寄存器中.

TLBP指令用于從TLB的所有entry中找出和EntryHi寄存器比對的内容的entry出來,将這個entry的索引放到index寄存器中

TLBWI指令用于将EntryHi和EntryLo以及Pagemask中的内容寫入到index寄存器索引的TLB entry中

TLBWR指令用于将EntryHi和EntryLo以及Pagemask中的内容寫入到random寄存器索引的TLB entry中.

下面看看相關的寄存器介紹

Index Register (CP0 Register 0, Select 0)

結合Linux的應用場景看MIPS32架構之記憶體管理

圖9 Index寄存器

這個寄存器的index位用于TLB的索引,主要在TLBP,TLBR以及TLBWI指令時使用.

Random Register (CP0 Register 1, Select 0)

結合Linux的應用場景看MIPS32架構之記憶體管理

圖10 Random寄存器

該寄存器的random位用于記錄TLBWR指令時随機産生的TLB 的索引值

EntryLo0, EntryLo1 Register (CP0 Register 2, 3, Select 0)

結合Linux的應用場景看MIPS32架構之記憶體管理

圖11 EntryLo系列寄存器

該寄存器中的PFN用于表示實體頁幀,C表示對應的這個實體頁的cache屬性,D表示對應的這個實體頁的是否可寫,這裡dirty并不是真正的髒,而是可寫的含義,不要誤解.V表示該實體頁是否是有效的.G表示這個映射關系是否是全局的,也就是和程序空間無關的.

這個主要在将頁表用TLBWI或者TLBWR指令填寫到TLB中時所用.它會有指令TLBR所影響.

Context Register (CP0 Register 4, Select 0)

結合Linux的應用場景看MIPS32架構之記憶體管理

圖12 Context寄存器

該寄存器中的BadVPN2用于記錄在TLB異常發生時虛拟位址的31:13bit.

PageMask Register (CP0 Register 5, Select 0)

結合Linux的應用場景看MIPS32架構之記憶體管理

圖13 PageMask寄存器

該寄存器的Mask用于設定映射頁幀的大小,一般在Linux中我們使用的是4K的頁,當然也有些特殊情況.

Wired Register (CP0 Register 6, Select 0)

結合Linux的應用場景看MIPS32架構之記憶體管理

圖14 Wired寄存器

實際上該寄存器用于鎖定TLB entry的,在用TLBWR指令操作TLB時,2^wired以下的entry不會被替換掉.但是可以由TLBWI指令替換掉.

BadVAddr Register (CP0 Register 8, Select 0)

結合Linux的應用場景看MIPS32架構之記憶體管理

圖15 BadVAddr寄存器

由于記錄導緻TLB 異常或者位址錯誤(比如在user模式下通路高2G空間)異常的虛拟位址.

EntryHi Register (CP0 Register 10, Select 0)

結合Linux的應用場景看MIPS32架構之記憶體管理

圖16 EntryHi寄存器

在TLB的結構中已經介紹過VPN2和ASID,這裡不再贅述.

1.5 Kernel中的記憶體(頁表)管理和MIPS32 TLB異常

下面看看kernel中頁表的映射過程,MIPS32在kernel中使用了兩級頁表完成虛拟位址到實體位址的映射過程,

結合Linux的應用場景看MIPS32架構之記憶體管理

圖17 kernel的頁表

找一個虛拟位址對應實體位址中的資料的過程是這樣的,首先從程序的task_struct中拿到mm,mm的pgd成員就是程序的一級頁表所在頁幀,然後将虛拟位址的高10bit,也就是[31:22]拿出來作為pgd的索引,根據這個索引值得到二級頁表的所在的頁位址,再根據虛拟位址的21:12間的10bit在二級頁表中索引得到實體頁位址,然後根據虛拟位址的11:0的低12bit到實體頁中索引得到了該虛拟位址對應的值.這是kernel中在虛拟位址擷取值的過程,對于MIPS32的MMU來說,這裡虛拟位址的低12bit,11:0,同樣是頁内位址,這是一緻的,但是對于一級頁表,二級頁表,MIPS的MMU是不知道的,這點不像ARM那樣由硬體規定好了,是以在TLB refill異常時,需要軟體人員根據一級和二級頁表找到對應的實體頁,然後填入到TLB中,TLB refill異常處理的目标是把引起異常的位址所對應的實體頁填入到TLB中,而途徑是将EntryHi寄存器和EntryLo1 EntryLo0寄存器的内容通過TLBWR指令刷入到TLB中.這裡介紹一下詳細的過程,EntryHi寄存器的VPN2域由硬體自動填入觸發異常時的位址(的高19bit),ASID域是在程序切換時就填入的,是以EntryHi寄存器在TLB refill時是不用考慮的.而EntryLo寄存器的更新需要由軟體來完成,我們需要根據觸發異常時的位址擷取對應的實體位址并填入這個寄存器,如果以核心中用的是二級頁表的方式,首先擷取一級頁表的首地值(mm->pgd),然後在CP0的BadVaddr中擷取引發異常的虛拟位址,将虛拟位址的高10bit(31:22)作為索引得到二級頁表的虛拟位址,接下來通過CP0的Context寄存器擷取引發異常的BadVPN2,也就是虛拟位址的21:12位為索引找到實體頁的位址填入到EntryLo中,然後用TLBWR指令将EntryHi,EntryLo1,EntryLo0填入到TLB中。

下面看看某款MIPS32 R2單核處理器的TLB refill異常處理相關代碼

<1> 0x80000000:0x3c1b806a lui     k1, 0x806a

<2> 0x80000004:0x401a4000 mfc0    k0, c0_badvaddr

<3> 0x80000008:0x8f7b2990 lw      k1, 0x2990(k1)

<4> 0x8000000c:0x1ad582      srl     k0, k0, 22

<5> 0x80000010:0x1ad080      sll     k0, k0, 2

<6> 0x80000014:0x37ad821     addu    k1, k1, k0

<7> 0x80000018:0x401a2000    mfc0    k0, co_context

<8> 0x8000001c:0x8f7b0000    lw      k1, 0(k1)

<9> 0x80000020:0x1ad042      srl     k0, k0, 0x1 

<10> 0x80000024:0x335a0ff8    andi    k0, k0, 0xff8

<11> 0x80000028:0x37ad821     addu    k1, k1, k0

<12> 0x8000002c:0x8f7a0000    lw      k0, 0(k1)

<13> 0x80000030:0x8f7b0004    lw      k1, 4(k1)

<14> 0x80000034:0x1ad182       srl     k0, k0, 0x6 

<15> 0x80000038:0x409a1000    mtc0    k0, c0_entrylo0

<16> 0x8000003c:0x1bd982      srl     k1, k1, 0x6 

<17> 0x80000040:0x409b1800    mtc0    k1, c0_entrylo1

<18> 0x80000044:0x42000006   tlbwr 

<19> 0x80000048:0x0           nop

<20> 0x8000004c:0x42000018    eret

第一句和第三句實際為了擷取目前程序的pgd的位址放到k1寄存器中,在代碼中的實作是這樣的,

UASM_i_LA_mostly(p, k1, pgdc);

uasm_i_lw(p, ptr, uasm_rel_lo(pgdc), ptr);

這裡的看一下pgdc是怎麼擷取的.

pgdc的定義是這樣的long pgdc = (long)pgd_current;

而pgd_current的定義和指派如下

long pgdc = (long)pgd_current;

 34 #ifdef CONFIG_MIPS_PGD_C0_CONTEXT

 35 

 36 #define TLBMISS_HANDLER_SETUP_PGD(pgd)                                  \

 37 do {                                                                    \

 38         void (*tlbmiss_handler_setup_pgd)(unsigned long);               \

 39         extern u32 tlbmiss_handler_setup_pgd_array[16];                 \

 40                                                                         \

 41         tlbmiss_handler_setup_pgd =                                     \

 42                 (__typeof__(tlbmiss_handler_setup_pgd)) tlbmiss_handler_setup_pgd_array; \

 43         tlbmiss_handler_setup_pgd((unsigned long)(pgd));                \

 44 } while (0)

 45         

 46 #define TLBMISS_HANDLER_SETUP()                                         \

 47         do {                                                            \

 48                 TLBMISS_HANDLER_SETUP_PGD(swapper_pg_dir);              \

 49                 write_c0_xcontext((unsigned long) smp_processor_id() << 51); \

 50         } while (0)

 51 

 52 #else 

 53 

 54 

 59 extern unsigned long pgd_current[];

 60 

 61 #define TLBMISS_HANDLER_SETUP_PGD(pgd) \

 62         pgd_current[smp_processor_id()] = (unsigned long)(pgd)

 63 

 64 #ifdef CONFIG_32BIT

 65 #define TLBMISS_HANDLER_SETUP()                                         \

 66         write_c0_context((unsigned long) smp_processor_id() << 25);     \

 67         back_to_back_c0_hazard();                                       \

 68         TLBMISS_HANDLER_SETUP_PGD(swapper_pg_dir)

 69 #endif

 70 #ifdef CONFIG_64BIT

 71 #define TLBMISS_HANDLER_SETUP()                                         \

 72         write_c0_context((unsigned long) smp_processor_id() << 26);     \

 73         back_to_back_c0_hazard();                                       \

 74         TLBMISS_HANDLER_SETUP_PGD(swapper_pg_dir)

 75 #endif

 76 #endif 

這裡的例子是CONFIG_MIPS_PGD_C0_CONTEXT沒有配置的,是以會走else部分.

而在程序進行切換時,switch_mm處理時會調用TLBMISS_HANDLER_SETUP_PGD()這個宏,将要排程的程序的pgd指派到pgd_current.

接着分析TLB refill異常處理,第二句擷取引發TLB refill異常的虛拟位址到K0寄存器.

第四句,将K0寄存器的值向右移動22bit,得到虛拟位址的高10bit,也就是得到一級頁表的索引.

第五句,将K0寄存器的值向左移動2bit,這麼做的目的是為了将這個索引4位元組對齊,每一個二級頁表的位址在一級頁表中占用4位元組,是以我們需要對索引做4位元組對齊的處理.

第六句,擷取二級頁表所對應的索引在一級頁表中的位址,放到K1中.

第八句,擷取以K1為位址的值,并放到K1中,也就是擷取了二級頁表所在位址放到K1寄存器.

第七句,将CP0的Context寄存器放到K0寄存器中,這是為了擷取虛拟位址在二級頁表中的索引的第一步.可能有人會問,為什麼不再使用CP0的Badvaddr寄存器,這是為了節省一條指令,這再一次證明,核心的開發人員考慮的是多麼的細緻.

第九句,将K0寄存器中的值向右移動1bit,這是由于Context的[22:4]存放着虛拟位址的[31:13],不是[31:12],為了擷取這個虛拟位址對應的實體頁在二級頁表中的索引,根據圖17知道,我們需要擷取虛拟位址的[21:12],是以需要将Context寄存器右移動3bit,然後再移動2bit(因為這裡是[31:13],而不是[31:12]),是以整體來看就是右移動了1bit,如果在第七句中用了Badvaddr寄存器的話,那麼這裡至少需要先将這個值右移13bit,然後再向左移動3bit(因為需要将低3bit清0),才能得到,是以可以看出節省了一條指令.

第十句,通過與0xff8,這9個bit進行與操作,得到了虛拟位址在二級頁表中的真正的索引值.

第十一句,擷取虛拟位址對應在二級頁表中的位址放到K1中.

第十二句,将奇數實體頁位址放到K0寄存器中.

第十三句,将偶數實體頁位址放到K1寄存器中.前面已經介紹過了,在TLB中,一個VPN對應兩個實體頁.

第十四句,将K0向右移動6bit,這是由于在EntryLo0寄存器中,PFN[31:12]占用該寄存器的[25:6].

第十五句,是以将K0中的值寫入到CP0的EntryLo0寄存器中,由此可見,在二級頁表中,[31:12]存放了PFN,而[11:6]存放了C,D,V,G等EntryLo寄存器的相關位,而[5:0]存放的無關緊要.

第十六句,第十七句和第十四和十五句類似.

第十八句,通過tlbwr指令,将EntryHi,EntryLo0,EntryLo1随機寫入到TLB中.

第十九句,nop,空語句,應該是等待上一條指令完成

第二十句,傳回到觸發異常的位址處.注意eret這條指令的工作是,一條指令内清除SR(EXL)位,并同時将EPC中儲存的位址放到PC中,也就是傳回到那個位址開始取指.這裡它并沒有提到改變CPU的運作模式,是以資料上都會提到CPU在SR(EXL)=1(異常發生時)都會處于Kernel模式,而不管SR(KSU)為何值.這樣也就能保證在kenrel模式使用該異常.

上面的好幾條基于基址寄存器尋址的指令都是穿插進行,這是為了提高執行的效率,因為基址寄存器需要提前一個周期準備好,否則就需要等待一個周期.

對于上面代碼可能有人會有疑問,這段代碼中沒有儲存現場,那現場不會被破壞嗎?的确,這段代碼沒有保護現場,但是仔細看看,修改的寄存器隻有CP0的協處理器EntryLo1和EntryLo0,以及K0和K1,K0和K1這兩個寄存器比較特殊,C語言編寫的程式在編譯時是不會用到這兩個寄存器的,而在使用者态的彙編編寫代碼的中,也不會用這兩個寄存器,隻有在kernel态時,異常(包括中斷)發生後的儲存現場時才會用到這兩個寄存器,MIPS雖然不能不像ARM有那麼多運作模式,每種不同的模式都有自己獨有的一部分寄存器(MIPS隻有兩種運作模式,不同模式共用一套寄存器),但是MIPS照樣能夠想出其他辦法來解決好這個問題(多增加了兩個寄存器)。

在ARM和X86中,這部分是由硬體自動完成,而在MIPS中,這個是軟體參與完成的,由于這個異常發生的頻率非常高,是以為了能夠高效的處理這個異常,MIPS體系結構對于這個異常做了特殊的處理,它有一個獨立的入口點,不過這個入口點是可以配置的(關于這兩句話的具體含義我們放到異常和中斷中來解釋),一般在kernel中這個入口點設定為0x80000000,kernel為了優化這個異常的處理,隻有二十條指令,這段代碼是在啟動過程中生成的,而不是編譯出來的,這是為了防止編譯工具做編譯優化時反而降低了性能.

1.6 實際的TLB組織方式

上面提到了,TLB本身就是一種cache,對頁表的緩存。在硬體實作上,cache一般有三種方式,直接相連,全相連群組相連.這三種方式的結構在網上有很多資料可以參考,比如

http://blog.chinaunix.net/uid-26817832-id-3244916.html

這裡不在啰嗦。

由于TLB的特殊性,要求TLB命中率高,并且對于速度還要求非常快,為了滿足這樣的目标,就采用小容量的全相連cache的實作方式。MIPS32 R2的TLB的entry是多個,上面介紹的圖中列出了32個,如果不考慮頁表鎖存的因素,由于每一個頁映射關系可以放到這32個中的任意一個entry中,是以要通路一個虛拟位址時,需要将這個虛拟位址和TLB中的所有entry都比對一遍,這個工作相對比較費時的,是以又引出了下面的模型

結合Linux的應用場景看MIPS32架構之記憶體管理

圖22 MIPS32實際上的TLB關系

MIPS32 R2将TLB分為指令TLB(ITLB)和資料TLB(DTLB)以及JTLB,ITLB和DTLB分别隻有四個entry,當要通路的資料或者指令對應的位址不在DTLB或者ITLB中時,就會先到JTLB中去檢視是否有相比對的entry,如果有,那麼會把相應的entry copy到DTLB或者ITLB中,這樣能夠減少索引比對的時間,畢竟在4個裡面查找比在32個裡面查找快的多.

了解了MIPS32 R2的TLB相關的背景知識後,我們接着分析我們的例子,這是我們的主線.還是分析CPU從位址0x4005e0取指令的過程,如前面分析的,當TLB中沒有比對的位址映射關系時,這時候會觸發TLB refill異常,異常處理函數會将程序的ASID,以及引發該異常的虛拟位址和映射的實體位址填到TLB中.異常處理傳回後,繼續從這個虛拟位址對應的實體位址中取指令,這時候從TLB中能夠得到0x4005e0對應的實體位址,可是,這時候的實體位址一定是有效的嗎?如果是第一次從0x4005e0取指令,那麼這時候的實體位址是無效的,因為在每一個程序被fork時,它的pgd所在的頁都被做了初始化,初始化的值也就是pte所在的頁面全部集中到bss段中的一個頁面中,這樣擷取的實體位址就變成0,并且TLB data域中的v和d都為0,是以這時候讀取這個虛拟位址0x4005e0時,就會因為TLB 的data域中的V=0(映射關系是無效的)會觸發另外一種異常,TLB load異常,這個異常處理過程非常複雜,也就是我們常見的do_page_fault的處理.這個異常的處理的思路大概是首先儲存現場,根據虛拟位址找到适合的vma,如果找不到正确的,這是一個錯誤,如果是kernel模式,那麼就打出oops,如果是user模式,就發sigsegv信号給這個程序。如果找到了就配置設定實體記憶體,填寫頁表,如果是檔案映射,需要将對應偏移的檔案内容讀取到這個實體記憶體中;接下來把虛拟頁-實體頁的對應關系寫入tlb中,恢複現場.

由于現實中MIPS32 R2中有JTLB,DTLB,ITLB之分,是以上面的取值過程圖可以細化如下

結合Linux的應用場景看MIPS32架構之記憶體管理

圖23 MIPS32 取值過程圖

思考這樣一個問題,上面分析的是在user模式下,那麼TLB相關異常(TLB Refill 、Load)會不會在Kernel模式發生?如果會,那麼什麼情況下會發生?

實際上TLB的異常是不分user模式還是kernel模式的,都可能發生,甚至在中斷處理過程中都可以發生,不過中斷處理時我們一般不讓這樣的情況出現.比如在kernel模式下,用vmalloc配置設定的記憶體在讀寫時會觸發TLB異常,在用copy_from_user和copy_to_user時也可能會發生TLB異常.

下面看一副完整的通路位址的流程圖

結合Linux的應用場景看MIPS32架構之記憶體管理

圖24 TLB位址轉換流程圖

這裡詳細介紹一下這個流程.當CPU通路一個虛拟位址時,

第一步,ASID在程序切換時就已經儲存在了EntryHi寄存器中,而VPN是虛拟位址的[31:13],進入第二步;

第二步,判斷目前CPU運作的模式,如果是user模式,則進入第三步,如果是kernel模式,則進入第四步;

第三步,判斷這個虛拟位址是否是屬于user空間,如果屬于則進入第六步,否則進入第五步.

第四步,判斷這個虛拟位址是屬于kseg0或者kseg1位址區域,如果屬于kseg0/kseg1,那麼這個位址是需要以unmap方式通路,否則進入第六步;

第五步,既然在user模式下通路了非user空間的位址,那麼這是一種非法操作,會觸發address error異常,進入相應的異常處理程式.

第六步,對比虛拟位址對應的VPN和TLB中的VPN是否比對,如果不比對,說明TLB中沒有緩存相應的位址轉換關系,則進入TLB refill異常.否則進入第七步.

第七步,檢視TLB中相比對的entry中的G位是否為1,如果G不是1,那麼表明這個比對項不是全局的,那麼進入第八步,如果G是1那麼就表明這是個全局的比對項,和程序無關,進入第九步;

第八步,判斷EntryHi中的ASID和TLB中的ASID是否比對,如果比對,則進入第九步,否則說明在TLB中沒有和目前虛拟位址比對的entry,需要對TLB進行refill,是以進入TLB refill異常.

第九步,判斷TLB比對項中的V位是否為1,如果V=1,那麼表明目前TLB的這個entry是有效的,進入第十步,如果V=0,那麼表明這個entry是無效的,觸發TLB invalid異常.

第十步,判斷TLB的entry中的D位是否為1,如果D=0,表明這個虛拟位址對應的實體頁是隻讀的,不可寫,進入第十一步;如果D=1,表明這個虛拟位址對應的實體頁是可讀寫的,進入第十二步,

第十一步,判斷目前的操作是否為write,如果是write操作,那麼會觸發TLB modified異常.如果是read操作,那麼進入第十二步.

第十二步,判斷TLB對應entry中的C域是否為二進制的010或者111,如果是,表明目前位址是不經過cache的,直接通路實體記憶體,否則這個位址是經過cache的,通路相應的cache即可.

1.7 總結與問題

這樣我們就把MIPS32的記憶體管理相關的知識介紹完了,請回顧一下幾個問題

1) MIPS32 有幾種運作模式,每種運作模式下都是運作了哪些程式,與其相關的MIPS32的記憶體映射圖是怎樣的?

2) MIPS32的MMU TLB結構是怎樣的?

3) MIPS32的TLB操作接口和指令有哪些,功能是什麼?

4) MIPS32的TLB相關異常處理(refill、invalid,modified)的思路是怎樣的?

接下來的文章分析MIPS32的cache結構。

繼續閱讀