Linux核心筆記004 - 從記憶體管理開始,認識Linux核心
jmpcall發表於2020-05-28
1. 系統初始化
Linux核心筆記001、Linux核心筆記002、Linux核心筆記003,對應的是《Linux核心原始碼情景分析》第一章內容,在進入第二章學習之前,本篇筆記先跳躍到第10.1節——系統初始化(第一階段):
- 真實模式
記憶體是揮發性的儲存介質,斷電後,資料就沒有了,相應的,開機時,是透過"燒"在不揮發儲存介質上的初始載入程式(比如BIOS程式),將核心程式從磁碟引導區讀入記憶體的。在核心被載入到記憶體後,指令就會從BIOS跳轉到核心程式碼開始執行,這時CPU還是真實模式狀態,核心程式碼做好一些基礎準備後,再透過設定相關暫存器,將CPU切換到保護模式狀態。
這個過程後續會詳細學習,本篇筆記暫不記錄,目前只需要知道一個gdt_table變數:
ENTRY(gdt_table) .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x0000000000000000 /* not used */ .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */ .quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */ .quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */ .quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */ .quad 0x0000000000000000 /* not used */ .quad 0x0000000000000000 /* not used */ /* * The APM segments have byte granularity and their bases * and limits are set at run time. */ .quad 0x0040920000000000 /* 0x40 APM set up for bad BIOS's */ .quad 0x00409a0000000000 /* 0x48 APM CS code */ .quad 0x00009a0000000000 /* 0x50 APM CS 16 code (16 bit) */ .quad 0x0040920000000000 /* 0x58 APM DS data */ .fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */
以上程式碼,就是核心對這個變數的初始化內容,編譯後,這個內容會存入核心映象檔案的資料段,跟映象檔案的指令段內容一樣,也會在開機時,被載入到記憶體的指定位置。書中1.2節,介紹過GDTR/LDTR暫存器,透過gdt_table變數名,應該已經可以猜出,它就是GDTR指向的全域性描述符表,用於CPU切換到保護模式時進行段式定址。
另外,在真實模式階段,核心會將CS暫存器設定為__KERNEL_CS,即0x10(二進位制:10|0|00),按照保護模式下段暫存器的含義,即:index=2、TI=0、RPL=0。
"TI=0"表示使用全域性描述符表,即gdt_table,則"index=2"索引到的描述符為0x00cf9a000000ffff,轉換為二進位制:
0000 0000 1100 1111 1001 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
再結合段描述符的結構定義,得到:
① B0-B15、B16-B31都為0,即段基址為0
② L0-L15、L16-L19都為1,G位為1,即段長度為4G
也就是說,CS暫存器"指向"的段,是從0地址開始,4G長度的整個記憶體。其實,"3、4、5"索引處的描述符,同樣是這種情況:"3"與"2"的區別,僅在於type欄位,分別表示程式碼段、資料段,用於賦值給CS和DS、ES、SS;"4、5"與"2、3"相比,又僅僅是RPL欄位有區別,表示許可權級別分別為0、3。
這是因為,Linux核心緊接著就會跳轉到startup_32程式碼處,開啟CPU的頁式管理功能,它根本沒打算使用段式對映的方式,進行記憶體管理,只是由於80386定址邏輯,總是會先經過段式對映過程,Linux核心這樣設定,一方面保證不會遇到CPU內部的檢查錯誤(越權、越界),另一方面,對映後邏輯地址也能保持不變,在軟體層,對於後續的頁式對映過程,讓段式對映過程變得"透明"了;
- 保護模式
跳轉到startup_32時,CPU已經切換到保護模式狀態了,同進入保護模式前,要設定好段暫存器、段描述符表的道理一樣,在開啟頁式對映功能前,也要準備好一定量的目錄表、頁面表內容。
我當初就有過一個疑問:開啟頁式管理時訪問記憶體,要事先由分配函式建立了對映關係才行,那麼剛開啟頁式管理時,分配函式本身需要訪問的記憶體,又是什麼時候建立對映關係的呢?
其實,這裡就是源頭。可以理解成,頁式管理開啟前,指令中包含什麼地址,實際訪問到的也是這個地址,沒有"分配"的概念,頁式管理開啟後,指令中直接包含的地址,都要利用"分配"操作事先建立的頁式對映關係,才能得到實體地址。這裡就相當於,在為開啟頁式對映後緊接著的一些操作"分配"記憶體,跟應用程式開發中,執行一個演算法前,先呼叫malloc()分配一塊記憶體,是一樣的道理。
/* * swapper_pg_dir is the main page directory, address 0x00101000 * * On entry, %esi points to the real-mode code as a 32-bit pointer. */ /* * 引導過程更之前的階段,會將startup_32程式碼片段,複製到0x100000實體地址處,並且跳轉語句為"ljmp 0x100000",而不是"ljmp startup_32": * 如果在程式裡,將跳轉語句寫成"ljmp startup_32",編譯後會生成形似"ff 2d XX XX XX CX"的二進位制指令,"XXXXXXX"部分,表示startup_32指令塊在二進位制檔案中的偏移,它受整個程式中定義變數的多少,以及其它函式的情況影響,增刪一個函式,或者在某個函式增刪一行程式碼,都有可能會影響startup_32的位置,另外,為了保證核心基本邏輯,訪問的都是核心空間,核心程式中所有符號的地址,都會加上鍊接指令碼中指定的偏移0xC0000000,從而最終生成到跳轉指令中的地址為0xCXXXXXXX * 而此時已經是保護模式,再加上Linux核心的設計,使得段式對映前後的地址保持不變,所以"ljmp startup_32"指令就會跳轉到真實記憶體的0xCXXXXXXX處,而不是startup_32真正所在的0x100000處 */ ENTRY(stext) ENTRY(_stext) startup_32: /* * Set segments to known values */ /* * DS、ES、FS、GS都設定為__KERNEL_DS * Linux核心真正希望使用的只有頁式管理,但由於80386硬體設計的原因,進入頁式對映前,要保證段式對映也能順利完成 */ cld ; DF標誌位清0 movl $(__KERNEL_DS),%eax movl %eax,%ds movl %eax,%es movl %eax,%fs movl %eax,%gs #ifdef CONFIG_SMP /* * BX在本段程式碼中,表示"是否為次cpu" * bx與自己相或,結果作為跳轉條件,如果bx為0,即主cpu執行到這,會跳轉到緊接著的第一個"1"標號處 */ orw %bx,%bx jz 1f /* * New page tables may be in 4Mbyte page mode and may * be using the global pages. * * NOTE! If we are on a 486 we may have no cr4 at all! * So we do not try to touch it unless we really have * some bits in it to set. This won't work if the BSP * implements cr4 but this AP does not -- very unlikely * but be warned! The same applies to the pse feature * if not equally supported. --macro * * NOTE! We have to correct for the fact that we're * not yet offset PAGE_OFFSET.. */ /* * 次cpu進入startup_32前,bx被設定為1,執行這段程式碼,最關鍵的邏輯是,直接跳轉到"3"標號處,使用主cpu設定好的頁表,自己不再設定 */ #define cr4_bits mmu_cr4_features-__PAGE_OFFSET cmpl $0,cr4_bits je 3f /* 如果支援PSE/PAE,設定CR4暫存器 */ movl %cr4,%eax # Turn on paging options (PSE,PAE,..) orl cr4_bits,%eax movl %eax,%cr4 jmp 3f 1: #endif /* * Initialize page tables */ /* * 彙編程式碼中,可以透過.org讓編譯器將變數安排在指定偏移處,比如pg0、empty_zero_page,分別被安排在二進位制檔案中的0x2000、0x4000處,加上鍊接指令碼指定的偏移,編譯地址分別為0xC0002000、0xC0004000 * "Linux核心筆記002"已經說明過,0-3G範圍的虛擬地址,在不同程式中,會對映到不同的實體地址,而3G-4G的虛擬地址為核心空間,對映關係不能因程式不同而不同,否則就跟使用者空間一樣屬於各個程式的私有空間了,從而,對於3G-3G+896MB範圍的所有虛擬地址,都是按照"減0xC0000000偏移"的規則,建立一個固定的對映關係(剩餘128MB核心空間屬於高階記憶體,後期學習) * 那麼,對於虛擬地址0xC0002000、0xC0004000,經過對映後,對應的實體地址就應該分別為0x2000、0x4000,然而程式碼執行到此處,還沒開啟頁式管理,也還沒有建立這樣的對映,所以透過指令本身減掉了0xC0000000(__PAGE_OFFSET)偏移,它跟開啟頁式管理後,cpu透過對映找到的實體地址是一樣的 * "1"、"2"標號處程式碼結合一起,正是用於建立0xC0000000-0xC0800000這部分核心空間(開頭8MB)的對映,從0x2000開始,依次寫入頁表項0x007、0x1007、0x2007..,直到0x4000處結束,從而建立了2個PT(頁表),後續再指定好目錄表,並設定好目錄項,開啟項式管理後,就可以正常訪問0xC0000000-0xC0800000範圍內的虛擬地址了 */ movl $pg0-__PAGE_OFFSET,%edi /* initialize page tables */ movl $007,%eax /* "007" doesn't mean with right to kill, but PRESENT+RW+USER */ 2: stosl ; 將EAX值,複製到ES:DI,此時為保護模式狀態,擴充套件段為從0開始的整個4G空間,所以ES:DI從0x2000開始 add $0x1000,%eax ; 0x007、0x1007、0x2007 .. cmp $empty_zero_page-__PAGE_OFFSET,%edi jne 2b /* * Enable paging */ /* * swapper_pg_dir由.org指定偏移為0x1000,程式將它的地址設定到CR3暫存器中,即用它指向的一頁內容,作為目錄表 * "80386硬體API"會把CR3"引數"的值,直接當作實體地址,由於此時還沒有開啟頁式管理,仍然需要指令本身從編譯生成的虛擬地址中,減掉__PAGE_OFFSET偏移 * 然後,將CR0最高位(PG標誌位)設定為1,開啟頁式管理 */ 3: movl $swapper_pg_dir-__PAGE_OFFSET,%eax movl %eax,%cr3 /* set the page table pointer.. */ movl %cr0,%eax orl $0x80000000,%eax movl %eax,%cr0 /* ..and set paging (PG) bit */ /* * 以下這條跳轉指令,用於丟棄已經在"cpu的取指令流水線"中的內容(Intel在i386技術資料中的建議) * 另外,每執行一條指令,IP暫存器的值,就會加上這條指令的長度,指向下一條指令,到目前為止,IP暫存器都是在0x100000的基礎上加,接下來這條指令的地址大概為0x100XXX,由於上一條指令已經開啟頁式對映,所以這個地址也需要有對映關係,cpu才能得到它的實體地址,其實,稍後就可以看到,目錄表最開始2項,也有初始值,也指向上面建立的2個頁表,從而使得0-8MB虛擬地址也有對映關係,並且可以保持對映前後的值不變,目的就是用於這種過渡期 */ jmp 1f /* flush the prefetch-queue */ 1: /* * 編譯後,"1f"標號的地址為0xCXXXXXXX,按照如下指令跳轉一下,IP暫存器的值就是核心空間的地址了,就不用依賴目錄表中最開始的2個目錄項了,另外,引用程式中的變數,也不用依賴指令本身減掉__PAGE_OFFSET,從而完全過渡到頁式管理 */ movl $1f,%eax jmp *%eax /* make sure eip is relocated */ 1: /* Set up the stack pointer */ lss stack_start,%esp
pg0、pg1、empty_zero_page位置安排:
/* * The page tables are initialized to only 8MB here - the final page * tables are set up later depending on memory size. */ .org 0x2000 ENTRY(pg0) .org 0x3000 ENTRY(pg1) /* * empty_zero_page must immediately follow the page tables ! (The * initialization loop counts until empty_zero_page) */ .org 0x4000 ENTRY(empty_zero_page)
目錄表初始化內容:
/* * This is initialized to create an identity-mapping at 0-8M (for bootup * purposes) and another mapping of the 0-8M area at virtual address * PAGE_OFFSET. */ .org 0x1000 ENTRY(swapper_pg_dir) /* 用於0-8MB虛擬地址對映(屬於使用者空間,與核心空間開頭8MB對映到相同的實體地址,臨時用於過渡,最終會被撤消) */ .long 0x00102007 .long 0x00103007 /* 接下來766個目錄項初始化為0 */ .fill BOOT_USER_PGD_PTRS-2,4,0 /* default: 766 entries */ /* 用於 0xC0000000 - 0xC0000000+8MB 核心地址對映 */ .long 0x00102007 .long 0x00103007 /* 接下來254個目錄項初始化為0 */ /* default: 254 entries */ .fill BOOT_KERNEL_PGD_PTRS-2,4,0
2. Linux記憶體管理的基本框架
透過Linux核心對待80386段式管理特性的方式,很容易可以理解,軟體可以根據自己的需要,選擇性的使用硬體特性,比如剪刀一般是用於剪東西,但有些人也會用它松螺絲。換句話說,只要最終能將"硬體API"的"引數"設定正確,保證硬體內部不出錯,將"引數"設定成什麼,以及如何"設定",都由軟體自己決定。
對於頁式管理特性的利用,Linux核心的做法如下:
- 雖然看起來有些"特別"
線性地址到實體地址的對映,80386的硬體邏輯是:線性地址前10位作為索引,到目錄表中找到目錄項,從而找到一張頁表;再根據接著的10位,到頁表中找到而表項,從而找到目標頁;根據最後12位的偏移值,最終在目標頁中對映到一個實體地址。
那麼,Linux核心在建立對映關係時,也應該將線性地址看成3部分,只不過硬體是使用,軟體是設定,比如線性地址"0x80000000",如果核心按8位、8位、4位、12位劃分,建立對映時,它設定的就是0x80下標的目錄項,而硬體執行對映時,找到的是0x200下標的目錄項,顯然就會是空的或者其它虛擬地址的目錄項。
0x 1000000000 0000000000 000000000000 // 10,10,12 0x 10000000 00000000 0000 000000000000 // 8,8,4,12
- 但也很容易理解
首先,4層是邏輯劃分,如果劃分成10,0,10,12,那麼實際劃分仍然是3層,這是可行性;另外,Linux軟體不光只執行在80386上,還要支援其它型號的cpu,包括Intel的其它系列,以及其它廠商的cpu,不光要考慮當下,還要考慮將來,因為cpu都已經從8位發展到32位了,將來勢必會發展到更多位數,邏輯上支援4層,對於不同的cpu,簡單指定下位數的分配就能適應了,這是軟體設計的必要性。
3. 地址對映的全過程
前面已經學習了10.1節,那麼這部分內容其實就非常好理解了,書裡面拿了一個使用者空間的地址舉例,目前為止,初學者可能還是不能完全體會使用者態和核心態的本質,為了保持節奏,可以先不用擔心這一點,學習完第三章——中斷、異常和系統呼叫,以及第四章——程式與程式排程,自然就能明白了。
目前為止,只是學習了對映過程,關於記憶體管理的內容還很多:
- 每個程式有4G虛擬空間,其中0-3G由各個程式獨自使用,一方面數量有上限,也屬於資源,另一方面對映關係還沒有撤消,就不能拿來對映到另外一個實體地址,所以核心要對每個程式的虛擬空間進行管理,另外,實體地址更加需要管理;
- 虛擬空間又分為棧和堆,區域性變數使用的是棧記憶體,malloc()分配的是堆記憶體,核心對它們的管理,也有區別;
- 換入換出技術,支援將記憶體的資料,臨時存入交換分割槽,需要時再從磁碟讀回記憶體,也涉及到複雜的管理邏輯。
所以不要鬆懈,繼續加油!
相關文章
- Linux核心筆記005 - 越界訪問記憶體,Linux核心處理過程2020-06-06Linux筆記記憶體
- linux記憶體管理(六)- 核心新struct - folio2024-06-11Linux記憶體Struct
- Linux核心筆記002 - i386 的頁式記憶體管理機制2020-05-13Linux筆記記憶體
- 認識linux核心(linux核心的作用)2024-05-07Linux
- Linux 核心配置筆記2024-07-05Linux筆記
- 【Linux】核心學習筆記(一)——程序管理2024-03-21Linux筆記
- Linux核心自旋鎖使用筆記2019-05-14Linux筆記
- Linux核心記憶體保護機制:aslr和canary2024-12-10Linux記憶體
- Linux實體記憶體管理2024-11-28Linux記憶體
- 自用學習資料,Linux核心之[記憶體管理]的一些分享2021-12-23Linux記憶體
- Ubuntu複習筆記-認識Linux2022-01-02Ubuntu筆記Linux
- Linux共享記憶體的管理2018-06-07Linux記憶體
- Linux 記憶體區管理 slab2024-04-26Linux記憶體
- linux記憶體管理(二)- vmalloc2024-06-11Linux記憶體
- 掌握鴻蒙輕核心靜態記憶體的使用,從原始碼分析開始2021-06-21鴻蒙記憶體原始碼
- Linux核心筆記003 - Linux核心程式碼裡面的C語言和組合語言2020-05-14Linux筆記C語言組合語言
- Swoole 核心開發備忘:記憶體管理優化(swString)2020-06-28記憶體優化
- linux記憶體管理(一)實體記憶體的組織和記憶體分配2024-06-07Linux記憶體
- Linux 的記憶體分頁管理2018-08-08Linux記憶體
- Linux 記憶體管理 pt.32023-05-17Linux記憶體
- Linux 記憶體管理 pt.12023-04-27Linux記憶體
- Linux 記憶體管理 pt.22023-05-05Linux記憶體
- Linux的記憶體分頁管理2020-03-26Linux記憶體
- Linux-記憶體和磁碟管理2022-02-14Linux記憶體
- Linux記憶體洩露案例分析和記憶體管理分享2024-10-24Linux記憶體洩露
- 《Linux核心完全註釋》學習筆記:2.7 Linux核心原始碼的目錄結構2024-06-12Linux筆記原始碼
- Linux核心學習筆記(5)– 程式排程概述2018-09-06Linux筆記
- linux核心管理初步2018-03-25Linux
- linux記憶體管理學習總結2024-11-04Linux記憶體
- [Linux]共享記憶體2024-12-07Linux記憶體
- Linux使用者空間記憶體管理2018-09-26Linux記憶體
- linux 非連續記憶體區管理 vmalloc2024-04-26Linux記憶體
- linux記憶體管理(八)- 反向對映RMAP2024-06-15Linux記憶體
- linux記憶體管理(十)- 頁面回收(二)2024-06-18Linux記憶體
- linux記憶體管理(十一)- 頁面遷移2024-06-18Linux記憶體
- 淺析Linux Kernel[5.11.0]記憶體管理(一)2022-01-18Linux記憶體
- Linux共享記憶體(二)2018-03-09Linux記憶體
- Linux 虛擬記憶體2019-07-05Linux記憶體