內(nèi)核實驗:從啟動到進程調(diào)度的完整實戰(zhàn)指南)
簡介上海交通大學(xué)頭歌實踐教學(xué)平臺Chcore操作系統(tǒng)實驗整理版docx資料定位為操作系統(tǒng)課程配套學(xué)習(xí)材料適合正在完成內(nèi)存管理、系統(tǒng)調(diào)用與缺頁異常實驗的本科生與自學(xué)者。文檔以實驗二內(nèi)存管理和實驗三系統(tǒng)調(diào)用與缺頁異常為主線系統(tǒng)梳理分頁機制、頁表管理、內(nèi)存分配與回收、內(nèi)存保護、換頁流程、系統(tǒng)調(diào)用實現(xiàn)、中斷與異常處理、缺頁異常處理流程、頁替換策略及緩存預(yù)讀取等核心知識點并給出評測可通的關(guān)鍵命令提示方便對照環(huán)境操作和排錯。壓縮包內(nèi)共1個docx文檔整體大小583KB內(nèi)容集中適合直接閱讀或打印。目前已有5031人瀏覽學(xué)習(xí)適合借助具體實驗深入理解操作系統(tǒng)底層機制快速完成實驗并提升系統(tǒng)編程能力。 如果你正在學(xué)操作系統(tǒng)、但又不想只看書刷題那我強烈建議你去“頭歌”上把 ChCore 操作系統(tǒng)實驗完整做一遍。這個實驗不是那種給一個虛擬教學(xué)系統(tǒng)寫兩行代碼就算完事的作業(yè)而是讓你真正把一個教學(xué)操作系統(tǒng)內(nèi)核從啟動到進程調(diào)度跑起來。ChCore 是上海交大軟件學(xué)院設(shè)計的教學(xué)操作系統(tǒng)和很多學(xué)校用的 xv6、uCore 系列相比它的代碼結(jié)構(gòu)更貼近現(xiàn)代內(nèi)核的組織方式實驗題目的梯度也安排得比較合理。做完一遍之后你對 Linux 內(nèi)核里“進程”“虛擬內(nèi)存”“系統(tǒng)調(diào)用”“調(diào)度器”這些概念的理解會完全不一樣。這篇文章我會從一個實際做完這套實驗的視角把 ChCore 實驗的整體思路、核心知識點、實操流程和踩坑記錄全部梳理一遍。不管你是剛開始接觸操作系統(tǒng)實驗的本科生還是想補內(nèi)核基礎(chǔ)的在職開發(fā)者只要愿意花時間跟著動手這套實驗都能給你帶來實打?qū)嵉奶嵘?. 實驗整體設(shè)計與思路拆解1.1 ChCore 實驗到底在做什么ChCore 實驗的目標很明確讓你從零開始逐步實現(xiàn)一個可運行的操作系統(tǒng)內(nèi)核。整個實驗基于 ARM64 架構(gòu)這和大部分學(xué)校教的 x86 平臺不太一樣但 ARM64 在現(xiàn)代移動設(shè)備和服務(wù)器領(lǐng)域已經(jīng)是絕對主流所以學(xué)這個并不吃虧。實驗被拆成多個 Lab每個 Lab 對應(yīng)內(nèi)核的一個核心模塊。按目前常見的版本大致包括啟動與內(nèi)核初始化、物理內(nèi)存管理、虛擬內(nèi)存與頁表、異常與系統(tǒng)調(diào)用、進程管理與調(diào)度、進程間通信等。每個 Lab 內(nèi)部又分為若干個小任務(wù)任務(wù)之間有明顯的遞進關(guān)系前一個任務(wù)沒做好后面的實驗基本跑不起來。這里我想先解釋一個很多人困惑的問題為什么操作系統(tǒng)實驗不直接讓大家去讀 Linux 源碼因為 Linux 源碼體量太大一個初學(xué)者扎進去很容易迷失在復(fù)雜的驅(qū)動和兼容性代碼里。ChCore 的做法是給出一個精簡但五臟俱全的內(nèi)核骨架把核心機制用清晰的文件組織起來然后在關(guān)鍵位置留空給你填。這樣既能讓你理解真實內(nèi)核的工作方式又不會因為代碼量過大而勸退。1.2 為什么這套實驗值得認真做我自己做過不少教學(xué)內(nèi)核實驗橫向?qū)Ρ认聛鞢hCore 有幾個特點很突出。第一它的代碼結(jié)構(gòu)非常接近真實工程。ChCore 不是把幾個 .c 文件堆在一起的教學(xué)玩具而是有相對完整的構(gòu)建系統(tǒng)、Makefile、鏈接腳本和內(nèi)核加載流程。做完實驗?zāi)銜Α耙粋€內(nèi)核是怎么被編譯、鏈接、引導(dǎo)起來的”有一個整體認識這種認知在只做算法題或者寫應(yīng)用層代碼時很難獲得。第二實驗里涉及的概念都是操作系統(tǒng)課程的核心考點。頁表、異常向量表、上下文切換、調(diào)度隊列、鎖這些既是面試常問的內(nèi)容也是理解現(xiàn)代操作系統(tǒng)不能繞開的東西。你在 ChCore 里親手寫過一遍之后再去看到 Linux 里對應(yīng)的實現(xiàn)會有一種“原來如此”的貫通感。第三頭歌平臺本身做了很好的實驗流程封裝。你不需要手動搭建交叉編譯環(huán)境也不用愁怎么把內(nèi)核燒到開發(fā)板上平臺已經(jīng)把編譯器和運行環(huán)境都準備好了你只需要在線編寫代碼、提交驗證。對很多被實驗環(huán)境折騰到放棄的同學(xué)來說這一點非常友好。2. 核心知識點解析與關(guān)鍵技術(shù)點2.1 內(nèi)核啟動與初始化第一次接觸鏈接腳本和啟動代碼ChCore 實驗的第一個攔路虎通常是 Lab1 的啟動部分。這里涉及兩個關(guān)鍵內(nèi)容鏈接腳本和啟動匯編代碼。鏈接腳本linker script的作用是告訴鏈接器內(nèi)核的各個段應(yīng)該放在什么地址。ChCore 運行在 ARM64 平臺內(nèi)核一般會被加載到高地址。如果你不理解鏈接腳本就會出現(xiàn)一種很詭異的現(xiàn)象代碼燒進去后PC程序計數(shù)器跳到一個不存在的地址然后整個內(nèi)核靜默死掉。啟動代碼則是用匯編寫的。ARM64 的啟動過程要設(shè)置棧指針、清零 BSS 段、設(shè)置異常向量表最后跳轉(zhuǎn)到 C 語言的入口函數(shù)。這些步驟聽起來簡單但順序錯了或者寄存器用錯了都會導(dǎo)致內(nèi)核無法啟動。我個人的學(xué)習(xí)經(jīng)驗是這里不要急著跳過去哪怕你把代碼抄一遍也必須搞清楚每一行匯編在做什么。因為后面異常處理、上下文切換里還會出現(xiàn)大量匯編代碼啟動階段的基礎(chǔ)沒打好后面會越看越懵。2.2 物理內(nèi)存與虛擬內(nèi)存看清地址轉(zhuǎn)換的本質(zhì)內(nèi)存管理是操作系統(tǒng)實驗的重頭戲ChCore 在這里的設(shè)計也很有代表性。物理內(nèi)存管理模塊負責(zé)把物理頁分配出去。你會實現(xiàn)一個頁分配器管理哪些物理頁是空閑的、哪些已經(jīng)被占用。這里往往要求用鏈表或者位圖來記錄頁的狀態(tài)。聽起來不太難但要做到分配效率高、碎片少還是需要花點心思的。虛擬內(nèi)存部分則涉及頁表。ARM64 使用多級頁表你需要理解頁表項的格式、地址翻譯的過程以及如何在內(nèi)核里操作頁表。ChCore 會讓你實現(xiàn)地址映射、取消映射、權(quán)限設(shè)置這些操作。寫這部分代碼的時候你會對“虛擬地址和物理地址是怎么一一對應(yīng)的”有非常直觀的體會。再往后你會遇到“缺頁異?!薄.斠粋€程序訪問了還沒有映射到物理頁的虛擬地址時CPU 會觸發(fā)缺頁異常內(nèi)核需要在這個異常處理函數(shù)里完成頁的分配和映射。這個過程要是實現(xiàn)得不對用戶程序就會莫名其妙地崩潰。排這類問題往往是實驗里最痛苦但也最有收獲的環(huán)節(jié)。2.3 異常處理與系統(tǒng)調(diào)用內(nèi)核態(tài)和用戶態(tài)的橋梁ChCore 的異常處理實驗會讓你實現(xiàn)異常向量表處理來自用戶態(tài)的系統(tǒng)調(diào)用。系統(tǒng)調(diào)用的流程是用戶程序通過一條特殊的指令陷入內(nèi)核CPU 跳到異常向量表中對應(yīng)的入口然后內(nèi)核保存現(xiàn)場、執(zhí)行系統(tǒng)調(diào)用處理函數(shù)、恢復(fù)現(xiàn)場最后返回用戶態(tài)。這套機制看起來很標準但實際寫的時候問題很多。比如怎么區(qū)分異常來源是內(nèi)核態(tài)還是用戶態(tài)怎么正確的保存和恢復(fù)寄存器系統(tǒng)調(diào)用號和參數(shù)怎么傳遞返回值怎么寫給用戶程序這些細節(jié)任何一個出錯用戶程序都會拿不到正確的結(jié)果。調(diào)試這個階段的問題通常需要結(jié)合 GDB 或者打印日志來看。我建議你從一開始就給關(guān)鍵函數(shù)加上清晰的打印內(nèi)核崩潰的時候至少能看出來是死在哪個函數(shù)、哪個地址不然光靠猜真的會瘋掉。2.4 進程管理與調(diào)度讓多個程序輪流跑起來進程管理實驗的目標是讓內(nèi)核能夠創(chuàng)建、切換和調(diào)度多個進程。這里最核心的一段代碼是上下文切換函數(shù)通常叫switch_to或者context_switch。它負責(zé)保存當前進程的寄存器現(xiàn)場恢復(fù)下一個進程的寄存器現(xiàn)場然后跳轉(zhuǎn)到下一個進程的指令流里。在這個階段你會實現(xiàn)進程控制塊PCB用一個結(jié)構(gòu)體來記錄進程的狀態(tài)、棧、調(diào)度信息等。然后實現(xiàn)調(diào)度器從就緒隊列里挑一個進程來運行。ChCore 里通常會有時間片輪轉(zhuǎn)或者優(yōu)先級調(diào)度的要求你需要選擇合適的調(diào)度策略并保證正確性。調(diào)度實驗的難點在于并發(fā)。多個進程共用一個 CPU稍不注意就會互相踩內(nèi)存、破壞棧出現(xiàn)各種奇奇怪怪的錯誤。很多你在應(yīng)用層寫代碼時不會遇到的內(nèi)存破壞問題在這里會集中爆發(fā)。3. 實操過程從環(huán)境準備到逐關(guān)完成3.1 環(huán)境準備與工程結(jié)構(gòu)如果你是在頭歌平臺做實驗環(huán)境問題基本被解決了大半。你只需要登錄平臺進入對應(yīng)的 ChCore 實驗課程打開在線代碼倉庫就能直接開始寫代碼。平臺通常會提供一個倉庫地址和一個遠程代碼提交入口。你可以在線編輯代碼也可以把倉庫下載到本地開發(fā)再推送到遠程去驗證。我個人的習(xí)慣是下載到本地用 IDE 寫因為本地代碼補全、跳轉(zhuǎn)和調(diào)試都更方便。ChCore 工程的目錄結(jié)構(gòu)大致如下kernel/ common/ 公共工具如鏈表、日志 mm/ 內(nèi)存管理相關(guān)代碼 arch/ 架構(gòu)相關(guān)代碼啟動、異常、匯編 process/ 進程管理和調(diào)度 syscall/ 系統(tǒng)調(diào)用實現(xiàn) ipc/ 進程間通信 CMakeLists.txt 構(gòu)建配置 Makefile 編譯腳本你的任務(wù)就是把這些目錄里標識了 TODO 或者留空的地方補全。每個實驗的題目會明確指出你要在哪個文件里完成什么函數(shù)照著要求一步步做就行。3.2 實驗 1啟動與基礎(chǔ)輸出Lab1 一般要求你完成內(nèi)核的鏈接腳本配置讓內(nèi)核能夠被正確加載到高地址然后建立基本的輸出能力。這里你可能需要修改的是鏈接腳本里的內(nèi)存布局以及在匯編啟動代碼里初始化串口輸出。我第一次做的時候就在這里栽了跟頭。鏈接腳本里某個段的加載地址和執(zhí)行地址沒區(qū)分開導(dǎo)致內(nèi)核雖然被加載到了正確位置但運行時訪問的地址不對整個系統(tǒng)直接卡死。后來我把鏈接腳本里AT和運行地址的區(qū)別搞明白之后才真正理解“加載地址”和“運行地址”不是一回事。3.3 實驗 2/3內(nèi)存管理代碼實戰(zhàn)內(nèi)存管理相關(guān)的實驗建議按照“先物理內(nèi)存、再虛擬內(nèi)存、后缺頁異?!钡捻樞騺碜?。物理內(nèi)存分配器寫起來相對簡單關(guān)鍵是理解頁的數(shù)據(jù)結(jié)構(gòu)設(shè)計。ChCore 用了一個經(jīng)典的思路用一個全局的struct page數(shù)組來管理所有物理頁每個頁結(jié)構(gòu)里用鏈表指針把空閑頁串起來。寫虛擬內(nèi)存相關(guān)代碼的時候頁表操作要非常小心。ARM64 的頁表項里有很多位段比如類型位、有效位、訪問權(quán)限位等。你在設(shè)置映射的時候必須按照規(guī)范把這些位填好否則訪問內(nèi)存就會觸發(fā)異常。這里分享一個提升效率的技巧你可以先實現(xiàn)一個簡單的地址打印函數(shù)在映射完成后把虛擬地址和物理地址的對齊關(guān)系打印出來肉眼比對一下是否和預(yù)期一致。這比直接用 GDB 打斷點要直觀得多。3.4 實驗 4/5系統(tǒng)調(diào)用與進程調(diào)度實現(xiàn)系統(tǒng)調(diào)用實驗的核心是實現(xiàn)syscall的派發(fā)邏輯和處理函數(shù)。你需要定義一個系統(tǒng)調(diào)用號到處理函數(shù)的映射表然后根據(jù)用戶傳入的系統(tǒng)調(diào)用號跳轉(zhuǎn)執(zhí)行。一個常見的設(shè)計是syscall總?cè)肟谑且粋€匯編函數(shù)它把當前進程的寄存器現(xiàn)場壓入內(nèi)核棧然后調(diào)用 C 語言的syscall_handler。在syscall_handler里你用 switch-case 或者函數(shù)指針表找到對應(yīng)的處理函數(shù)執(zhí)行完成后再把返回值寫回到保存的寄存器里。調(diào)度器的實現(xiàn)則要看裝備的是哪種調(diào)度算法。如果要求時間片輪轉(zhuǎn)你就需要維護一個 FIFO 的就緒隊列每次定時器中斷觸發(fā)時把當前進程放到隊尾然后取出隊首進程來運行。寫到這里我想特別提醒上下文切換函數(shù)必須非常謹慎地處理棧指針和返回地址。很多調(diào)度器崩潰都是因為切換時棧沒有切換對導(dǎo)致返回到了錯誤的地址。我一般會在切換函數(shù)前后打印當前進程的 PID這樣一旦崩潰能快速定位是哪個進程切出去了。3.5 調(diào)試與驗證讓輸出成為你的朋友內(nèi)核調(diào)試和普通應(yīng)用調(diào)試很不一樣。在內(nèi)核里你不能輕易用printf因為可能你連堆都還沒初始化好。ChCore 提供了一些日志接口你可以用它來輸出調(diào)試信息但要注意在關(guān)鍵路徑上輸出太多日志會影響時序反過來導(dǎo)致 bug 更難復(fù)現(xiàn)。我的經(jīng)驗是盡量把日志打印放在“狀態(tài)變化”的位置而不是“每個循環(huán)”里。比如在進程狀態(tài)從就緒變?yōu)檫\行時打一條在頁表映射成功時打一條這樣既能追蹤流程又不會刷屏。如果平臺支持 GDB 遠程調(diào)試那是最好不過的。你可以在內(nèi)核入口處打斷點單步執(zhí)行啟動流程觀察寄存器的變化。這種斷點級別的調(diào)試對理解底層代碼幫助巨大。4. 常見問題與排查技巧實錄4.1 內(nèi)核啟動即崩潰沒有任何輸出這類問題十有八九是鏈接腳本、啟動匯編或者最基本的串口配置出了問題。排查順序建議是檢查鏈接腳本里的入口地址是否正確檢查啟動匯編里是否設(shè)置了棧指針檢查 BSS 段是否清空檢查串口的初始化代碼是否被編譯進內(nèi)核。如果你用的是頭歌的在線環(huán)境可以試著干凈地重新編譯一次因為有時候是緩存導(dǎo)致的鏈接錯誤。4.2 分配物理頁后訪問出現(xiàn)異常物理頁分配好之后如果不做映射或者映射的權(quán)限不對訪問就會出問題。這里需要確認兩點一是頁的虛擬地址是否被正確映射到分配的物理地址二是頁表項里的權(quán)限位是否允許當前 CPU 模式訪問。一個容易忽略的地方是內(nèi)核態(tài)訪問用戶態(tài)內(nèi)存時權(quán)限位檢查的邏輯可能和你想的不一樣。ARM64 的權(quán)限控制里區(qū)分了 EL1內(nèi)核和 EL0用戶態(tài)你得確保頁表項設(shè)置了AP[1]或類似允許內(nèi)核訪問的位。4.3 系統(tǒng)調(diào)用返回后用戶程序依舊崩潰系統(tǒng)調(diào)用執(zhí)行完安全返回用戶態(tài)是一個很精密的環(huán)節(jié)。你可以檢查以下幾個方面廣東省保存和恢復(fù)的寄存器數(shù)量是否一致系統(tǒng)調(diào)用返回值是否寫到了保存的寄存器里返回地址是否被正確恢復(fù)到了發(fā)生異常時的 PC是否把spsr和elr也正確處理了。最經(jīng)典的錯誤是在異常處理里用了一個臨時變量結(jié)果在恢復(fù)現(xiàn)場時把原本的寄存器給覆蓋了。這種問題盯著代碼看半天看不出來最好用 GDB 在異常返回的位置打一個斷點觀察每個寄存器的值是否符合預(yù)期。4.4 調(diào)度器切了幾次就死機調(diào)度器崩潰常見原因有兩個一個是就緒隊列指針被破壞另一個是上下文切換時棧搞錯了。你可以把就緒隊列的插入和刪除操作都加上日志看隊列長度是否正確同時在切換時打印“從進程 A 切到進程 B”如果發(fā)現(xiàn)進程號突然出現(xiàn)異常值就說明 PCB 的內(nèi)存被污染了。另外調(diào)度器里面如果有臨界區(qū)資源要記得加鎖。兩個進程同時修改就緒隊列會造成鏈表環(huán)這種偶發(fā)問題特別難查。4.5 常見問題速查表現(xiàn)象優(yōu)先排查點補充說明啟動無輸出鏈接腳本地址、棧指針、串口初始化可以先用最小的串口輸出代碼逐一驗證訪問內(nèi)存觸發(fā)異常頁表映射、權(quán)限位、地址對齊用地址打印函數(shù)驗證映射關(guān)系用戶程序崩潰系統(tǒng)調(diào)用返回值、現(xiàn)場恢復(fù)重點檢查 ELR、SPSR、通用寄存器調(diào)度幾次后死機隊列指針、棧切換、競態(tài)條件給隊列操作加日志確認 PCB 未被污染內(nèi)核打印亂碼串口波特率、時鐘初始化檢查時鐘和外設(shè)的初始化順序5. 寫在最后的一點個人體會我把 ChCore 實驗完整做完之后最大的感受是操作系統(tǒng)里那些抽象概念真的是“紙上得來終覺淺”。你背十遍“頁表是多級映射的”不如實際寫一遍頁表操作來得深刻。那些在課堂上聽起來云里霧里的術(shù)語比如“陷入內(nèi)核”“上下文切換”“就緒隊列”在親手敲完代碼之后突然就變成了非常具體的東西。如果你是自學(xué)的建議嚴格按照 Lab 順序走不要跳關(guān)。前面的啟動和內(nèi)存管理沒搞懂后面的進程調(diào)度會舉步維艱。同時一定要學(xué)會利用頭歌平臺的驗證機制每完成一個小任務(wù)就及時提交看結(jié)果拖到最后一次性排一堆 bug那是真的絕望。最后再分享一個小技巧如果你在某個實驗里卡了很久不妨把題目要求重新讀一遍然后把你已經(jīng)實現(xiàn)的部分全部注釋掉從頭再按自己的思路實現(xiàn)一遍。很多時候你認為“這里應(yīng)該這么寫”和題目真正要求的“你應(yīng)該在這里做什么”之間存在一個被忽略的差距。重寫一遍往往能讓你意識到自己思路里的盲區(qū)。本文還有配套的精品資源點擊獲取