實(shí)驗(yàn)報(bào)告拆解:從Linux信號機(jī)制到缺頁統(tǒng)計(jì))
簡介西南交通大學(xué)操作系統(tǒng)實(shí)驗(yàn)報(bào)告HXP評分A/95分是面向軟件工程專業(yè)學(xué)生、聚焦Linux環(huán)境與操作系統(tǒng)核心機(jī)制的課程實(shí)踐資料適合需要參考完整實(shí)驗(yàn)流程或撰寫同類報(bào)告的高校學(xué)習(xí)者。報(bào)告以兩個(gè)主線實(shí)驗(yàn)展開實(shí)驗(yàn)一從Linux界面、目錄結(jié)構(gòu)入手通過終端掌握cd、ls、ps、chmod等Shell命令并利用重定向與管道完成命令組合隨后用vi編輯C源程序經(jīng)gcc編譯、gdb調(diào)試熟悉Linux下的程序開發(fā)與排錯方法。實(shí)驗(yàn)二重點(diǎn)剖析/proc虛擬文件系統(tǒng)通過編程讀取cpuinfo、version、stat、uptime、meminfo、loadavg、interrupts等內(nèi)核文件理解系統(tǒng)狀態(tài)與內(nèi)存管理。后續(xù)實(shí)驗(yàn)還涵蓋軟中斷通信、進(jìn)程調(diào)度和線程同步完整展示了從基礎(chǔ)操作到內(nèi)核觀測的實(shí)踐路徑。壓縮包僅含1個(gè)docx文檔共1.48MB報(bào)告帶清晰目錄結(jié)構(gòu)便于直接查閱或打印。該資源已有401人學(xué)習(xí)對準(zhǔn)備Linux實(shí)驗(yàn)、復(fù)習(xí)進(jìn)程調(diào)度或需要高分參考模板的同學(xué)具有較高借鑒價(jià)值。1. 一份95分的操作系統(tǒng)實(shí)驗(yàn)報(bào)告比代碼更重要的是過程記錄做操作系統(tǒng)實(shí)驗(yàn)最怕的不是寫不出代碼而是寫得出來卻說不清楚設(shè)計(jì)思路和實(shí)驗(yàn)現(xiàn)象。這份在某高校胡老師班拿到95分的實(shí)驗(yàn)報(bào)告覆蓋了Linux環(huán)境與C調(diào)試、軟中斷通信、進(jìn)程調(diào)度、線程同步、內(nèi)存管理、系統(tǒng)調(diào)用、缺頁統(tǒng)計(jì)七個(gè)實(shí)驗(yàn)正好是大多數(shù)高校操作系統(tǒng)課的核心實(shí)驗(yàn)矩陣。我拆完這份資源后最直接的感受是它值錢的地方不在那些現(xiàn)成的C代碼而在每份實(shí)驗(yàn)里“怎么描述現(xiàn)象、怎么解釋結(jié)果、怎么踩坑再爬出來”的記錄方式。如果你正在補(bǔ)實(shí)驗(yàn)報(bào)告、準(zhǔn)備課程設(shè)計(jì)素材或者想看看95分的學(xué)生是怎么組織實(shí)驗(yàn)文本的這份報(bào)告可以作為一份完整的參照模板。代碼可以重寫但那種對實(shí)驗(yàn)現(xiàn)象的觀察深度直接抄是抄不來的。2. 報(bào)告的骨架拆解七個(gè)實(shí)驗(yàn)覆蓋了Linux操作系統(tǒng)的哪些核心機(jī)制2.1 從目錄結(jié)構(gòu)看課程設(shè)計(jì)的完整鏈路這份報(bào)告從實(shí)驗(yàn)一到實(shí)驗(yàn)七鏈路設(shè)計(jì)有明顯遞進(jìn)關(guān)系。實(shí)驗(yàn)一和觀察Linux是基礎(chǔ)層解決的是“會不會用Linux”的問題——目錄結(jié)構(gòu)、基本Shell命令、gcc編譯、vim編輯、/proc文件系統(tǒng)的基本讀取實(shí)驗(yàn)二和三進(jìn)入進(jìn)程層用軟中斷通信和進(jìn)程調(diào)度理解“進(jìn)程是怎么被創(chuàng)建、通信和切換的”實(shí)驗(yàn)四到六延伸到線程同步、內(nèi)存管理和系統(tǒng)調(diào)用屬于并發(fā)與資源管理的范疇最后的缺頁次數(shù)統(tǒng)計(jì)實(shí)驗(yàn)則把視角拉到內(nèi)存管理的底層用/proc/stat讀取缺頁信息來驗(yàn)證理論。這個(gè)順序和你自己學(xué)校的實(shí)驗(yàn)安排大概率能對上。如果你們學(xué)校的實(shí)驗(yàn)順序不同也不用擔(dān)心報(bào)告的每個(gè)實(shí)驗(yàn)是相對獨(dú)立的你可以直接挑對應(yīng)的那章來看不必從頭按順序讀。我拆這份報(bào)告時(shí)是按“能直接用”的標(biāo)準(zhǔn)來篩選的——也就是說每個(gè)實(shí)驗(yàn)的代碼、編譯命令、實(shí)驗(yàn)結(jié)果描述方式都要能單獨(dú)抽出來復(fù)用到你自己的報(bào)告里。2.2 評分高的關(guān)鍵在哪個(gè)人聲明與問題記錄報(bào)告的序言后面緊跟一段“個(gè)人聲明”很簡短就說了一件事報(bào)告里用到的命令統(tǒng)一用粗體加下劃線標(biāo)注方便閱覽。這個(gè)細(xì)節(jié)看起來不起眼其實(shí)是給評閱老師的第一印象分。95分的報(bào)告通常不是每一個(gè)實(shí)驗(yàn)都完美但整份文檔的格式一致性、可讀性、以及問題記錄的誠實(shí)度拉高了整體評價(jià)。報(bào)告中每一章結(jié)尾幾乎都有“實(shí)驗(yàn)總結(jié)”部分里面寫的不只是成果還有具體的失敗記錄。比如實(shí)驗(yàn)一里寫了“配置沒有問題但網(wǎng)絡(luò)連接一直失敗”然后記錄了怎么查找博客、修復(fù)網(wǎng)絡(luò)順帶學(xué)習(xí)了三種網(wǎng)絡(luò)模式的區(qū)別。這種寫法比單純寫“我學(xué)會了Linux目錄結(jié)構(gòu)”有價(jià)值得多因?yàn)槔蠋熌軓奈淖掷锟吹秸鎸?shí)的動手過程。你在寫自己的報(bào)告時(shí)建議每章末尾都留出一段“問題與解決”哪怕只寫兩條也比空泛的“通過本次實(shí)驗(yàn)我掌握了”要有說服力。2.3 環(huán)境選型用什么Linux跑這套實(shí)驗(yàn)最省事報(bào)告全程在虛擬機(jī)里完成涉及的環(huán)境工具包括vim、gcc、gdb、以及遠(yuǎn)程傳文件的xftp。從報(bào)告里觀察/proc/version的結(jié)果來看實(shí)驗(yàn)環(huán)境是Red Hat系列的Linux發(fā)行版內(nèi)核版本3.10.0。這個(gè)內(nèi)核版本對應(yīng)的系統(tǒng)通常自帶gcc 4.8.5對于實(shí)驗(yàn)中的fork、signal、pthread這些接口來說完全夠用。如果你現(xiàn)在要從零復(fù)現(xiàn)這套實(shí)驗(yàn)我建議直接用Ubuntu 20.04以上的發(fā)行版理由有兩點(diǎn)一是gcc版本更新編譯時(shí)的警告信息更友好二是apt安裝build-essential一條命令就能補(bǔ)齊工具鏈。當(dāng)然要是你們學(xué)校指定了某個(gè)實(shí)驗(yàn)環(huán)境以學(xué)校要求為準(zhǔn)。虛擬機(jī)的網(wǎng)絡(luò)模式報(bào)告里也提到了三種——橋接、NAT、主機(jī)模式并做了個(gè)人理解式的總結(jié)橋接容易IP沖突、NAT既能訪問外網(wǎng)又不暴露虛擬機(jī)、主機(jī)模式完全隔離。對于實(shí)驗(yàn)場景NAT是最省心的選擇。提示報(bào)告中有一段關(guān)于jiffies的理解——Linux內(nèi)核用全局變量jiffies記錄節(jié)拍時(shí)間開機(jī)時(shí)讀取RTC作為基準(zhǔn)這個(gè)思路和Windows的滴答計(jì)數(shù)是同一類機(jī)制。寫報(bào)告時(shí)能把這類課外延伸寫進(jìn)去是加分的。3. 軟中斷通信實(shí)驗(yàn)把信號機(jī)制落到兩份可復(fù)現(xiàn)的C代碼3.1 實(shí)驗(yàn)要求與信號機(jī)制的關(guān)系軟中斷通信實(shí)驗(yàn)是這份報(bào)告里代碼價(jià)值最高的部分。實(shí)驗(yàn)要求分兩種場景場景一是父進(jìn)程創(chuàng)建兩個(gè)子進(jìn)程然后通過Ctrl\發(fā)送SIGQUIT給父進(jìn)程由父進(jìn)程再向兩個(gè)子進(jìn)程發(fā)送自定義信號終止它們場景二是父進(jìn)程創(chuàng)建子進(jìn)程、子進(jìn)程再創(chuàng)建孫進(jìn)程信號到達(dá)后按孫進(jìn)程、子進(jìn)程、父進(jìn)程的順序依次終止。要理解這個(gè)實(shí)驗(yàn)先要分清兩個(gè)概念SIGQUIT和信號16/17。SIGQUIT是終端組合鍵Ctrl\產(chǎn)生的信號默認(rèn)動作是終止進(jìn)程并生成核心轉(zhuǎn)儲而信號16和17在Linux下沒有標(biāo)準(zhǔn)定義屬于用戶自定義信號實(shí)驗(yàn)里用它來模擬父子進(jìn)程間的“軟中斷”通信。流程就是父進(jìn)程收到SIGQUIT后調(diào)用kill函數(shù)向子進(jìn)程發(fā)送16/17信號子進(jìn)程收到后設(shè)置wait_mark為0退出等待循環(huán)。3.2 第一個(gè)場景父進(jìn)程創(chuàng)建兩個(gè)子進(jìn)程的代碼這是報(bào)告里實(shí)驗(yàn)二的第一個(gè)核心代碼我重新組織了注釋和說明讓它更便于你直接編譯運(yùn)行#include stdio.h #include stdlib.h #include signal.h #include unistd.h void waiting(); void stop(); int wait_mark; int main() { int p1, p2; while ((p1 fork()) -1) ; // 創(chuàng)建子進(jìn)程p1失敗則忙等重試 sleep(1); if (p1 0) // 父進(jìn)程分支 { printf(parent process is %d \n, getpid()); while ((p2 fork()) -1) ; // 創(chuàng)建子進(jìn)程p2 if (p2 0) // 仍然是父進(jìn)程分支 { wait_mark 1; signal(SIGQUIT, stop); // 注冊SIGQUIT處理函數(shù) waiting(); // 等待用戶按下Ctrl\ kill(p1, 16); // 給p1發(fā)送信號16 kill(p2, 17); // 給p2發(fā)送信號17 wait(0); // 回收子進(jìn)程 wait(0); printf(\n parent process %d is killed!\n, getpid()); exit(0); } else { // 子進(jìn)程p2分支 printf(child P2 process %d is created by %d \n, getpid(), getppid()); signal(SIGQUIT, SIG_IGN); // 忽略SIGQUIT wait_mark 1; signal(17, stop); // 注冊信號17處理函數(shù) waiting(); lockf(1, 1, 0); printf(\n child P2 process %d is killed by parent %d , getpid(), getppid()); lockf(1, 0, 0); } } else { // 子進(jìn)程p1分支 printf(child P1 process %d is created by %d \n, getpid(), getppid()); signal(SIGQUIT, SIG_IGN); // 忽略SIGQUIT wait_mark 1; signal(16, stop); // 注冊信號16處理函數(shù) waiting(); lockf(1, 1, 0); printf(\n child P1 process %d is killed by parent %d , getpid(), getppid()); lockf(1, 0, 0); } return 0; } void waiting() { while (wait_mark ! 0) ; // 忙等直到wait_mark被信號處理函數(shù)置0 } void stop() { wait_mark 0; }這份代碼的核心邏輯不復(fù)雜但有三個(gè)地方需要特別說明。第一是fork后的分支判斷。fork()返回0表示當(dāng)前在子進(jìn)程中返回正整數(shù)表示當(dāng)前在父進(jìn)程中返回-1表示創(chuàng)建失敗。代碼里用了while循環(huán)包裹fork目的是防止進(jìn)程數(shù)達(dá)到系統(tǒng)上限時(shí)創(chuàng)建失敗忙等重試直到成功。sleep(1)放在第一個(gè)fork后面是為了讓子進(jìn)程有時(shí)間完成后續(xù)的signal注冊避免父進(jìn)程信號發(fā)過去時(shí)子進(jìn)程還沒準(zhǔn)備好。第二是signal(SIGQUIT, SIG_IGN)這一段。子進(jìn)程必須忽略SIGQUIT否則用戶按下Ctrl\時(shí)父子進(jìn)程會一起被系統(tǒng)殺死就看不到實(shí)驗(yàn)要求的“父進(jìn)程轉(zhuǎn)發(fā)信號終止子進(jìn)程”這個(gè)現(xiàn)象了。父進(jìn)程負(fù)責(zé)接收SIGQUIT子進(jìn)程只接收來自父進(jìn)程的信號16或17這是整個(gè)實(shí)驗(yàn)的信號流向設(shè)計(jì)。第三是lockf的使用。兩個(gè)子進(jìn)程都可能在waiting()退出后調(diào)用printf輸出結(jié)束信息如果不加鎖兩個(gè)進(jìn)程的輸出會交錯在一起終端顯示亂掉。lockf(1, 1, 0)是對標(biāo)準(zhǔn)輸出加寫鎖lockf(1, 0, 0)是解鎖保證一個(gè)進(jìn)程的輸出是原子的。3.3 編譯與運(yùn)行驗(yàn)證編譯命令很簡單gcc直接編譯即可gcc -o sig_test1 sig_test1.c ./sig_test1程序啟動后會輸出父進(jìn)程和兩個(gè)子進(jìn)程的PID然后卡住——這是父進(jìn)程在waiting()里忙等。此時(shí)按下Ctrl\終端會顯示^\隨后看到兩個(gè)子進(jìn)程依次打印被終止的信息最后父進(jìn)程打印退出信息。需要提醒的是代碼里的信號編號16和17在部分新版本Linux上可能已經(jīng)被系統(tǒng)占用。signal(16, stop)在某些架構(gòu)上可能注冊失敗。如果你在運(yùn)行的發(fā)行版上遇到“這個(gè)信號無法使用”的情況可以把16和17換成SIGUSR1和SIGUSR2這是系統(tǒng)保留給用戶程序使用的標(biāo)準(zhǔn)信號語義上更規(guī)范。這是我拆這份報(bào)告后建議你做的一個(gè)改進(jìn)點(diǎn)。3.4 第二個(gè)場景三層進(jìn)程父、子、孫的終止順序第二段代碼做的是三進(jìn)程傳遞關(guān)鍵點(diǎn)在于子進(jìn)程既接收父進(jìn)程的信號又轉(zhuǎn)發(fā)信號給自己的子進(jìn)程。流程是父進(jìn)程收到SIGQUIT后向子進(jìn)程發(fā)信號16子進(jìn)程的信號處理函數(shù)執(zhí)行wait_mark0子進(jìn)程從waiting()退出后再向?qū)O進(jìn)程發(fā)信號16等孫進(jìn)程結(jié)束后自己再退出。這樣從用戶視角看按下Ctrl\依次看到孫進(jìn)程、子進(jìn)程、父進(jìn)程的結(jié)束信息。這段代碼我再貼一次核心結(jié)構(gòu)因?yàn)楹芏嗳藭凇罢l的wait_mark先被置0”這個(gè)問題上繞暈// 父進(jìn)程分支 wait_mark 1; signal(SIGQUIT, stop); waiting(); kill(p1, 16); // 向子進(jìn)程發(fā)送信號16 wait(0); // 等待子進(jìn)程退出 // 子進(jìn)程分支 signal(SIGQUIT, SIG_IGN); signal(16, stop); waiting(); kill(p1, 16); // 子進(jìn)程向?qū)O進(jìn)程發(fā)送信號16 wait(0); // 等待孫進(jìn)程退出 // 孫進(jìn)程分支 signal(SIGQUIT, SIG_IGN); signal(16, stop); waiting(); // 打印結(jié)束信息退出這段代碼里最容易忽略的是每個(gè)進(jìn)程在waiting()退出后的執(zhí)行順序。子進(jìn)程的kill(p1, 16)調(diào)用和孫進(jìn)程的waiting()退出之間存在一個(gè)時(shí)間窗。因?yàn)閷O進(jìn)程在收到信號16后才會把wait_mark置0所以子進(jìn)程kill之后必須調(diào)用wait(0)阻塞等待孫進(jìn)程結(jié)束。如果不加wait(0)子進(jìn)程會立刻繼續(xù)執(zhí)行printf和exit終端上看到的順序就可能變成“子進(jìn)程先結(jié)束、孫進(jìn)程再結(jié)束”和實(shí)驗(yàn)要求不符。4. 進(jìn)程調(diào)度與線程同步從報(bào)告看驗(yàn)證環(huán)節(jié)的設(shè)計(jì)思路4.1 調(diào)度實(shí)驗(yàn)的驗(yàn)證思路FCFS與RR不能只看代碼報(bào)告里的實(shí)驗(yàn)三是進(jìn)程調(diào)度實(shí)驗(yàn)實(shí)驗(yàn)步驟寫得比較簡略大致是創(chuàng)建工程后驗(yàn)證FCFS先來先服務(wù)與RR時(shí)間片輪轉(zhuǎn)兩種策略。很多同學(xué)做這個(gè)實(shí)驗(yàn)時(shí)有一個(gè)誤區(qū)代碼跑出結(jié)果就算做完。但從這份95分報(bào)告的風(fēng)格來看驗(yàn)證環(huán)節(jié)比代碼本身更值得花心思。FCFS和RR的區(qū)別要落到你畫的進(jìn)程執(zhí)行順序圖上。假設(shè)有三個(gè)進(jìn)程到達(dá)時(shí)間分別是0、1、2服務(wù)時(shí)間分別是4、3、2FCFS下進(jìn)程按到達(dá)順序執(zhí)行總完成時(shí)間依次是4、7、9RR下如果時(shí)間片為2進(jìn)程輪流占用CPU執(zhí)行順序會完全不一樣。報(bào)告里沒有貼具體的進(jìn)程數(shù)據(jù)但你在復(fù)現(xiàn)時(shí)應(yīng)該自己構(gòu)造好這樣一組數(shù)據(jù)并在報(bào)告里畫出調(diào)度時(shí)序圖。調(diào)度實(shí)驗(yàn)的代碼通常是模擬實(shí)現(xiàn)和內(nèi)核里的真實(shí)調(diào)度器沒有直接關(guān)系。報(bào)告里的做法是在用戶態(tài)用C語言實(shí)現(xiàn)一個(gè)調(diào)度器的模擬框架定義PCB結(jié)構(gòu)體、就緒隊(duì)列、時(shí)間片計(jì)數(shù)器然后按不同策略跑同一組進(jìn)程數(shù)據(jù)輸出完成時(shí)間、周轉(zhuǎn)時(shí)間、帶權(quán)周轉(zhuǎn)時(shí)間。這個(gè)模擬過程的價(jià)值在于你能直觀看到RR策略下短進(jìn)程沒有被長時(shí)間阻塞、但上下文切換次數(shù)上升了這個(gè)典型 trade-off。4.2 線程同步實(shí)驗(yàn)鎖定與信號量的標(biāo)準(zhǔn)寫法實(shí)驗(yàn)四是線程同步實(shí)驗(yàn)按報(bào)告的目錄結(jié)構(gòu)看實(shí)驗(yàn)內(nèi)容圍繞線程的同步互斥展開。線程同步和進(jìn)程同步最大的區(qū)別在于線程共享同一地址空間不需要像進(jìn)程那樣通過信號或管道來傳遞數(shù)據(jù)直接用共享變量配合鎖、信號量或條件變量就能完成。這里有一個(gè)常見的翻車點(diǎn)用while循環(huán)包裹wait操作是推薦寫法但很多人會隨手寫成if。兩者的區(qū)別是if只在被喚醒時(shí)檢查一次條件而while會在每次被喚醒后重新檢查條件是否仍然滿足。多線程環(huán)境下可能發(fā)生虛假喚醒或者多個(gè)消費(fèi)者被同時(shí)喚醒后只有一個(gè)能拿到資源如果沒有用while重新檢查條件就會產(chǎn)生數(shù)據(jù)不一致。這個(gè)級別的分析寫進(jìn)實(shí)驗(yàn)報(bào)告是明顯加分的內(nèi)容。4.3 內(nèi)存管理與缺頁統(tǒng)計(jì)實(shí)驗(yàn)/proc是觀測窗口實(shí)驗(yàn)五是內(nèi)存管理實(shí)驗(yàn)通過/proc文件系統(tǒng)觀察系統(tǒng)的meminfo、loadavg、interrupts、filesystem等信息實(shí)驗(yàn)七是缺頁次數(shù)統(tǒng)計(jì)核心是讀取/proc/stat里的缺頁相關(guān)字段。這兩個(gè)實(shí)驗(yàn)的關(guān)鍵是理解/proc下每個(gè)文件對應(yīng)的內(nèi)核數(shù)據(jù)來源。比如/proc/meminfo里的MemTotal、MemFree、Buffers、Cached字段直接反映了系統(tǒng)物理內(nèi)存的使用分布/proc/stat里的pgfault和pgmajfault字段前者是普通缺頁次數(shù)包含從頁面緩存中恢復(fù)的后者是主要缺頁次數(shù)需要磁盤I/O兩者之間的差距能反映內(nèi)存回收和頁面緩存的效率。讀取這些數(shù)據(jù)的C程序本質(zhì)上就是打開文件、讀字符串、按空格解析、打印輸出。報(bào)告里強(qiáng)調(diào)“不能粘貼運(yùn)行結(jié)果”意思是必須用自己的代碼去讀取然后對數(shù)值做解釋。你要是自己寫的話一個(gè)典型的解析流程是#include stdio.h #include stdlib.h #include string.h int main() { FILE *fp; char line[256]; unsigned long pgfault 0, pgmajfault 0; fp fopen(/proc/stat, r); if (!fp) { perror(open /proc/stat); return -1; } while (fgets(line, sizeof(line), fp)) { if (strncmp(line, pgfault, 7) 0) { sscanf(line, pgfault %lu, pgfault); } else if (strncmp(line, pgmajfault, 10) 0) { sscanf(line, pgmajfault %lu, pgmajfault); } } fclose(fp); printf(pgfault: %lu\n, pgfault); printf(pgmajfault:%lu\n, pgmajfault); return 0; }這段代碼做的事情很直接打開/proc/stat文件逐行讀取用strncmp匹配以pgfault和pgmajfault開頭的行sscanf提取數(shù)值最終打印。實(shí)測時(shí)發(fā)現(xiàn)pgfault的數(shù)值增長很快而pgmajfault的增長相對緩慢這就是頁面緩存命中率的體現(xiàn)——大部分缺頁都能從用戶空間的頁面緩存里恢復(fù)不需要真正發(fā)起磁盤I/O。注意/proc/stat里的pgfault和pgmajfault是系統(tǒng)累計(jì)值不是某個(gè)進(jìn)程的缺頁數(shù)。想要統(tǒng)計(jì)指定進(jìn)程的缺頁次數(shù)需要讀取/proc/[pid]/stat里的minflt和majflt字段這個(gè)區(qū)別寫報(bào)告時(shí)容易搞混。報(bào)告中實(shí)驗(yàn)七是按“系統(tǒng)缺頁次數(shù)”做的所以用/proc/stat是對的。5. 避坑指南六個(gè)能讓你重寫報(bào)告的常見問題5.1 fork代碼從Windows復(fù)制到Linux后無法編譯現(xiàn)象在Windows的IDE里寫好C代碼復(fù)制進(jìn)Linux后用gcc編譯報(bào)錯fork undeclared或signal not declared。原因Windows的C標(biāo)準(zhǔn)庫沒有UNIX的POSIX接口fork、signal這些函數(shù)是Linux系統(tǒng)調(diào)用Windows環(huán)境下IDE的庫根本不包含它們代碼里雖然寫了#include unistd.h和#include signal.h但編譯器找不到對應(yīng)實(shí)現(xiàn)。解決直接在Linux虛擬機(jī)里用vim編寫或粘貼代碼然后在Linux的終端里完成編譯。如果你實(shí)在習(xí)慣用Windows編輯器寫代碼用xftp之類的工具傳進(jìn)虛擬機(jī)再編譯但不要把Windows下的編譯錯誤當(dāng)成Linux下的行為來排查。報(bào)告中提到的xftp傳文件方式對于傳大量小文件確實(shí)比反復(fù)復(fù)制粘貼省事。5.2 按下Ctrl\沒有反應(yīng)程序繼續(xù)卡住現(xiàn)象程序運(yùn)行后按Ctrl\終端沒有任何反應(yīng)進(jìn)程不退出。原因兩種情況最常見。第一種是子進(jìn)程沒有忽略SIGQUIT導(dǎo)致Ctrl\直接終止了所有收到該信號的進(jìn)程你還沒看到輸出終端就回到Shell了第二種是父進(jìn)程注冊了SIGQUIT處理函數(shù)但waiting()里的wait_mark變量被編譯器優(yōu)化了。解決確認(rèn)子進(jìn)程分支里調(diào)用了signal(SIGQUIT, SIG_IGN)父進(jìn)程分支里調(diào)用了signal(SIGQUIT, stop)。如果確認(rèn)沒錯把wait_mark聲明為volatile int防止編譯器把它優(yōu)化到寄存器里導(dǎo)致信號處理函數(shù)改了值、主循環(huán)卻感知不到。這是比較隱蔽的坑通常在開啟-O2優(yōu)化后暴露。5.3 兩個(gè)子進(jìn)程的輸出信息混在一起現(xiàn)象實(shí)驗(yàn)一里P1和P2的結(jié)束信息同時(shí)出現(xiàn)在終端字符交錯在一起無法辨認(rèn)。原因兩個(gè)子進(jìn)程同時(shí)從waiting()退出同時(shí)調(diào)用printf輸出到標(biāo)準(zhǔn)輸出。printf雖然有行緩沖但多個(gè)進(jìn)程同時(shí)操作同一文件描述符沒有原子性。解決用lockf對標(biāo)準(zhǔn)輸出加文件鎖。lockf(1, 1, 0)對文件描述符1stdout加寫鎖lockf(1, 0, 0)解鎖。同一時(shí)刻只有一個(gè)進(jìn)程能持有鎖輸出就變成原子操作了。這也是報(bào)告中代碼里lockf出現(xiàn)的原因在復(fù)現(xiàn)或講解時(shí)不要省略它。5.4 gdb調(diào)試時(shí)斷點(diǎn)不生效現(xiàn)象用gdb設(shè)置斷點(diǎn)后運(yùn)行程序斷點(diǎn)沒有被命中程序直接在終端跑完了。原因最常見的情況是編譯時(shí)沒有加-g參數(shù)。gdb依賴調(diào)試符號表來關(guān)聯(lián)源代碼行號和機(jī)器指令地址沒有調(diào)試信息斷點(diǎn)自然無法設(shè)置。解決編譯時(shí)使用gcc -g -o test test.c。如果還需要更多調(diào)試信息可以用gcc -g3 -O0-g3包含宏定義信息-O0關(guān)閉優(yōu)化避免調(diào)試時(shí)變量被優(yōu)化掉。另外如果你用的是Code::Blocks這類IDE調(diào)試要確認(rèn)項(xiàng)目的編譯選項(xiàng)里開啟了“生成調(diào)試信息”這一項(xiàng)。5.5 缺頁統(tǒng)計(jì)實(shí)驗(yàn)結(jié)果不穩(wěn)定現(xiàn)象連續(xù)兩次運(yùn)行缺頁統(tǒng)計(jì)程序打印出的數(shù)值差異巨大甚至第二次運(yùn)行比第一次運(yùn)行數(shù)值還小看起來不合理。原因/proc/stat里展示的是從系統(tǒng)啟動以來的累計(jì)值每次讀取都是一個(gè)更大的數(shù)字不會變小。但如果你讀取的是某個(gè)特定進(jìn)程的/proc/[pid]/stat字段進(jìn)程每次運(yùn)行的minflt和majflt都會不同因?yàn)樗鼈內(nèi)Q于程序運(yùn)行時(shí)的訪問模式、是否有文件I/O、系統(tǒng)的頁面緩存命中率等因素。差異大是正常的關(guān)鍵在你怎么解讀。解決報(bào)告里寫結(jié)果時(shí)不要只寫一個(gè)時(shí)間點(diǎn)的靜態(tài)值而是連續(xù)采樣幾次記錄數(shù)據(jù)的變化趨勢再結(jié)合當(dāng)前系統(tǒng)的負(fù)載情況做解釋。比如連續(xù)讀取5秒內(nèi)pgfault的增長量可以估算系統(tǒng)每秒的缺頁頻率這個(gè)頻率如果在穩(wěn)定區(qū)間波動就能證明程序的觀測方法是有效的。5.6 報(bào)告里貼了大量代碼卻沒有結(jié)果解讀現(xiàn)象實(shí)驗(yàn)報(bào)告里代碼占了整頁實(shí)驗(yàn)結(jié)果只貼了一張截圖沒有任何文字說明。原因這是最常見的丟分點(diǎn)。老師評實(shí)驗(yàn)報(bào)告不是看你會不會復(fù)制代碼而是看你能不能解釋代碼行為和實(shí)驗(yàn)現(xiàn)象。一張沒頭沒尾的截圖換誰都看不懂你想表達(dá)什么。解決至少寫三部分內(nèi)容。第一運(yùn)行環(huán)境的簡要說明包括Linux版本、內(nèi)核版本第二輸入數(shù)據(jù)是什么比如調(diào)度實(shí)驗(yàn)中三個(gè)進(jìn)程的到達(dá)時(shí)間和服務(wù)時(shí)間第三輸出結(jié)果怎么解讀比如為什么P2在RR策略下比FCFS策略下完成得更早這個(gè)現(xiàn)象反映了什么調(diào)度特性。這也是這份95分報(bào)告里做得最好的地方——每個(gè)實(shí)驗(yàn)結(jié)果后都有一小段“結(jié)果分析”單獨(dú)解釋圖中現(xiàn)象發(fā)生的原因。6. 驗(yàn)證方法論把一份課堂報(bào)告變成你自己的能力憑證拿到這份資源后怎么判斷你已經(jīng)“吃透”了而不是“抄完”了我習(xí)慣用一組快速驗(yàn)證來檢測你可以照著過一遍。第一項(xiàng)驗(yàn)證是軟中斷實(shí)驗(yàn)的改造能力。原代碼用信號16和17做通信你把它改成用SIGUSR1和SIGUSR2重新編譯運(yùn)行確認(rèn)三種進(jìn)程的終止順序和原來一致。能改得動說明你理解了信號編號只是觸發(fā)標(biāo)識真正決定行為的是每個(gè)進(jìn)程注冊的處理函數(shù)。第二項(xiàng)驗(yàn)證是/proc讀取程序的擴(kuò)展能力。寫一個(gè)與報(bào)告功能等價(jià)但代碼結(jié)構(gòu)完全不同的C程序比如用一次性fread讀取整個(gè)/proc/stat文件再解析而不是fgets逐行掃描。能改用另一種方式實(shí)現(xiàn)相同功能說明你對文件I/O和字符串解析有判斷力而不是只會背模板。第三項(xiàng)驗(yàn)證是敘述視角的轉(zhuǎn)換。把報(bào)告里的一段實(shí)驗(yàn)結(jié)果用自己的話重新描述一遍不要看原文。如果寫出來的文字仍然包含以下要素輸入條件、執(zhí)行過程、輸出結(jié)果、原因分析說明你已經(jīng)掌握了實(shí)驗(yàn)報(bào)告的基本結(jié)構(gòu)。這個(gè)能力比代碼本身更通用后面的課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)都能復(fù)用。我自己拆這種課程資源時(shí)有個(gè)習(xí)慣永遠(yuǎn)先看失敗記錄和問題記錄再看成功代碼。因?yàn)榇a只能說明一個(gè)版本的功能而問題記錄能讓你避開未來可能踩的同一個(gè)坑。這份報(bào)告里記錄的網(wǎng)絡(luò)連接問題、Windows與Linux環(huán)境差異、jiffies概念的理解恰恰是實(shí)際操作中真正會浪費(fèi)半天時(shí)間的點(diǎn)。從那以后我每次拿到類似的課程資源都強(qiáng)制自己先摘出“遇到的問題”那一頁再決定要不要花時(shí)間看代碼。希望這份拆解也幫到你至少可以少走一段彎路。本文還有配套的精品資源點(diǎn)擊獲取