實(shí)驗(yàn)復(fù)習(xí)指南:從進(jìn)程調(diào)度到文件系統(tǒng)的核心機(jī)制與易錯(cuò)點(diǎn))
簡介這份資源是面向高校操作系統(tǒng)課程學(xué)習(xí)者與系統(tǒng)軟件開發(fā)入門者的HNU_ostep實(shí)驗(yàn)合集圍繞進(jìn)程管理、內(nèi)存管理、文件系統(tǒng)與I/O系統(tǒng)等核心模塊展開幫助讀者在理論學(xué)習(xí)之外通過動(dòng)手編碼理解操作系統(tǒng)的內(nèi)部機(jī)制。壓縮包共收錄2000個(gè)文件以910個(gè)C源文件與760個(gè)頭文件為主體輔以Python腳本、Shell腳本、CMake與Make構(gòu)建文件、Markdown與TXT說明文檔以及少量PDF、DOCX講義資料整體約42.51MB目錄結(jié)構(gòu)便于按實(shí)驗(yàn)?zāi)K檢索。其中ostep_code目錄提供實(shí)驗(yàn)相關(guān)源碼藍(lán)本readme.txt給出實(shí)驗(yàn)總覽、環(huán)境搭建與任務(wù)要求讀者可據(jù)此修改調(diào)度算法、觀察不同策略對(duì)進(jìn)程執(zhí)行效率的影響并借助附書資料加深對(duì)虛擬內(nèi)存、頁面置換、文件權(quán)限控制等機(jī)制的理解。目前已有165人學(xué)習(xí)適合需要系統(tǒng)完成操作系統(tǒng)實(shí)驗(yàn)、積累底層開發(fā)經(jīng)驗(yàn)的學(xué)生參考。1. 操作系統(tǒng)實(shí)驗(yàn)到底在練什么一張從內(nèi)核到用戶態(tài)的知識(shí)地圖操作系統(tǒng)實(shí)驗(yàn)這門課很多人第一次翻開實(shí)驗(yàn)指導(dǎo)書時(shí)是懵的——明明理論課剛講完進(jìn)程調(diào)度、虛擬內(nèi)存、文件系統(tǒng)實(shí)驗(yàn)卻讓你去讀一段幾百行的 C 代碼還要在某個(gè)TODO處填幾行。它到底想練什么梳理下來會(huì)發(fā)現(xiàn)操作系統(tǒng)實(shí)驗(yàn)的核心不是讓你從零寫一個(gè)內(nèi)核而是讓你在已有的骨架代碼上親手補(bǔ)全那些理論課上被一筆帶過的關(guān)鍵機(jī)制進(jìn)程怎么被創(chuàng)建和切換、內(nèi)存怎么被映射和回收、文件怎么被打開和讀寫、系統(tǒng)調(diào)用怎么從用戶態(tài)陷入內(nèi)核態(tài)。整門實(shí)驗(yàn)的知識(shí)地圖大致分四層最底層是引導(dǎo)與中斷中間是進(jìn)程與調(diào)度再往上是內(nèi)存管理最上層是文件系統(tǒng)與系統(tǒng)調(diào)用接口。每一層都對(duì)應(yīng)理論課的一個(gè)章節(jié)但實(shí)驗(yàn)要求你看到真實(shí)的代碼路徑而不是只記住概念。適合誰適合已經(jīng)學(xué)過操作系統(tǒng)原理、能看懂 C 和匯編、但一遇到「上下文切換到底保存了哪些寄存器」「頁表項(xiàng)里那幾個(gè)標(biāo)志位分別管什么」就卡住的人。這份筆記就是把這條路徑上的重點(diǎn)、難點(diǎn)和易漏點(diǎn)系統(tǒng)整理一遍讓你復(fù)習(xí)時(shí)不用再翻散落的實(shí)驗(yàn)報(bào)告。2. 進(jìn)程與調(diào)度從 PCB 到上下文切換的復(fù)習(xí)重點(diǎn)2.1 進(jìn)程控制塊到底存了什么進(jìn)程控制塊PCB是操作系統(tǒng)里最核心的數(shù)據(jù)結(jié)構(gòu)之一實(shí)驗(yàn)里通常要求你補(bǔ)全它的字段定義和初始化邏輯。梳理下來會(huì)發(fā)現(xiàn)PCB 里真正影響調(diào)度和切換的字段就那么幾類標(biāo)識(shí)信息pid、父進(jìn)程 pid、狀態(tài)信息就緒、運(yùn)行、阻塞、上下文信息寄存器快照、棧指針、程序計(jì)數(shù)器、調(diào)度信息優(yōu)先級(jí)、時(shí)間片剩余、內(nèi)存信息頁表基址、代碼段和數(shù)據(jù)段邊界、文件信息打開文件描述符表。實(shí)驗(yàn)里最容易翻車的地方是上下文信息的保存順序——如果保存和恢復(fù)的順序不一致進(jìn)程切換后就會(huì)跑到錯(cuò)誤的地址去執(zhí)行。// 典型的上下文保存結(jié)構(gòu)示意 struct context { uint64_t ra; // 返回地址決定切換后從哪繼續(xù)執(zhí)行 uint64_t sp; // 棧指針指向該進(jìn)程的內(nèi)核棧 uint64_t s0; // 被調(diào)用者保存寄存器 uint64_t s1; // ... 其余被調(diào)用者保存寄存器 };這段結(jié)構(gòu)體的關(guān)鍵在于ra和sp必須成對(duì)保存和恢復(fù)s0~s11這些被調(diào)用者保存寄存器由切換函數(shù)負(fù)責(zé)而調(diào)用者保存寄存器由編譯器在函數(shù)調(diào)用時(shí)自行處理。實(shí)驗(yàn)里如果只保存了ra忘了sp切換后棧就亂了表現(xiàn)為進(jìn)程跑飛或直接觸發(fā)異常。參數(shù)說明ra在 RISC-V 里是返回地址寄存器sp是棧指針不同架構(gòu)名字不同但作用一致。2.2 調(diào)度器不是越復(fù)雜越好實(shí)驗(yàn)里常見的調(diào)度算法有時(shí)間片輪轉(zhuǎn)、優(yōu)先級(jí)調(diào)度和多級(jí)反饋隊(duì)列。很多人一上來就想寫多級(jí)反饋隊(duì)列覺得高級(jí)結(jié)果時(shí)間片和優(yōu)先級(jí)升降規(guī)則沒調(diào)好反而出現(xiàn)進(jìn)程餓死。血淚經(jīng)驗(yàn)是先把時(shí)間片輪轉(zhuǎn)跑通確保每個(gè)進(jìn)程都能被切換到再加優(yōu)先級(jí)。調(diào)度器的核心邏輯其實(shí)就三步從就緒隊(duì)列選一個(gè)進(jìn)程、保存當(dāng)前進(jìn)程上下文、恢復(fù)目標(biāo)進(jìn)程上下文。重點(diǎn)在于就緒隊(duì)列的維護(hù)——進(jìn)程從阻塞變就緒時(shí)要入隊(duì)時(shí)間片用完時(shí)要重新入隊(duì)進(jìn)程退出時(shí)要移出隊(duì)列。易錯(cuò)點(diǎn)是忘記在時(shí)鐘中斷里遞減時(shí)間片導(dǎo)致輪轉(zhuǎn)永遠(yuǎn)不觸發(fā)。調(diào)度算法關(guān)鍵參數(shù)適用場景實(shí)驗(yàn)常見問題時(shí)間片輪轉(zhuǎn)時(shí)間片長度通用分時(shí)時(shí)間片過大退化成先來先服務(wù)優(yōu)先級(jí)調(diào)度優(yōu)先級(jí)數(shù)值實(shí)時(shí)任務(wù)低優(yōu)先級(jí)進(jìn)程餓死多級(jí)反饋隊(duì)列隊(duì)列級(jí)數(shù)、升降規(guī)則混合負(fù)載規(guī)則復(fù)雜調(diào)試?yán)щy2.3 上下文切換的完整路徑一次完整的上下文切換發(fā)生在這些時(shí)刻時(shí)鐘中斷、系統(tǒng)調(diào)用、進(jìn)程主動(dòng)讓出。路徑通常是中斷/異常入口 → 保存用戶態(tài)上下文到內(nèi)核棧 → 進(jìn)入內(nèi)核處理 → 調(diào)度器選新進(jìn)程 → 切換內(nèi)核棧和 PCB → 恢復(fù)新進(jìn)程上下文 → 返回用戶態(tài)。實(shí)驗(yàn)里最容易忽略的是內(nèi)核棧的切換——每個(gè)進(jìn)程有自己的內(nèi)核棧切換時(shí)必須同時(shí)切換棧指針否則新進(jìn)程會(huì)用舊進(jìn)程的內(nèi)核棧數(shù)據(jù)互相覆蓋。梳理下來會(huì)發(fā)現(xiàn)上下文切換的代碼通常很短但每一步都不能少少一步就是玄學(xué)崩潰。3. 內(nèi)存管理頁表、缺頁與地址轉(zhuǎn)換的易錯(cuò)點(diǎn)3.1 頁表項(xiàng)里每個(gè)標(biāo)志位都要會(huì)用虛擬內(nèi)存實(shí)驗(yàn)的核心是頁表。頁表項(xiàng)PTE里除了物理頁號(hào)還有一堆標(biāo)志位有效位 V該映射是否有效、讀寫位 R/W是否可寫、用戶位 U用戶態(tài)是否可訪問、訪問位 A是否被訪問過、臟位 D是否被寫過。實(shí)驗(yàn)里經(jīng)常要求你實(shí)現(xiàn)按需分頁這時(shí)有效位就是關(guān)鍵——初始時(shí)把所有頁的有效位清零訪問時(shí)觸發(fā)缺頁異常在異常處理里分配物理頁并置有效位。最容易錯(cuò)的是忘記刷新 TLB改了頁表項(xiàng)之后如果 TLB 里還有舊緩存CPU 還是會(huì)用舊的映射表現(xiàn)為改了頁表但程序行為不變。補(bǔ)法是在修改頁表后執(zhí)行sfence.vma或等效的 TLB 刷新指令。// 缺頁異常處理的核心邏輯示意 void handle_page_fault(uint64_t fault_addr) { pte_t *pte walk_page_table(fault_addr); if (!(*pte PTE_V)) { // 分配物理頁建立映射 void *page alloc_page(); *pte PA2PTE(page) | PTE_V | PTE_R | PTE_W | PTE_U; flush_tlb(); // 關(guān)鍵刷新 TLB } else { // 權(quán)限錯(cuò)誤或其他異常 kill_process(); } }這段代碼里walk_page_table負(fù)責(zé)逐級(jí)查找頁表PA2PTE把物理地址轉(zhuǎn)成頁表項(xiàng)格式flush_tlb是必須的收尾動(dòng)作。參數(shù)說明fault_addr是觸發(fā)缺頁的虛擬地址通常從異常寄存器里讀。實(shí)驗(yàn)里如果缺頁處理寫對(duì)了但沒刷新 TLB會(huì)出現(xiàn)「第一次訪問正常第二次訪問又缺頁」的詭異現(xiàn)象。3.2 地址轉(zhuǎn)換的每一步都要能手工推實(shí)驗(yàn)考試或調(diào)試時(shí)經(jīng)常需要你手工把一個(gè)虛擬地址翻譯成物理地址。步驟是從頁表基址寄存器拿到一級(jí)頁表物理地址用虛擬地址的各級(jí)索引逐級(jí)查表最后加上頁內(nèi)偏移。梳理下來會(huì)發(fā)現(xiàn)出錯(cuò)最多的地方是索引位數(shù)算錯(cuò)——比如 39 位虛擬地址、4KB 頁大小、三級(jí)頁表每級(jí)索引 9 位頁內(nèi)偏移 12 位。如果記成每級(jí) 10 位整個(gè)轉(zhuǎn)換就全錯(cuò)了。建議在實(shí)驗(yàn)前把所在架構(gòu)的地址劃分畫一遍標(biāo)清楚每一級(jí)的位數(shù)和含義。3.3 內(nèi)存分配器的碎片問題實(shí)驗(yàn)里如果要求實(shí)現(xiàn)簡單的內(nèi)存分配器重點(diǎn)不是寫得多花哨而是避免碎片。常見做法是維護(hù)空閑鏈表分配時(shí)找足夠大的塊釋放時(shí)合并相鄰空閑塊。易漏點(diǎn)是合并時(shí)只合并了后一塊忘了前一塊導(dǎo)致空閑塊越切越碎。補(bǔ)法是在空閑塊頭部記錄大小和狀態(tài)釋放時(shí)檢查前后相鄰塊是否空閑是則合并。這個(gè)邏輯在實(shí)驗(yàn)里通常幾十行就能寫完但邊界條件很多建議用幾個(gè)固定場景手工推演一遍。4. 文件系統(tǒng)與系統(tǒng)調(diào)用從打開到讀寫的完整鏈路4.1 文件描述符表的層級(jí)關(guān)系文件系統(tǒng)實(shí)驗(yàn)里一個(gè)容易繞暈的點(diǎn)是文件描述符、打開文件表項(xiàng)、inode 三者之間的關(guān)系。梳理下來會(huì)發(fā)現(xiàn)進(jìn)程的 PCB 里有一張文件描述符表每個(gè)表項(xiàng)指向一個(gè)系統(tǒng)級(jí)的打開文件表項(xiàng)打開文件表項(xiàng)里記錄了讀寫偏移和指向 inode 的指針inode 里才是文件的元數(shù)據(jù)和數(shù)據(jù)塊指針。實(shí)驗(yàn)里??嫉氖莊ork之后文件描述符的共享問題——父子進(jìn)程共享打開文件表項(xiàng)所以讀寫偏移是共享的但文件描述符表是各自獨(dú)立的。如果實(shí)驗(yàn)里要求實(shí)現(xiàn)dup就是讓兩個(gè)文件描述符指向同一個(gè)打開文件表項(xiàng)偏移也共享。層級(jí)存儲(chǔ)位置關(guān)鍵內(nèi)容共享范圍文件描述符表進(jìn)程 PCB指向打開文件表項(xiàng)的指針進(jìn)程內(nèi)打開文件表項(xiàng)系統(tǒng)全局讀寫偏移、狀態(tài)標(biāo)志、inode 指針可跨進(jìn)程共享inode磁盤/內(nèi)存緩存文件元數(shù)據(jù)、數(shù)據(jù)塊指針全局唯一4.2 系統(tǒng)調(diào)用的陷入與返回系統(tǒng)調(diào)用是用戶態(tài)和內(nèi)核態(tài)之間的橋梁。實(shí)驗(yàn)里通常要求你補(bǔ)全系統(tǒng)調(diào)用的入口和分發(fā)邏輯。路徑是用戶態(tài)把系統(tǒng)調(diào)用號(hào)放入指定寄存器執(zhí)行陷入指令CPU 切換到內(nèi)核態(tài)入口代碼保存用戶態(tài)上下文根據(jù)系統(tǒng)調(diào)用號(hào)查分發(fā)表執(zhí)行對(duì)應(yīng)處理函數(shù)把返回值放回寄存器恢復(fù)上下文返回用戶態(tài)。最容易錯(cuò)的是返回值處理——很多實(shí)驗(yàn)框架約定返回值放在特定寄存器如果放錯(cuò)寄存器用戶態(tài)拿到的就是垃圾值。補(bǔ)法是先看實(shí)驗(yàn)框架的文檔或注釋確認(rèn)返回值寄存器和錯(cuò)誤碼約定。// 系統(tǒng)調(diào)用分發(fā)邏輯示意 long syscall_handler(long num, long arg0, long arg1, long arg2) { switch (num) { case SYS_READ: return sys_read(arg0, (void *)arg1, arg2); case SYS_WRITE: return sys_write(arg0, (void *)arg1, arg2); case SYS_OPEN: return sys_open((const char *)arg0, arg1); case SYS_CLOSE: return sys_close(arg0); default: return -ENOSYS; // 未知系統(tǒng)調(diào)用返回錯(cuò)誤 } }這段分發(fā)邏輯的關(guān)鍵是參數(shù)傳遞順序和錯(cuò)誤碼約定。參數(shù)說明num是系統(tǒng)調(diào)用號(hào)arg0~arg2是從用戶態(tài)寄存器里取出的參數(shù)返回值負(fù)數(shù)表示錯(cuò)誤。實(shí)驗(yàn)里如果sys_read忘了檢查用戶態(tài)緩沖區(qū)地址是否合法就可能被惡意程序利用這也是易漏的安全檢查點(diǎn)。4.3 文件讀寫與緩存一致性文件讀寫實(shí)驗(yàn)里重點(diǎn)在于緩存和磁盤的一致性。如果實(shí)現(xiàn)了頁緩存或塊緩存寫操作先寫緩存什么時(shí)候?qū)懟卮疟P就是個(gè)策略問題。實(shí)驗(yàn)里常見做法是寫穿透write-through或?qū)懟豾rite-back。寫穿透簡單但性能差寫回性能好但要在適當(dāng)時(shí)機(jī)刷盤。易錯(cuò)點(diǎn)是關(guān)閉文件時(shí)忘記刷盤導(dǎo)致數(shù)據(jù)丟失。補(bǔ)法是在close系統(tǒng)調(diào)用里強(qiáng)制把臟塊寫回。梳理下來會(huì)發(fā)現(xiàn)文件系統(tǒng)的實(shí)驗(yàn)代碼量通常比進(jìn)程和內(nèi)存大但核心邏輯就是「查緩存、沒有就加載、改了標(biāo)記臟、該刷就刷」這四步。5. 實(shí)驗(yàn)調(diào)試與查漏補(bǔ)缺那些最容易忽略的細(xì)節(jié)5.1 易漏點(diǎn)一中斷使能狀態(tài)易漏點(diǎn)在臨界區(qū)里忘記關(guān)中斷或者出了臨界區(qū)忘記開中斷。為什么漏中斷是異步的調(diào)試時(shí)如果沒觸發(fā)就感覺不到問題一旦觸發(fā)就是隨機(jī)崩潰。怎么補(bǔ)在操作共享數(shù)據(jù)結(jié)構(gòu)前后成對(duì)使用關(guān)中斷和開中斷并在實(shí)驗(yàn)報(bào)告里標(biāo)注哪些代碼段屬于臨界區(qū)。梳理下來會(huì)發(fā)現(xiàn)很多玄學(xué) bug 都是中斷沒管好。5.2 易漏點(diǎn)二棧對(duì)齊易漏點(diǎn)上下文切換或異常入口沒有保證棧指針對(duì)齊。為什么漏某些架構(gòu)要求棧指針 16 字節(jié)對(duì)齊不對(duì)齊時(shí)訪問浮點(diǎn)或向量指令會(huì)觸發(fā)異常但普通整數(shù)指令可能正常導(dǎo)致問題隱蔽。怎么補(bǔ)在保存上下文前檢查棧指針對(duì)齊必要時(shí)調(diào)整。實(shí)驗(yàn)里如果遇到「明明邏輯對(duì)但就是跑不通」先查棧對(duì)齊。5.3 易漏點(diǎn)三頁表刷新時(shí)機(jī)易漏點(diǎn)修改頁表后沒有立即刷新 TLB。為什么漏TLB 是硬件緩存刷新指令執(zhí)行了但可能被亂序執(zhí)行繞過或者刷新范圍不對(duì)。怎么補(bǔ)修改頁表后執(zhí)行全量刷新或按地址刷新并確保刷新指令在修改之后執(zhí)行。這個(gè)點(diǎn)在內(nèi)存實(shí)驗(yàn)里出現(xiàn)頻率極高。5.4 易漏點(diǎn)四系統(tǒng)調(diào)用參數(shù)校驗(yàn)易漏點(diǎn)直接使用用戶態(tài)傳來的指針沒有校驗(yàn)地址合法性。為什么漏實(shí)驗(yàn)環(huán)境通常信任用戶程序但考試或評(píng)分時(shí)可能用非法地址測試。怎么補(bǔ)在系統(tǒng)調(diào)用入口檢查指針是否落在用戶地址空間范圍內(nèi)越界直接返回錯(cuò)誤碼。這是從實(shí)驗(yàn)走向真實(shí)內(nèi)核的必經(jīng)一步。5.5 易漏點(diǎn)五進(jìn)程退出時(shí)的資源回收易漏點(diǎn)進(jìn)程退出時(shí)只釋放了部分資源比如釋放了內(nèi)存但沒關(guān)閉文件或者沒回收 PCB。為什么漏單個(gè)進(jìn)程退出時(shí)問題不明顯反復(fù)創(chuàng)建退出后才暴露資源泄漏。怎么補(bǔ)在進(jìn)程退出路徑上逐項(xiàng)檢查內(nèi)存、文件描述符、PCB、內(nèi)核棧確保全部回收。梳理下來會(huì)發(fā)現(xiàn)資源回收的代碼通常分散在多處建議畫一張回收清單對(duì)照檢查。6. 復(fù)習(xí)清單從知識(shí)地圖到動(dòng)手驗(yàn)證的最后一公里復(fù)習(xí)操作系統(tǒng)實(shí)驗(yàn)最有效的方式不是反復(fù)看代碼而是按知識(shí)地圖逐層自測。下面這份清單按模塊列出每一條都對(duì)應(yīng)一個(gè)可以動(dòng)手驗(yàn)證的動(dòng)作。第一層引導(dǎo)與中斷能說清楚啟動(dòng)流程從加電到進(jìn)入內(nèi)核的每一步能手工寫出中斷入口保存了哪些寄存器能解釋時(shí)鐘中斷如何觸發(fā)調(diào)度。第二層進(jìn)程與調(diào)度能畫出 PCB 的關(guān)鍵字段能手工推演一次上下文切換的寄存器變化能說出時(shí)間片輪轉(zhuǎn)和優(yōu)先級(jí)調(diào)度的區(qū)別以及各自的問題。第三層內(nèi)存管理能手工翻譯一個(gè)虛擬地址到物理地址能解釋頁表項(xiàng)每個(gè)標(biāo)志位的作用能說出缺頁異常處理的完整步驟和 TLB 刷新時(shí)機(jī)。第四層文件系統(tǒng)與系統(tǒng)調(diào)用能畫出文件描述符到 inode 的層級(jí)關(guān)系能說出fork和dup對(duì)打開文件表項(xiàng)的影響能寫出系統(tǒng)調(diào)用從陷入到返回的完整路徑。模塊自測動(dòng)作通過標(biāo)準(zhǔn)引導(dǎo)與中斷畫出中斷入口的寄存器保存順序能解釋每個(gè)寄存器的用途進(jìn)程與調(diào)度手工推演一次上下文切換能說出 ra 和 sp 的變化內(nèi)存管理手工翻譯一個(gè)虛擬地址每級(jí)索引位數(shù)正確文件系統(tǒng)畫出 fd → 表項(xiàng) → inode 關(guān)系能解釋 fork 后的共享行為系統(tǒng)調(diào)用寫出分發(fā)邏輯的偽代碼參數(shù)和返回值寄存器正確查漏補(bǔ)缺的具體技巧找一個(gè)沒跑通的實(shí)驗(yàn)不要直接看答案而是在關(guān)鍵路徑上加打印。比如上下文切換實(shí)驗(yàn)在保存和恢復(fù)前后各打印一次寄存器值對(duì)比是否一致缺頁實(shí)驗(yàn)在缺頁處理前后打印頁表項(xiàng)看有效位和物理頁號(hào)是否按預(yù)期變化。梳理下來會(huì)發(fā)現(xiàn)大部分 bug 不是邏輯寫錯(cuò)而是某個(gè)步驟沒執(zhí)行或執(zhí)行順序不對(duì)。從那以后我每次做實(shí)驗(yàn)都強(qiáng)制走一遍「先畫路徑、再標(biāo)關(guān)鍵點(diǎn)、最后加打印驗(yàn)證」的流程比盲目改代碼快得多。希望這份清單能幫你在復(fù)習(xí)時(shí)少走彎路把時(shí)間花在真正需要?jiǎng)邮烛?yàn)證的地方。本文還有配套的精品資源點(diǎn)擊獲取