摘要 Linux內核的設計要考慮到在各種不同的微處理器上的實現,還有考慮到在64位的微處理器(如Alpha)上的實現 四、內存管理 1、基本框架 Linux內核的設計要考慮到在各種不同的微處理器上的實現,還有考慮到在64位的微處理器(如Alpha)上的實現,所以不能僅僅針對i386結構來設計它的映射機制,而要以只要假象的、虛擬的微處理器和MMU(內存管理單元)為基礎,設計出一種通用的模式,再把它分別落實到具體的微處理器上。因此,Linux內核的映射機制被設計成三層,在頁面目錄和頁表之間增設了一層“中間目錄”。在代碼中,頁面目錄稱為PGD,中間目錄稱為PMD,而頁表稱為PT。PT的表項稱為PTE。PGD,PMD,PT均為數組,相應的,在邏輯上也把線性地址從高到低分為4各位段,個占若干位,分別用作目錄PGD的下標、中間目錄PMD的下標、頁表中的下標和物理頁面內的位移。 就i386微處理器來說,CPU實際上不是按三層而是按兩層的模型來進行地址映射,這就需要將虛擬的三層映射落實到具體的兩層的映射,跳過中間的PMD層次。 2、地址映射的全過程 i386微處理器一律對程序中的地址先進行段式映射,然後才能進行頁式映射。而Linux所采用的方法實際上使段式映射的過程中不起什麼作用。 下面通過一個簡單的程序來看看Linux下的地址映射的全過程: #include greeting() { printf(“Hello world! ”); } main() { greeing(); } 該程序在主函數中調用greeting 來顯示“Hello world!”,經過編譯和反匯編,我們得到了它的反匯編的結果。 08048568: 8048568: 55 push1 %ebp 8048856b:89 e5 mov1 %esp,%ebp 804856b: 68 04 94 04 08 push1 $0x8048404 8048570: e8 ff fe ff ff call 8048474 8048575: 83 c4 04 add1 $0x4,%esp 8048578: c9 leave 8048579: c3 ret 804857a: 89 f6 mov1 %esi,%esi 0804857c : 804857c: 55 push1 %ebp 804857d: 89 e5 mov1 %esp,%ebp 804857f: e8 e4 ff ff ff call 8048568 8048584: c9 leave 8048585: c3 ret 8048586: 90 nop 8048587: 90 nop 從上面可以看出,greeting()的地址為0x8048568。在elf格式的可執行代碼中,總是在0x8000000開始安排程序的“代碼段”,對每個程序都是這樣。 當程序在main中執行到了“call 8048568”這條指令,要轉移到虛擬地址8048568去。 首先是段式映射階段。地址8048568是一個程序的入口,更重要的是在執行的過程中有CPU的EIP所指向的,所以在代碼段中。I386cpu使用CS的當前值作為段式映射的選擇子。 內核在建立一個進程時都要將其段寄存器設置好,把DS、ES、SS都設置成_USER_DS,而把CS設置成_USER_CS,這也就是說,在Linux內核中堆棧段和代碼段是不分的。 Index TI DPL #define_KERNEL_CS 0x10 0000 0000 0001 0000 #define_KERNEL_DS 0x18 0000 0000 0001 1000 #define_USER_CS 0x23 0000 0000 0010 0011 #define_USER_DS 0x2B 0000 0000 0010 1011 _KERNEL_CS: index=2,TI=0,DPL=0 _KERNEL_DS: index=3,TI=0,DPL=0 _USERL_CS: index=4,TI=0,DPL=3 _USERL_DS: index=5,TI=0,DPL=3 TI全都是0,都使用全局描述表。內核的DPL都為0,最高級別;用戶的DPL都是3,最低級別。_USER_CS在GDT表中是第4項,初始化GDT內容的代碼如下: ENTRY(gdt-table) .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x0000000000000000 /* not used */ .quad 0x00cf9a00000ffff /* 0x10 kernel 4GB code at 0x00000000 */ .quad 0x00cf9200000ffff /* 0x18 kernel 4GB data at 0x00000000 */ .quad 0x00cffa00000ffff /* 0x23 user 4GB code at 0x00000000 */ .quad 0x00cff200000ffff /* 0x2b user 4GB data at 0x00000000 */ GDT 表中第一、二項不用,第三至第五項共四項對應於前面的四個段寄存器的數值。 將這四個段描述項的內容展開: K_CS: 0000 0000 1100 1111 1001 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 K_DS: 0000 0000 1100 1111 1001 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 U_CS: 0000 0000 1100 1111 11111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 U_DS: 0000 0000 1100 1111 1111 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 這四個段描述項的下列內容都是相同的。 ·BO-B15/B16-B31 都是0 基地址全為0 ·LO-L15、L16-L19都是1 段的界限全是0xfffff ·G位都是1 段長均為4KB ·D位都是1 32位指令 ·P位都是1 四個段都在內存中 不同之處在於權限級別不同,內核的為0級,用戶的為3級。 由此可知,每個段都是從地址0開始的整個4GB地虛存空間,虛地址到線性地址的映射保持原值不變。 再回到greeting 的程序中來,通過段式映射把地址8048568映射到自身,得到了線性地址。 每個進程都有自身的頁目錄PGD,每當調度一個進程進入運行時,內核都要為即將運行的進程設置好控制寄存器CR3,而MMU硬件總是從CR3中取得當前進程的頁目錄指針。 當程序要轉到地址0x8048568去的時候,進程正在運行中,CR3已經設置好了,指向本進程的頁目錄了。 8048568: 0000 1000 0000 0100 1000 0101 0110 1000 按照線性地址的格式,最高10位 0000100000,十進制的32,就以下標32去頁目錄表中找其頁目錄項。這個頁目錄項的高20位後面添上12個0就得到該頁面表的指針。找到頁表後,再看線性地址的中間10位001001000,十進制的72。就以72為下標在找到的頁表中找到相應的表項。頁面表項重的高20位後添上12個0就得到了物理內存頁面的基地址。線性地址的底12位和得到的物理頁面的基地址相加就得到要訪問的物理地址。 3 地址映射的效率分析 在頁式映射的過程中,CPU要訪問內存三次,第一次是頁面目錄,第二次是頁面表,第三次才是真正要訪問的目標。這樣,把原來不用分頁機制一次訪問內存就能得到的目標,變為三次訪問內存才能得到,明顯執行分頁機制在效率上的犧牲太大了。 為了減少這種開銷,最近被執行過的地址轉換結果會被保留在MMU的轉換後備緩存(TLB)中。雖然在第一次用到具體的頁面目錄和頁面表時要到內存中讀取,但一旦裝入了TLB中,就不需要再到內存中去讀取了,而且這些都是由硬件完成的,因此速度很快。 TLB對應權限大於0級的程序來說是不可見的,只有處於系統0層的程序才能對其進行操作。 當CR3的內容變化時,TLB中的所有內容會被自動變為無效。Linux中的_flush_tlb宏就是利用這點工作的。_flush_tlb只是兩條匯編指令,把CR3的值保存在臨時變量tmpreg裡,然後立刻把tmpreg的值拷貝回CR3,這樣就將TLB中的全部內容置為無效。除了無效所有的TLB中的內容,還能有選擇的無效TLB中某條記錄,這就要用到INVLPG指令。 五、進程管理 1.I386硬件任務切換機制 Intel 在i386體系的設計中考慮到了進程的管理和調度,並從硬件上支持任務間的切換。為此目的,Intel在i386系統結構中增設了一種新的段“任務狀態段”TSS。一個TSS雖然說像代碼段,數據段等一樣,也是一個段,實際上卻是一個104字節的數據結構,用以記錄一個任務的關鍵性的狀態信息。 像其他段一樣,TSS也要在段描述表中有個表項。不過TSS只能在GDT中,而不能放在任何一個LDT中或IDT中。若通過一個段選擇項訪問一個TSS,而選擇項中的TI位為1,就會產生一次GP異常。 另外,CPU中還增設一個任務寄存器TR,指向當前任務的TSS。相應地,還增加了一條指令LTR,對TR寄存器進行裝入操作。像CS和DS一樣,TR也有一個程序不可見部分,每當將一個段選擇碼裝入到TR中時,CPU就會自動找到所選擇的TSS描述項並將其裝入到TR的程序不可見部分,以加速以後對該TSS段的訪問。 還有,在IDT表中,除了中斷門、陷阱門和調用門以為,還定義了一種任務門。任務門中包含一個TSS段選擇碼。當CPU因中斷而穿過一個任務門時,就會將任務門中的選擇碼自動裝入TR,使TR指向新的TSS,並完成任務的切換。CPU還可以通過JMP和CALL指令實現任務切換,當跳轉或調用的目標段實際上指向GDT表中的一個TSS描述項時,就會引起一次任務切換。 2. Linux的任務切換和現場保護 Intel 關於任務切換的設計十分的周到,而且提供了十分簡潔的任務切換機制。但是,Linux並不采用i386硬件提供的任務切換機制。 Linux之所以這樣做,很大程度是從效率的角度考慮。有CPU自動完成的這種任務切換並不是只相當於一條指令。實際上,i386中通過JMP指令或CALL指令完成任務切換的過程是一個相當復雜的過程,其執行過程長達300多個CPU時鐘周期。在執行過程,CPU實際上做了所有需要做的事,而其中有的事在一定條件下