課設(shè)代碼包實(shí)戰(zhàn)指南:從進(jìn)程調(diào)度到文件系統(tǒng)的完整拆解)
簡介這份資源是華中科技大學(xué)操作系統(tǒng)實(shí)驗(yàn)與課設(shè)的完整代碼合集面向計(jì)算機(jī)專業(yè)學(xué)生及希望深入理解操作系統(tǒng)原理的自學(xué)者覆蓋進(jìn)程管理、內(nèi)存管理、文件系統(tǒng)、設(shè)備管理、死鎖避免、線程調(diào)度、網(wǎng)絡(luò)編程與系統(tǒng)調(diào)用等核心模塊適合課程實(shí)驗(yàn)、課設(shè)參考與考研復(fù)習(xí)等場景。壓縮包共249個(gè)文件約41.66MB以class與jar編譯產(chǎn)物、xml配置、c與java源碼為主另含makefile構(gòu)建腳本、nc與ihex等硬件相關(guān)文件、out與exe可運(yùn)行結(jié)果及少量圖片和文檔便于對照源碼與運(yùn)行效果。目前已有169人學(xué)習(xí)下載。通過調(diào)度器、文件系統(tǒng)、設(shè)備驅(qū)動(dòng)等具體實(shí)驗(yàn)任務(wù)讀者可獲取可編譯運(yùn)行的參考實(shí)現(xiàn)、構(gòu)建腳本與排錯(cuò)思路并借助清晰的目錄結(jié)構(gòu)快速定位各知識(shí)點(diǎn)代碼為系統(tǒng)方向開發(fā)打下實(shí)踐基礎(chǔ)。1. 操作系統(tǒng)課設(shè)代碼包到底能給你省下多少時(shí)間如果你正在準(zhǔn)備操作系統(tǒng)課程設(shè)計(jì)大概率經(jīng)歷過這樣的場景對著課本上進(jìn)程調(diào)度、內(nèi)存管理、文件系統(tǒng)的理論描述腦子里能理解個(gè)大概但一旦要?jiǎng)邮謱懘a就不知道從哪下手。調(diào)度算法到底怎么組織數(shù)據(jù)結(jié)構(gòu)頁面置換的命中率怎么統(tǒng)計(jì)文件系統(tǒng)的目錄樹用什么結(jié)構(gòu)存這些問題課本不會(huì)給你答案但一份完整的課設(shè)代碼可以。華中科技大學(xué)操作系統(tǒng)實(shí)驗(yàn)與課設(shè)的代碼包在課程設(shè)計(jì)圈子里流傳很廣原因很直接它的實(shí)驗(yàn)覆蓋了操作系統(tǒng)課程的核心模塊從進(jìn)程管理、線程調(diào)度、內(nèi)存分配、頁面置換到文件系統(tǒng)實(shí)現(xiàn)每個(gè)實(shí)驗(yàn)都有可運(yùn)行的代碼和對應(yīng)的實(shí)驗(yàn)報(bào)告。對于正在做課設(shè)的人來說這份代碼的價(jià)值不在于照抄而在于它提供了一個(gè)可運(yùn)行的參照系——你能看到每個(gè)模塊的完整實(shí)現(xiàn)路徑知道調(diào)度隊(duì)列怎么建、信號量怎么封裝、磁盤塊怎么映射。這篇文章會(huì)從代碼結(jié)構(gòu)拆解開始逐步講清楚每個(gè)實(shí)驗(yàn)?zāi)K的實(shí)現(xiàn)思路、關(guān)鍵參數(shù)怎么調(diào)、編譯運(yùn)行時(shí)會(huì)遇到哪些坑以及怎么在它的基礎(chǔ)上改出自己的版本。2. 先看清代碼包的目錄結(jié)構(gòu)和實(shí)驗(yàn)?zāi)K劃分2.1 拿到壓縮包后的第一件事目錄樹和構(gòu)建入口很多人拿到代碼包之后直接找 main 函數(shù)開始讀這是效率最低的做法。操作系統(tǒng)課設(shè)的代碼通常按實(shí)驗(yàn)編號分目錄每個(gè)目錄是一個(gè)獨(dú)立的小型系統(tǒng)或模擬器。正確的第一步是先把目錄樹打印出來看清楚模塊邊界和構(gòu)建方式。# 解壓后先看頂層結(jié)構(gòu)不要急著進(jìn)子目錄 unzip operating-system-codes.zip -d os-course cd os-course # 打印兩層目錄樹快速定位每個(gè)實(shí)驗(yàn)的入口 find . -maxdepth 2 -type d | sort # 找所有 Makefile 和 CMakeLists確認(rèn)構(gòu)建方式 find . -name Makefile -o -name CMakeLists.txt | sort這段命令的邏輯很直接find -maxdepth 2限制遞歸深度避免目錄太多刷屏第二步找構(gòu)建文件是為了確認(rèn)每個(gè)實(shí)驗(yàn)是獨(dú)立編譯還是統(tǒng)一構(gòu)建。常見的組織方式是每個(gè)實(shí)驗(yàn)一個(gè)子目錄各自帶 Makefile互不依賴。如果發(fā)現(xiàn)只有一個(gè)頂層 Makefile說明所有實(shí)驗(yàn)共用一套構(gòu)建規(guī)則這時(shí)候要重點(diǎn)看 Makefile 里的SRCS和TARGET變量確認(rèn)每個(gè)實(shí)驗(yàn)的源文件列表。參數(shù)方面-maxdepth 2可以根據(jù)實(shí)際情況調(diào)整。如果目錄層級特別深改成 3 層如果頂層就很清晰保持 2 層足夠。關(guān)鍵是要在動(dòng)手改代碼之前先建立一張模塊地圖。2.2 各實(shí)驗(yàn)?zāi)K的功能邊界和依賴關(guān)系典型的操作系統(tǒng)課設(shè)代碼包會(huì)包含以下幾類實(shí)驗(yàn)每類的功能邊界和依賴關(guān)系不同實(shí)驗(yàn)?zāi)K核心功能依賴典型源文件數(shù)進(jìn)程調(diào)度模擬 PCB 管理、調(diào)度算法無3-5內(nèi)存管理分區(qū)分配、頁面置換無4-6文件系統(tǒng)目錄樹、文件讀寫、磁盤塊管理內(nèi)存管理6-10同步互斥信號量、生產(chǎn)者消費(fèi)者進(jìn)程調(diào)度2-4設(shè)備管理磁盤調(diào)度、緩沖池?zé)o3-5這張表的作用是幫你判斷閱讀順序。進(jìn)程調(diào)度和內(nèi)存管理通常是最獨(dú)立的模塊適合先讀文件系統(tǒng)往往依賴內(nèi)存管理的抽象如果先讀文件系統(tǒng)會(huì)卡在底層數(shù)據(jù)結(jié)構(gòu)上。同步互斥實(shí)驗(yàn)如果用了自己實(shí)現(xiàn)的線程庫需要先理解進(jìn)程調(diào)度的 PCB 結(jié)構(gòu)。我一般會(huì)建議按「進(jìn)程調(diào)度 → 同步互斥 → 內(nèi)存管理 → 文件系統(tǒng) → 設(shè)備管理」的順序讀。前兩個(gè)模塊建立對 PCB、狀態(tài)轉(zhuǎn)換、等待隊(duì)列的直覺后三個(gè)模塊建立對地址空間、頁表、磁盤塊映射的直覺。每個(gè)模塊讀完之后先編譯運(yùn)行一遍看輸出是否符合預(yù)期再進(jìn)入下一個(gè)。2.3 編譯環(huán)境和工具鏈的版本確認(rèn)操作系統(tǒng)課設(shè)代碼最常見的翻車點(diǎn)不是邏輯錯(cuò)誤而是編譯環(huán)境不匹配。很多代碼包是在特定版本的 GCC 和 Linux 內(nèi)核下寫的換一個(gè)環(huán)境就可能報(bào)錯(cuò)。# 確認(rèn)編譯器版本很多老代碼在 GCC 10 上會(huì)有警告變錯(cuò)誤的問題 gcc --version # 確認(rèn)系統(tǒng)架構(gòu)32 位和 64 位代碼在指針長度上不兼容 uname -m # 嘗試編譯第一個(gè)實(shí)驗(yàn)先不加任何修改 cd experiment-01-scheduler make 21 | tee build.log編譯日志一定要用tee保存下來因?yàn)椴僮飨到y(tǒng)課設(shè)的代碼經(jīng)常有大量警告刷屏之后你找不到第一個(gè)錯(cuò)誤在哪。如果編譯失敗先看build.log里第一個(gè)error而不是最后一個(gè)后面的錯(cuò)誤往往是第一個(gè)錯(cuò)誤引發(fā)的連鎖反應(yīng)。常見做法是在 Makefile 里加-w關(guān)掉警告但我建議不要這么做。操作系統(tǒng)代碼里的警告經(jīng)常指向真實(shí)問題比如指針截?cái)?、未初始化變量、隱式類型轉(zhuǎn)換。先讓編譯通過再逐個(gè)看警告比直接關(guān)掉警告要安全。3. 進(jìn)程調(diào)度與同步互斥實(shí)驗(yàn)的代碼拆解3.1 PCB 數(shù)據(jù)結(jié)構(gòu)和調(diào)度隊(duì)列的組織方式進(jìn)程調(diào)度實(shí)驗(yàn)的核心是 PCB進(jìn)程控制塊的設(shè)計(jì)。代碼包里通常會(huì)用結(jié)構(gòu)體或類來表示 PCB關(guān)鍵字段包括進(jìn)程 ID、狀態(tài)、優(yōu)先級、時(shí)間片、等待時(shí)間、周轉(zhuǎn)時(shí)間等。// 典型的 PCB 結(jié)構(gòu)定義字段命名可能不同但核心信息一致 typedef struct PCB { int pid; // 進(jìn)程標(biāo)識(shí)符 int state; // 狀態(tài)就緒/運(yùn)行/阻塞/完成 int priority; // 優(yōu)先級數(shù)值越小優(yōu)先級越高或反之 int burst_time; // 需要占用的 CPU 時(shí)間 int remaining_time; // 剩余執(zhí)行時(shí)間用于時(shí)間片輪轉(zhuǎn) int wait_time; // 累計(jì)等待時(shí)間 int turnaround_time; // 周轉(zhuǎn)時(shí)間 完成時(shí)間 - 到達(dá)時(shí)間 struct PCB *next; // 隊(duì)列指針 } PCB; // 就緒隊(duì)列通常用鏈表或數(shù)組實(shí)現(xiàn) typedef struct { PCB *head; PCB *tail; int count; } ReadyQueue;這段代碼的關(guān)鍵在于state字段和next指針的配合。調(diào)度算法的本質(zhì)就是操作這兩個(gè)字段把 PCB 從就緒隊(duì)列摘下、改狀態(tài)為運(yùn)行、時(shí)間片用完再掛回就緒隊(duì)列。如果你要改調(diào)度算法改的就是「從就緒隊(duì)列選哪個(gè) PCB」和「運(yùn)行多久后觸發(fā)重新調(diào)度」這兩個(gè)邏輯。參數(shù)方面priority的語義要特別注意。有的代碼里數(shù)值越小優(yōu)先級越高有的反過來。讀代碼時(shí)先找到調(diào)度函數(shù)里的比較邏輯確認(rèn)方向否則你改優(yōu)先級的時(shí)候會(huì)得到完全相反的結(jié)果。burst_time和remaining_time的關(guān)系也要看清楚如果調(diào)度算法不支持搶占remaining_time可能不會(huì)被更新如果支持時(shí)間片輪轉(zhuǎn)每次調(diào)度后必須扣減。3.2 時(shí)間片輪轉(zhuǎn)和優(yōu)先級調(diào)度的實(shí)現(xiàn)差異時(shí)間片輪轉(zhuǎn)和優(yōu)先級調(diào)度在代碼結(jié)構(gòu)上的差異主要體現(xiàn)在兩個(gè)地方就緒隊(duì)列的排序方式和調(diào)度觸發(fā)條件。// 時(shí)間片輪轉(zhuǎn)就緒隊(duì)列按到達(dá)順序排列每次取隊(duì)頭 PCB* rr_schedule(ReadyQueue *q, int time_slice) { if (q-count 0) return NULL; PCB *p q-head; q-head p-next; if (q-head NULL) q-tail NULL; q-count--; // 執(zhí)行一個(gè)時(shí)間片 if (p-remaining_time time_slice) { p-remaining_time - time_slice; p-state STATE_READY; enqueue(q, p); // 沒執(zhí)行完放回隊(duì)尾 } else { p-state STATE_DONE; p-turnaround_time current_time - p-arrival_time; } return p; } // 優(yōu)先級調(diào)度就緒隊(duì)列按優(yōu)先級排序每次取優(yōu)先級最高的 PCB* priority_schedule(ReadyQueue *q) { if (q-count 0) return NULL; // 遍歷找優(yōu)先級最高的 PCB PCB *prev NULL, *best q-head, *best_prev NULL; PCB *cur q-head; while (cur ! NULL) { if (cur-priority best-priority) { best cur; best_prev prev; } prev cur; cur cur-next; } // 從隊(duì)列中摘除 best if (best_prev NULL) q-head best-next; else best_prev-next best-next; if (best q-tail) q-tail best_prev; q-count--; return best; }兩段代碼的差異很清晰時(shí)間片輪轉(zhuǎn)的隊(duì)列是 FIFO調(diào)度邏輯在出隊(duì)之后優(yōu)先級調(diào)度的隊(duì)列需要遍歷找最優(yōu)調(diào)度邏輯在出隊(duì)之前。如果你要把時(shí)間片輪轉(zhuǎn)改成優(yōu)先級調(diào)度不能只改調(diào)度函數(shù)還要改入隊(duì)邏輯——優(yōu)先級調(diào)度需要在入隊(duì)時(shí)就按優(yōu)先級插入否則每次調(diào)度都要遍歷整個(gè)隊(duì)列效率很低。參數(shù)方面time_slice的取值直接影響調(diào)度效果。取值太小會(huì)導(dǎo)致上下文切換開銷占比過高取值太大會(huì)退化成先來先服務(wù)。常見做法是取所有進(jìn)程平均 burst_time 的 1/2 到 1/3。如果你在實(shí)驗(yàn)報(bào)告里要畫調(diào)度序列圖建議把time_slice設(shè)成 1、2、4 各跑一遍對比周轉(zhuǎn)時(shí)間和等待時(shí)間的變化趨勢。3.3 信號量實(shí)現(xiàn)里的原子性保證和常見錯(cuò)誤同步互斥實(shí)驗(yàn)的核心是信號量的 P/V 操作。代碼包里通常會(huì)用簡單的整型變量加忙等待來實(shí)現(xiàn)但這里有一個(gè)容易被忽略的問題P 操作里的判斷和減一必須是原子的。// 有問題的實(shí)現(xiàn)判斷和減一之間存在競態(tài)窗口 void P_bad(Semaphore *s) { while (s-value 0) { // 忙等待 } s-value--; // 這里可能被中斷打斷 } // 改進(jìn)方案用關(guān)中斷或原子指令保護(hù)臨界區(qū) void P_good(Semaphore *s) { while (1) { disable_interrupts(); // 進(jìn)入臨界區(qū) if (s-value 0) { s-value--; enable_interrupts(); break; } enable_interrupts(); // 讓出 CPU 或忙等待 } }這段代碼的差異在于臨界區(qū)的保護(hù)。在單核模擬環(huán)境里如果 P 操作執(zhí)行到一半被時(shí)鐘中斷打斷另一個(gè)進(jìn)程可能看到value還是正數(shù)也進(jìn)入臨界區(qū)導(dǎo)致兩個(gè)進(jìn)程同時(shí)訪問共享資源。disable_interrupts和enable_interrupts是常見的保護(hù)手段但要注意關(guān)中斷的時(shí)間不能太長否則會(huì)影響系統(tǒng)響應(yīng)。參數(shù)方面信號量的初值決定了允許多少個(gè)進(jìn)程同時(shí)進(jìn)入臨界區(qū)?;コ庑盘柫砍踔禐?1同步信號量初值根據(jù)資源數(shù)量設(shè)定。如果你在實(shí)驗(yàn)里看到進(jìn)程卡死先檢查信號量初值是否合理再檢查 P/V 是否配對。常見的錯(cuò)誤是 P 操作執(zhí)行了但 V 操作沒執(zhí)行或者 V 操作執(zhí)行了兩次導(dǎo)致信號量值溢出。4. 內(nèi)存管理與文件系統(tǒng)實(shí)驗(yàn)的實(shí)現(xiàn)要點(diǎn)4.1 分區(qū)分配算法的數(shù)據(jù)結(jié)構(gòu)選擇內(nèi)存管理實(shí)驗(yàn)通常要求實(shí)現(xiàn)首次適應(yīng)、最佳適應(yīng)、最壞適應(yīng)等分區(qū)分配算法。代碼包里會(huì)用空閑分區(qū)表或空閑分區(qū)鏈來管理內(nèi)存。// 空閑分區(qū)表數(shù)組實(shí)現(xiàn)適合分區(qū)數(shù)量固定的場景 typedef struct { int start_addr; // 分區(qū)起始地址 int size; // 分區(qū)大小 int is_free; // 是否空閑 } Partition; Partition memory[MAX_PARTITIONS]; int partition_count 0; // 首次適應(yīng)從頭遍歷找到第一個(gè)滿足大小的空閑分區(qū) int first_fit(int request_size) { for (int i 0; i partition_count; i) { if (memory[i].is_free memory[i].size request_size) { return i; // 返回分區(qū)索引 } } return -1; // 分配失敗 } // 最佳適應(yīng)遍歷所有空閑分區(qū)找滿足大小的最小分區(qū) int best_fit(int request_size) { int best_idx -1; int best_size INT_MAX; for (int i 0; i partition_count; i) { if (memory[i].is_free memory[i].size request_size) { if (memory[i].size best_size) { best_size memory[i].size; best_idx i; } } } return best_idx; }首次適應(yīng)和最佳適應(yīng)的代碼差異很小但性能差異很大。首次適應(yīng)分配快但容易產(chǎn)生外部碎片最佳適應(yīng)碎片少但每次分配都要遍歷整個(gè)表。如果你在實(shí)驗(yàn)里要對比算法效果建議用同一組請求序列跑不同算法統(tǒng)計(jì)分配失敗次數(shù)和碎片大小。參數(shù)方面MAX_PARTITIONS決定了系統(tǒng)能管理的最大分區(qū)數(shù)。如果設(shè)得太小頻繁分配釋放后分區(qū)表會(huì)滿設(shè)得太大遍歷開銷增加。常見做法是設(shè)成 100 到 200。另外要注意分區(qū)的合并邏輯釋放一個(gè)分區(qū)后如果它和前后的空閑分區(qū)相鄰必須合并否則碎片會(huì)越來越多。4.2 頁面置換算法的命中率統(tǒng)計(jì)和對比方法頁面置換實(shí)驗(yàn)的核心是統(tǒng)計(jì)不同算法的命中率。代碼包里通常會(huì)模擬一個(gè)固定大小的物理內(nèi)存和一組頁面訪問序列然后分別用 FIFO、LRU、OPT 等算法跑一遍。# 頁面置換模擬的通用框架 def simulate(pages, frame_count, algorithm): frames [] hits 0 for page in pages: if page in frames: hits 1 if algorithm LRU: frames.remove(page) frames.append(page) # LRU命中后移到末尾 else: if len(frames) frame_count: frames.append(page) else: if algorithm FIFO: frames.pop(0) # 淘汰最早進(jìn)入的 elif algorithm LRU: frames.pop(0) # 淘汰最久未使用的 elif algorithm OPT: # 淘汰未來最長時(shí)間不被訪問的 farthest 0 victim 0 for i, f in enumerate(frames): try: next_use pages[pages.index(page)1:].index(f) except ValueError: next_use float(inf) if next_use farthest: farthest next_use victim i frames.pop(victim) frames.append(page) return hits / len(pages)這段代碼的關(guān)鍵在于frames列表的維護(hù)方式。FIFO 和 LRU 在代碼上看起來很像都是pop(0)但語義完全不同F(xiàn)IFO 淘汰的是最早進(jìn)入的頁面LRU 淘汰的是最久未被訪問的頁面。LRU 在命中時(shí)需要把頁面移到列表末尾FIFO 不需要。如果你把這兩個(gè)算法搞混命中率統(tǒng)計(jì)會(huì)完全錯(cuò)誤。參數(shù)方面frame_count決定了物理內(nèi)存大小。通常實(shí)驗(yàn)會(huì)要求跑 3、4、5 個(gè)頁框各一遍觀察命中率隨頁框數(shù)增加的變化。pages序列的長度建議至少 1000 次訪問太短的序列統(tǒng)計(jì)波動(dòng)太大。如果你要對比 OPT 和 LRU 的差距OPT 的命中率是理論上限LRU 通常比它低 10% 到 20%如果差距超過 30%檢查你的 LRU 實(shí)現(xiàn)是否正確。4.3 文件系統(tǒng)目錄樹和磁盤塊映射的實(shí)現(xiàn)文件系統(tǒng)實(shí)驗(yàn)通常是難度最高的部分因?yàn)樗枰瑫r(shí)管理目錄樹、文件控制塊、磁盤塊分配和空閑塊管理。// 目錄項(xiàng)結(jié)構(gòu)文件名 inode 編號 typedef struct { char name[MAX_FILENAME]; int inode_id; } DirEntry; // inode 結(jié)構(gòu)文件元信息 數(shù)據(jù)塊指針 typedef struct { int id; int size; int type; // 文件或目錄 int block_ptrs[MAX_BLOCKS]; // 直接塊指針 int indirect_ptr; // 間接塊指針 } Inode; // 創(chuàng)建文件分配 inode在父目錄中添加目錄項(xiàng) int create_file(int parent_dir_inode, const char *name) { int new_inode alloc_inode(); if (new_inode 0) return -1; // 在父目錄的數(shù)據(jù)塊中寫入目錄項(xiàng) DirEntry entry; strncpy(entry.name, name, MAX_FILENAME); entry.inode_id new_inode; add_dir_entry(parent_dir_inode, entry); return new_inode; }這段代碼的核心是 inode 和目錄項(xiàng)的分離。inode 存文件的元信息和數(shù)據(jù)塊指針目錄項(xiàng)只存文件名和 inode 編號。這種設(shè)計(jì)的好處是目錄項(xiàng)大小固定方便在磁盤塊中順序存儲(chǔ)缺點(diǎn)是查找文件需要先讀目錄項(xiàng)再讀 inode多一次磁盤訪問。參數(shù)方面MAX_BLOCKS決定了單個(gè)文件的最大直接塊數(shù)量。如果文件很大需要用到間接塊。間接塊的實(shí)現(xiàn)是文件系統(tǒng)實(shí)驗(yàn)里最容易出錯(cuò)的地方一級間接塊存的是數(shù)據(jù)塊指針二級間接塊存的是間接塊指針。如果你在實(shí)驗(yàn)里遇到大文件讀寫失敗先檢查間接塊的分配和釋放邏輯。5. 避坑與排查課設(shè)代碼跑不起來時(shí)先看這幾條5.1 編譯報(bào)錯(cuò)頭文件缺失和類型沖突現(xiàn)象make時(shí)報(bào)fatal error: xxx.h: No such file or directory或者error: conflicting types for xxx。原因代碼包可能依賴特定版本的庫或頭文件或者在不同源文件里對同一個(gè)函數(shù)/變量的聲明不一致。操作系統(tǒng)課設(shè)代碼經(jīng)常有多個(gè)源文件共享全局變量如果頭文件里聲明了extern int current_time;但某個(gè)源文件里定義成了static int current_time;就會(huì)報(bào)類型沖突。解決先看報(bào)錯(cuò)的頭文件名確認(rèn)系統(tǒng)里是否安裝了對應(yīng)的開發(fā)包。如果是類型沖突用grep -rn xxx .找到所有聲明和定義的位置統(tǒng)一類型。常見做法是把所有全局變量聲明集中到一個(gè)公共頭文件里其他源文件只包含這個(gè)頭文件。5.2 運(yùn)行時(shí)崩潰段錯(cuò)誤和死循環(huán)現(xiàn)象程序編譯通過但運(yùn)行時(shí)報(bào)Segmentation fault或者卡住不動(dòng)。原因段錯(cuò)誤通常是空指針解引用或數(shù)組越界。操作系統(tǒng)課設(shè)代碼里常見的空指針場景包括從空隊(duì)列取 PCB、訪問未初始化的頁表項(xiàng)、目錄項(xiàng)指針為 NULL 時(shí)直接解引用。死循環(huán)常見于信號量的忙等待邏輯如果 P 操作的條件永遠(yuǎn)不滿足進(jìn)程會(huì)一直循環(huán)。解決用gdb跑一遍崩潰時(shí)看bt輸出定位到具體行。如果是死循環(huán)用CtrlC中斷后看where輸出。常見做法是在隊(duì)列操作和信號量操作里加斷言比如assert(q-count 0)讓問題盡早暴露。5.3 結(jié)果不對調(diào)度序列和預(yù)期不一致現(xiàn)象程序能跑但輸出的調(diào)度序列、命中率、分配結(jié)果和預(yù)期不符。原因最常見的是參數(shù)方向搞反了。比如優(yōu)先級調(diào)度里代碼認(rèn)為數(shù)值越大優(yōu)先級越高但你按數(shù)值越小優(yōu)先級越高來構(gòu)造測試數(shù)據(jù)。另一個(gè)常見原因是時(shí)間統(tǒng)計(jì)的基準(zhǔn)點(diǎn)不對比如周轉(zhuǎn)時(shí)間應(yīng)該用完成時(shí)間減到達(dá)時(shí)間但代碼里用了當(dāng)前時(shí)間減到達(dá)時(shí)間。解決先打印中間狀態(tài)比如每次調(diào)度前后的就緒隊(duì)列內(nèi)容、每次頁面訪問后的頁框內(nèi)容。對比手工模擬的結(jié)果找到第一個(gè)不一致的位置。常見做法是在關(guān)鍵函數(shù)入口加printf輸出輸入?yún)?shù)和返回值跑一個(gè)小規(guī)模測試用例逐步縮小范圍。5.4 性能異常算法耗時(shí)遠(yuǎn)超預(yù)期現(xiàn)象小規(guī)模測試正常但數(shù)據(jù)量一大就慢得離譜。原因通常是數(shù)據(jù)結(jié)構(gòu)選擇不當(dāng)。比如優(yōu)先級調(diào)度用鏈表每次遍歷找最優(yōu)頁面置換用數(shù)組每次線性查找文件系統(tǒng)用線性掃描找空閑塊。這些操作在小規(guī)模下看不出來規(guī)模一大就變成瓶頸。解決把線性查找改成有序插入或哈希表。比如優(yōu)先級調(diào)度可以在入隊(duì)時(shí)按優(yōu)先級插入出隊(duì)時(shí)直接取隊(duì)頭空閑塊管理可以用位圖或空閑鏈表。常見做法是先保證正確性再用time命令或clock()函數(shù)測量耗時(shí)確認(rèn)瓶頸在哪個(gè)函數(shù)再針對性優(yōu)化。5.5 環(huán)境差異換一臺(tái)機(jī)器就編譯失敗現(xiàn)象在自己的機(jī)器上能跑換到實(shí)驗(yàn)室機(jī)器或同學(xué)機(jī)器上就報(bào)錯(cuò)。原因編譯器版本、系統(tǒng)架構(gòu)、庫版本不一致。比如代碼里用了gets這種在新版本 GCC 里被移除的函數(shù)或者用了 32 位特有的指針轉(zhuǎn)換。解決在 Makefile 里顯式指定-stdc99或-stdc11避免編譯器默認(rèn)標(biāo)準(zhǔn)不一致。指針轉(zhuǎn)換用intptr_t而不是int。常見做法是在項(xiàng)目根目錄放一個(gè)README寫清楚編譯命令和依賴版本換機(jī)器時(shí)先按 README 跑一遍。6. 怎么在課設(shè)代碼基礎(chǔ)上改出自己的版本6.1 從「能跑」到「能改」先加日志再動(dòng)邏輯很多人拿到代碼包后直接改核心邏輯結(jié)果改完之后跑不起來也說不清楚哪里出了問題。我的習(xí)慣是先在關(guān)鍵路徑上加日志確認(rèn)自己完全理解了代碼的執(zhí)行流程再動(dòng)手改。// 在調(diào)度函數(shù)入口加日志輸出當(dāng)前狀態(tài) PCB* schedule(ReadyQueue *q) { printf([DEBUG] before schedule: queue_count%d, current_time%d\n, q-count, current_time); PCB *p pick_next(q); printf([DEBUG] picked pid%d, remaining%d, priority%d\n, p-pid, p-remaining_time, p-priority); return p; }日志的作用是建立反饋回路。你改了一行代碼跑一遍看日志輸出是否符合預(yù)期。如果不符合你能立刻知道問題出在哪個(gè)環(huán)節(jié)。沒有日志的情況下你只能看最終結(jié)果排查范圍太大。參數(shù)方面日志的粒度要適中。太粗看不到細(xì)節(jié)太細(xì)刷屏。我一般會(huì)在調(diào)度、分配、置換這三個(gè)關(guān)鍵操作上加日志其他輔助函數(shù)不加。日志格式統(tǒng)一用[DEBUG]前綴方便用grep過濾。6.2 改調(diào)度算法從時(shí)間片輪轉(zhuǎn)到多級反饋隊(duì)列如果你要把時(shí)間片輪轉(zhuǎn)改成多級反饋隊(duì)列改動(dòng)集中在三個(gè)地方就緒隊(duì)列的組織、時(shí)間片的分配、進(jìn)程降級邏輯。改動(dòng)點(diǎn)時(shí)間片輪轉(zhuǎn)多級反饋隊(duì)列就緒隊(duì)列單個(gè) FIFO 隊(duì)列多個(gè)優(yōu)先級隊(duì)列時(shí)間片固定值隨優(yōu)先級降低而增大入隊(duì)邏輯直接放隊(duì)尾按優(yōu)先級放入對應(yīng)隊(duì)列降級邏輯無時(shí)間片用完降一級實(shí)現(xiàn)時(shí)先定義多個(gè)隊(duì)列和對應(yīng)的時(shí)間片數(shù)組然后改pick_next函數(shù)從最高優(yōu)先級隊(duì)列開始找找到就取隊(duì)頭如果高優(yōu)先級隊(duì)列為空降級到下一級。進(jìn)程用完時(shí)間片后如果還有剩余執(zhí)行時(shí)間從當(dāng)前隊(duì)列移到下一級隊(duì)列的隊(duì)尾。常見坑是降級邏輯的邊界條件最低一級隊(duì)列的進(jìn)程用完時(shí)間片后是放回原隊(duì)列還是繼續(xù)降級通常是放回原隊(duì)列否則進(jìn)程永遠(yuǎn)無法完成。另一個(gè)坑是時(shí)間片的計(jì)算如果時(shí)間片按 2 的冪次增長要防止溢出。6.3 改頁面置換從 LRU 到 Clock 算法LRU 的缺點(diǎn)是每次訪問都要移動(dòng)頁面開銷大。Clock 算法用循環(huán)隊(duì)列和訪問位來近似 LRU開銷小很多。# Clock 算法循環(huán)掃描訪問位為 1 則清零并跳過為 0 則淘汰 def clock_replace(frames, access_bits, hand, new_page): while True: if access_bits[hand] 0: victim frames[hand] frames[hand] new_page access_bits[hand] 1 hand (hand 1) % len(frames) return victim else: access_bits[hand] 0 hand (hand 1) % len(frames)這段代碼的關(guān)鍵是hand指針和access_bits數(shù)組的配合。每次訪問頁面時(shí)把對應(yīng)的訪問位置 1需要淘汰時(shí)從hand開始掃描遇到訪問位為 1 的就清零并繼續(xù)遇到 0 的就淘汰。這樣被訪問過的頁面有第二次機(jī)會(huì)不會(huì)被立即淘汰。參數(shù)方面access_bits的長度和frames一致初值全為 0。hand初值可以設(shè)為 0。如果你要對比 Clock 和 LRU 的命中率Clock 通常比 LRU 低 5% 到 10%但每次訪問的開銷小很多。在實(shí)驗(yàn)報(bào)告里可以統(tǒng)計(jì)總訪問次數(shù)和總掃描次數(shù)量化這個(gè) trade-off。6.4 驗(yàn)證方法用已知結(jié)果反推實(shí)現(xiàn)是否正確改完代碼后怎么確認(rèn)自己的實(shí)現(xiàn)是對的我的做法是構(gòu)造小規(guī)模測試用例手工算出預(yù)期結(jié)果再和程序輸出對比。比如頁面置換實(shí)驗(yàn)構(gòu)造訪問序列[1, 2, 3, 4, 1, 2, 5, 1, 2, 3]頁框數(shù)為 3手工模擬 FIFO 和 LRU 的命中次數(shù)。FIFO 的命中次數(shù)是 3LRU 的命中次數(shù)是 4。如果程序輸出和這個(gè)不一致說明實(shí)現(xiàn)有問題。調(diào)度實(shí)驗(yàn)同理構(gòu)造 3 個(gè)進(jìn)程到達(dá)時(shí)間分別為 0、1、2burst_time 分別為 5、3、2手工畫出甘特圖算出平均周轉(zhuǎn)時(shí)間。程序輸出和手工結(jié)果對比誤差應(yīng)該在浮點(diǎn)精度范圍內(nèi)。這種驗(yàn)證方法的優(yōu)點(diǎn)是反饋快、定位準(zhǔn)。你不需要等大規(guī)模測試跑完小規(guī)模用例幾秒鐘就能出結(jié)果。缺點(diǎn)是覆蓋不全所以小規(guī)模驗(yàn)證通過后還要跑一組隨機(jī)數(shù)據(jù)檢查有沒有崩潰或異常輸出。6.5 一個(gè)我踩過的坑別在最后一天改核心數(shù)據(jù)結(jié)構(gòu)血淚經(jīng)驗(yàn)課設(shè)截止前一天我把 PCB 的存儲(chǔ)方式從數(shù)組改成鏈表結(jié)果所有依賴下標(biāo)訪問的代碼全部報(bào)錯(cuò)改到凌晨三點(diǎn)才跑通。后來我給自己定了一條規(guī)矩核心數(shù)據(jù)結(jié)構(gòu)在課設(shè)開始的第一周就定下來后面只改算法邏輯不改數(shù)據(jù)結(jié)構(gòu)。如果你確實(shí)需要改數(shù)據(jù)結(jié)構(gòu)先寫一個(gè)適配層把舊接口映射到新結(jié)構(gòu)上保證上層代碼不用動(dòng)。比如從數(shù)組改成鏈表可以保留get_pcb_by_index函數(shù)內(nèi)部遍歷鏈表找到第 i 個(gè)元素。這樣上層代碼不用改你只需要改適配層的實(shí)現(xiàn)。另一個(gè)習(xí)慣是每次改完代碼后立刻git commit寫清楚改了什么。課設(shè)代碼經(jīng)常需要回退到之前的版本沒有版本控制的話你只能靠記憶恢復(fù)很容易出錯(cuò)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取