Linux核心筆記002 - i386 的頁式記憶體管理機制

jmpcall發表於2020-05-13
1. 虛擬地址
    只要不是實際記憶體的地址,都是虛擬地址,通常就是指邏輯地址,邏輯地址是指指令中的地址,比如寫一個下面這樣的程式:
#include <stdio.h>

static char ch = 'A';

int main()
{
    printf("%p, %c\n", &ch, ch);
    return 0;
}
    執行"gcc test.c -g -Wall",然後執行"objdump -S a.out",檢視main()函式的反編譯結果:
int main()
{
 80483e4:	55                   	push   %ebp
 80483e5:	89 e5                	mov    %esp,%ebp
 80483e7:	83 e4 f0             	and    $0xfffffff0,%esp
 80483ea:	83 ec 10             	sub    $0x10,%esp
	printf("%p, %c\n", &ch, ch);
 80483ed:	0f b6 05 14 a0 04 08 	movzbl 0x804a014,%eax    ; 將地址0x804a014處的內容,即'A',擴充套件成32位的無符號數0x00000041,儲存到eax
 80483f4:	0f be d0             	movsbl %al,%edx    ; 將eax最低位元組即'A',當作有符號數,擴充套件成32位,仍然是0x00000041,儲存到edx
 80483f7:	b8 f0 84 04 08       	mov    $0x80484f0,%eax    ;將格式字串"%p, %c\n"地址,儲存到eax
 80483fc:	89 54 24 08          	mov    %edx,0x8(%esp)    ; printf()實參:ch
 8048400:	c7 44 24 04 14 a0 04 	movl   $0x804a014,0x4(%esp)    ; printf()實參:&ch
 8048407:	08 
 8048408:	89 04 24             	mov    %eax,(%esp)    ; printf()實參:"%p, %c\n"
 804840b:	e8 f0 fe ff ff       	call   8048300 <printf@plt>
	return 0;
 8048410:	b8 00 00 00 00       	mov    $0x0,%eax
}
 8048415:	c9                   	leave  
 8048416:	c3                   	ret
    其中,指令"0f b6 05 14 a0 04 08",就包含著ch的地址"0x0804a014",並且在執行的時候,會讀取這個地址處的內容。
    程式中的"0x0804a014"這個地址,就是邏輯地址,它本質上只是用於描述一段邏輯的標號,表示這段程式執行時,需要一個1位元組的實際記憶體,並且用"0x0804a014"這個值標記它,就相當於上數學課時用的xyz,只不過除了xyz,數學中還可以用α、β、δ、φ、ξ、..等等標號,而二程式程式中,只用數字(指標/地址)作為標號,32位系統中,用32位的數字,64位系統中,用64位的數字。
    Linux核心筆記001中已經學習過X86 CPU的段式管理設計,它實際是提供給軟體,透過一種合理的安排,讓每個程式都能在自己專屬的段上使用記憶體,那麼即使兩個不同的程式使用相同的邏輯地址,也會對應到不同的實際記憶體單元,從而避免衝突。這麼看來,前面介紹的越權、越界檢查,都是手段,讓每個程式使用獨立的實際記憶體是才是目的,接著再把這個任務集中實現在核心層,就解放了應用程式對"記憶體衝突"的考慮,這正是保護模式的核心思想。
    邏輯地址的集合,可以叫作"虛擬(記憶體)空間",那麼拿32位系統來說,還需要理解2個東西:

  • 每個程式都有4G虛擬空間

        虛擬空間包含的是邏輯地址,只是用於描述程式邏輯的標號,由於指標型別的大小為32位,所以每個程式都有4G個標號可以使用,它跟實際記憶體有多大毫無關係。
  • 虛擬空間也是資源
         每個程式有4G虛擬空間的同時,也意味著只有4G虛擬空間,比如4G個標號都被無意義的物件佔用著,有意義的物件就沒有標號去對應了。

2. 實體地址

    實體地址很容易理解,就是指實際記憶體的地址。實體地址的集合,也相應的叫作"物理空間"。
    另外,有一組容易跟"虛擬空間"/"物理空間"混淆的概念:"虛擬記憶體"/"實際記憶體"。虛擬記憶體是指交換分割槽,即把硬碟當作記憶體使用(書2.6~2.9節)。

3. 什麼是核心

    上面提到,在核心層集中實現"每個程式使用獨立實際記憶體"的邏輯。那麼,什麼是核心?
    Linux核心筆記001中介紹過CPU的兩種硬體狀態:真實模式、保護模式。其實,在保護模式狀態下,CPU又存在兩種硬體狀態的區分:核心態、使用者態。主要是透過段暫存器中的RPL區分,核心態可以執行特權指令,使用者態不可以執行特權指令。有了硬體特性的支援,再加上電腦開機的第一條指令,又是從核心程式碼開始執行,那麼核心的實現只要保證:在跳轉到應用程式程式碼的同時,將CPU狀態設定為使用者態,使用者態向切換回核心態的同時,指令也必須跳轉回核心的程式碼區,就可以將權力掌握在自己手中。具體的實現,涉及到"門"的概念,以有CPL、DPL、CPL複雜的對比邏輯(後期會學習到)。
    我自己在剛開始學習的時候,對"核心"的理解,一直存在一個誤區:Linux核心是一個有高許可權的程式。
    其實這就涉及到"微核心"/"單核心"的區別:
  • 微核心
        微核心可以理解成一個獨掌大權的獨立的程式,透過程式間通訊的方式,為使用者程式服務,核心的程式碼也只有"核心程式"可以執行。
  • 單核心
        Linux核心屬於單核心,單核心可以理解成,操作所有程式公共記憶體的一套介面(比如系統呼叫、異常處理函式),每個使用者程式切換到核心態,執行核心的程式碼。
    上面不是剛說過,每個程式都會使用自己獨立的實體記憶體嘛,再加上按照應用程式開發的經驗,在同一個程式中建立的不同執行緒,才可以共享全域性的記憶體,不同程式共享資源,不是要透過程式間通訊的方式嘛,那麼,Linux核心操作的所有程式公共記憶體是什麼?
    理解這個問題之前,我們先要承認,所有程式是需要有公共記憶體的,否則自己佔用了哪個部分的物理空間,如果只有自己看得見,那麼別的程式進入核心態建立新程式時,怎麼知道還有哪些物理空間可以用呢?
    然後再理解,所有程式公共記憶體是什麼的問題:
    筆記第1節,已經介紹了"虛擬空間"的概念,Linux核心的設計,將4G虛擬空間,劃分成了核心空間(3G-4G)使用者空間(0-3G),既然是對虛擬空間的劃分,那麼就仍然是程式用於描述邏輯的標號,最終需要透過一個對映表,才能對應到實際的實體記憶體地址,那麼,核心的實現只要保證:3G-4G範圍的虛擬地址,使用一張"公共"的對映表,0-3G範圍的虛擬地址,為每個程式獨立建立一個對映表,即可。
    可能有些人會在這裡犯迷惑:問題的起點就是怎麼讓不同的程式,有公共的記憶體,而解決方法卻要用到一張"公共"的對映表,不是矛盾了嗎?
    產生這種問題,是因為沒有抓住"開機後核心程式碼優先執行"這一點,開機後,是核心程式碼一步步從真實模式切換到保護模式、建立了一些公共資源、建立了init程式,init程式又建立了更多的程式,才有的程式切換。

4. i386頁式記憶體管理

    Linux核心筆記001中,把X86 CPU段式記憶體管理的電路邏輯,按照軟體思維理解成一個"API",相應的,段暫存器、GDTR/LDTR暫存器,就相當於是"引數"。同樣的,頁式記憶體管理相當於另外一個"API",它的"引數"是:CR0暫存器(最高的PG位是頁式對映的開關)、CR3暫存器(指向目錄表)、指令中的邏輯地址(頁偏移)。
    由於要i386進入頁式記憶體對映的邏輯,它必須先要完成段式記憶體對映,所以段式管理的"引數"也要設定正確。另外,由於同樣的原因,指令中的邏輯地址,要經過2次對映,才能得到實體地址,為了方便整個過程的描述,就將第一次對映得到的結果稱為"線性地址",由於它也不是實體地址,從而也屬於虛擬地址,這就是為什麼一查資料,會同時出現"虛擬地址"、"邏輯地址"、"線性地址"、"實體地址"這麼多概念的原因。
  • 線性地址含義設計
    Linux核心筆記002 - i386 的頁式記憶體管理機制
  • i386 CPU頁式對映過程
Linux核心筆記002 - i386 的頁式記憶體管理機制
  • 目錄項含義設計
  Linux核心筆記002 - i386 的頁式記憶體管理機制
        頁表項與目錄項每個位域的作用,稍有不同,書上已經介紹的很明白了,另外整個過程沒有什麼理解難度,就不過多筆記了。
  • 為什麼要按目錄和頁分成兩級?
        這個設計是為了節省頁表佔用的記憶體,隨著程式佔用記憶體的增加,可以一頁一頁的增加頁表,否則剛建立程式時,就要分配一個完整的4M頁表,而系統中的程式,絕大部分只需要很小的記憶體,相應的,就只能利用到4M頁表中的很小一部分,造成浪費。不過隨著生產技術的進步,記憶體容量已經越來越大,少一步對映,相比浪費點記憶體,更好。

相關文章