境到軟中斷:一份95分操作系統(tǒng)實(shí)驗(yàn)報(bào)告全解析)
簡(jiǎn)介西南交通大學(xué)操作系統(tǒng)實(shí)驗(yàn)報(bào)告完整收錄胡曉鵬老師課程的六個(gè)實(shí)驗(yàn)涵蓋Linux環(huán)境與Shell命令、C程序編譯調(diào)試、/proc內(nèi)核狀態(tài)觀測(cè)、軟中斷通信、進(jìn)程調(diào)度及線程同步等主題適合正在學(xué)習(xí)操作系統(tǒng)或Linux編程的本科生對(duì)照參考。報(bào)告基于2020—2021學(xué)年軟件工程專業(yè)真實(shí)作業(yè)評(píng)分達(dá)95分不僅給出每次實(shí)驗(yàn)的目的、步驟與總結(jié)還詳細(xì)展示了cd、ls、ps、chmod等命令用法以及vi、gcc、gdb工具鏈操作和cpuinfo、meminfo、loadavg、interrupts等內(nèi)核文件的編程觀測(cè)方法。壓縮包內(nèi)共1個(gè)docx文檔約1.48MB目錄結(jié)構(gòu)按實(shí)驗(yàn)編號(hào)清晰劃分方便直接查閱或借鑒格式。該資源已有401人學(xué)習(xí)對(duì)需要完成同類實(shí)驗(yàn)、撰寫實(shí)驗(yàn)報(bào)告或理解Linux內(nèi)核抽象的同學(xué)具有較高參考價(jià)值可直接作為實(shí)驗(yàn)流程梳理、命令細(xì)節(jié)核對(duì)和報(bào)告排版范本使用。1. 一份 95 分的操作系統(tǒng)實(shí)驗(yàn)報(bào)告從讀懂 Linux 到跑通軟中斷某高校軟件工程專業(yè)的操作系統(tǒng)課設(shè)里有一份評(píng)分 95 的實(shí)驗(yàn)報(bào)告覆蓋了 Linux 環(huán)境與 C 語(yǔ)言調(diào)試、/proc 文件系統(tǒng)觀測(cè)、軟中斷通信、進(jìn)程調(diào)度、線程同步、內(nèi)存管理、系統(tǒng)調(diào)用和缺頁(yè)統(tǒng)計(jì)八個(gè)實(shí)驗(yàn)。這份報(bào)告值得參考的點(diǎn)不在于每個(gè)實(shí)驗(yàn)都跑通了而在于它把「代碼演示」和「機(jī)制理解」放在了一起——比如用 Ctrl\ 觸發(fā) SIGQUIT 來(lái)觀察父子進(jìn)程的終止順序比如通過(guò) /proc/cpuinfo 和 /proc/meminfo 去對(duì)應(yīng)課堂上講的內(nèi)核變量。對(duì)于正在做操作系統(tǒng)課設(shè)或準(zhǔn)備 OS 方向面試的人來(lái)說(shuō)這套實(shí)驗(yàn)編排剛好覆蓋了進(jìn)程管理、內(nèi)存管理、信號(hào)機(jī)制、系統(tǒng)調(diào)用四大主線。本文就按這份報(bào)告的結(jié)構(gòu)拆開(kāi)講從環(huán)境準(zhǔn)備到 /proc 觀測(cè)再到軟中斷和進(jìn)程調(diào)度最后落到內(nèi)存管理、系統(tǒng)調(diào)用和缺頁(yè)統(tǒng)計(jì)并給出驗(yàn)收和答辯的方法。如果你是照著課設(shè)要求逐項(xiàng)做可以直接按這個(gè)順序推進(jìn)。2. 實(shí)驗(yàn)環(huán)境準(zhǔn)備Linux 目錄結(jié)構(gòu)、Shell 命令與 gcc/gdb 調(diào)試鏈操作系統(tǒng)實(shí)驗(yàn)的第一步永遠(yuǎn)是環(huán)境。報(bào)告中的實(shí)驗(yàn) 1 花費(fèi)了大量篇幅在「熟悉 Linux 界面與目錄結(jié)構(gòu)」和「在終端里跑通一個(gè) C 程序」上。這一步看起來(lái)基礎(chǔ)實(shí)際決定了后面七個(gè)實(shí)驗(yàn)?zāi)懿荒茼樌七M(jìn)尤其是 fork、signal 這類依賴進(jìn)程機(jī)制的程序必須在一個(gè)能穩(wěn)定運(yùn)行的環(huán)境里調(diào)試。2.1 Linux 目錄結(jié)構(gòu)先搞清楚東西放在哪報(bào)告里記錄了一個(gè)很直觀的觀察Linux 和 Windows 的目錄組織方式完全不同。Windows 用盤符C:、D:區(qū)分存儲(chǔ)區(qū)域Linux 則是單一根目錄/用正斜杠/分割路徑。初次接觸時(shí)容易混淆的點(diǎn)在于/bin存放最常用的命令ls、cp、mv/home是用戶主目錄的根/etc保存系統(tǒng)配置文件/proc是虛擬文件系統(tǒng)運(yùn)行時(shí)的內(nèi)核信息都在這里。報(bào)告特別標(biāo)注了/proc下的數(shù)字目錄——每個(gè)數(shù)字對(duì)應(yīng)一個(gè)正在運(yùn)行的進(jìn)程 PID里面是進(jìn)程的運(yùn)行時(shí)信息。我把這些目錄的用途整理成一張速查表方便對(duì)照目錄路徑主要用途實(shí)驗(yàn)中的使用場(chǎng)景/bin常用命令存放處ls、cp、mv的執(zhí)行文件/home用戶主目錄存放實(shí)驗(yàn)代碼文件/proc虛擬文件系統(tǒng)內(nèi)核狀態(tài)接口實(shí)驗(yàn) 1、實(shí)驗(yàn) 7 讀取系統(tǒng)信息/tmp臨時(shí)文件編譯中間文件的臨時(shí)輸出/lostfound異常關(guān)機(jī)后的文件恢復(fù)區(qū)一般用不到做實(shí)驗(yàn)時(shí)一個(gè)典型的操作流程是用cd進(jìn)入工作目錄用ls確認(rèn)文件存在用mv重命名文件。報(bào)告里提到一個(gè)細(xì)節(jié)想修改已編輯好的文件名在終端用mv agrc.c oslab.1.c即可如果用 vim 編輯也可以在保存時(shí)用:w new_filename直接另存為新文件。兩個(gè)方法都能用前者是 Shell 層的操作后者是編輯器內(nèi)的操作場(chǎng)景不同選擇不同。2.2 Shell 命令與管道重定向不只是敲命令報(bào)告在實(shí)驗(yàn) 1 中列出的命令包括cd切換目錄、ls列出文件、ps查看進(jìn)程狀態(tài)、chmod修改權(quán)限以及 redirect重定向和 pipe管道機(jī)制。這里的核心在于Shell 不只是逐條執(zhí)行命令還能通過(guò)重定向把輸出寫入文件通過(guò)管道把前一個(gè)命令的輸出作為后一個(gè)命令的輸入。比如ps -ef | grep test就是查看進(jìn)程列表中是否有 test 程序的經(jīng)典組合ls -l filelist.txt則把目錄列表保存到文件里。這兩個(gè)機(jī)制在后續(xù)實(shí)驗(yàn)中頻繁用到——軟中斷實(shí)驗(yàn)里需要觀察進(jìn)程 PID 和父子關(guān)系ps和grep的組合能快速確認(rèn)進(jìn)程是否被正確創(chuàng)建和終止。2.3 gcc 編譯與 gdb 調(diào)試把 C 程序跑起來(lái)報(bào)告中的實(shí)驗(yàn)流程是用 vim 寫代碼按i進(jìn)入編輯模式寫完按ESC返回一般模式再用:wq保存退出最后用gcc編譯。這里有個(gè)常見(jiàn)錯(cuò)誤初次使用 vim 的人經(jīng)常不知道如何進(jìn)入編輯模式或者按了ESC后直接輸入字符導(dǎo)致命令沖突。報(bào)告記錄的步驟把這條路走通了進(jìn)入 vim 后按i、o、a、r中的任一鍵都能進(jìn)入編輯模式ESC返回后:開(kāi)頭的是命令模式:wq是保存并退出。編譯階段gcc是最直接的編譯器gcc -o test test.c生成可執(zhí)行文件gcc -g test.c -o test帶調(diào)試信息生成。要比較 gcc、gdb、codeblocks 的區(qū)別核心在于 debug 能力gdb 支持設(shè)置斷點(diǎn)、單步執(zhí)行、查看變量值codeblocks 是圖形化 IDE把編譯和調(diào)試集成了但對(duì)遠(yuǎn)程環(huán)境如虛擬機(jī)中的 Linux不友好。報(bào)告中提到一個(gè)血淚經(jīng)驗(yàn)在 Windows 的 IDE 里寫好帶fork()的代碼復(fù)制到 Linux 后報(bào)錯(cuò)——因?yàn)?Windows IDE 默認(rèn)的頭文件和庫(kù)不含 POSIX 的fork()。從那以后我的習(xí)慣是涉及進(jìn)程、信號(hào)、文件鎖的代碼直接在 Linux 環(huán)境里寫不再跨環(huán)境搬運(yùn)。注意gcc編譯時(shí)建議加-Wall參數(shù)查看全部警告。fork 相關(guān)的代碼如果出現(xiàn)隱式聲明警告通常是因?yàn)槿鄙?include unistd.h這是最容易被忽略的問(wèn)題。2.4 網(wǎng)絡(luò)配置的三種模式與踩坑記錄實(shí)驗(yàn)報(bào)告里記錄了一次網(wǎng)絡(luò)故障排查虛擬機(jī)裝好后配置沒(méi)問(wèn)題但網(wǎng)絡(luò)連接一直失敗。排查后定位到是虛擬機(jī)的網(wǎng)絡(luò)模式選錯(cuò)了并借此把三種模式學(xué)明白了橋接模式是讓虛擬機(jī)像局域網(wǎng)里的一臺(tái)獨(dú)立主機(jī)可以互訪也可以上網(wǎng)但 IP 容易沖突NAT地址轉(zhuǎn)換模式是虛擬機(jī)通過(guò)母機(jī)代理訪問(wèn)外網(wǎng)外部網(wǎng)絡(luò)訪問(wèn)不到虛擬機(jī)主機(jī)模式host-only則是虛擬機(jī)和宿主機(jī)形成獨(dú)立網(wǎng)絡(luò)虛擬機(jī)無(wú)法訪問(wèn)互聯(lián)網(wǎng)。最終選擇 NAT 模式既避免 IP 沖突又能上網(wǎng)。這里給初學(xué)者一個(gè)建議操作系統(tǒng)實(shí)驗(yàn)大多不需要外部網(wǎng)絡(luò)但需要宿主機(jī)與虛擬機(jī)之間傳輸代碼文件。用 NAT 模式配合x(chóng)ftp或scp是最省心的方案如果只有一臺(tái)機(jī)器做實(shí)驗(yàn)host-only 也夠用只是不能在線查資料。如果遇到網(wǎng)絡(luò)不通先 ping 網(wǎng)關(guān)再確認(rèn)虛擬網(wǎng)卡是否啟用最后檢查防火墻——三個(gè)步驟能排除九成問(wèn)題。3. /proc 文件系統(tǒng)觀測(cè)內(nèi)核變量的黑匣子怎么打開(kāi)實(shí)驗(yàn) 1 和實(shí)驗(yàn) 7 把重點(diǎn)放在了/proc文件系統(tǒng)上。/proc是 Linux 內(nèi)核向用戶態(tài)暴露狀態(tài)信息的虛擬文件系統(tǒng)讀取它以獲取 CPU 信息、內(nèi)存狀態(tài)、系統(tǒng)負(fù)載、中斷統(tǒng)計(jì)等數(shù)據(jù)。它之所以重要是因?yàn)椴僮飨到y(tǒng)課上學(xué)到的很多概念如進(jìn)程狀態(tài)、內(nèi)存分配、系統(tǒng)節(jié)拍都以文件的形式反映在這里且可以隨時(shí)查閱。3.1 /proc/cpuinfo、version、stat、uptime 的觀測(cè)方法報(bào)告中明確給出了用程序觀測(cè)的四項(xiàng)內(nèi)容/proc/cpuinfo提供 CPU 型號(hào)、核數(shù)、頻率等信息/proc/version顯示內(nèi)核版本號(hào)例如 Linux 3.10.0-1160包含內(nèi)核編譯信息/proc/stat保存系統(tǒng)啟動(dòng)以來(lái)的 CPU 統(tǒng)計(jì)和進(jìn)程切換數(shù)據(jù)/proc/uptime返回系統(tǒng)開(kāi)機(jī)時(shí)長(zhǎng)。觀測(cè)這些文件最簡(jiǎn)單的方式是cat /proc/cpuinfo但課程設(shè)計(jì)要求「編程實(shí)現(xiàn)觀測(cè)」這就涉及 C 語(yǔ)言的文件讀取操作核心代碼如下#include stdio.h #include string.h int main() { FILE *fp; char line[256]; fp fopen(/proc/version, r); if (fp NULL) { perror(open /proc/version failed); return 1; } while (fgets(line, sizeof(line), fp) ! NULL) { printf(%s, line); } fclose(fp); return 0; }這段代碼本質(zhì)上就是打開(kāi)文件、逐行讀取、打印。但設(shè)計(jì)要點(diǎn)在于/proc下的文件是內(nèi)核動(dòng)態(tài)生成的虛擬文件讀取它不需要特殊權(quán)限部分需要 root但打開(kāi)失敗時(shí)要用perror輸出錯(cuò)誤原因。如果報(bào)No such file or directory說(shuō)明當(dāng)前系統(tǒng)的內(nèi)核版本不包含該節(jié)點(diǎn)例如/proc/filesystems在部分精簡(jiǎn)內(nèi)核中不存在——此時(shí)需要檢查內(nèi)核配置而不是改代碼。將fopen的文件路徑換成/proc/cpuinfo、/proc/stat、/proc/uptime即可觀測(cè)其余項(xiàng)目。3.2 meminfo、loadavg、interrupts、filesystem 的編程觀測(cè)報(bào)告要求編程實(shí)現(xiàn)對(duì)/proc/meminfo、/proc/loadavg、/proc/interrupts、/proc/filesystems的觀測(cè)。這四個(gè)文件的含義分別是meminfo描述系統(tǒng)內(nèi)存總量、空閑量、緩存量對(duì)應(yīng)課程中的內(nèi)存管理章節(jié)loadavg顯示 1 分鐘、5 分鐘、15 分鐘的平均負(fù)載interrupts記錄各中斷號(hào)的發(fā)生次數(shù)對(duì)應(yīng)中斷機(jī)制filesystems列出當(dāng)前內(nèi)核支持的文件系統(tǒng)類型。通常的做法是寫一個(gè)通用的讀取函數(shù)傳入路徑參數(shù)即可如下所示void show_file(const char *path) { FILE *fp fopen(path, r); char line[256]; if (!fp) { perror(path); return; } while (fgets(line, sizeof(line), fp) ! NULL) { printf(%s, line); } fclose(fp); } int main() { show_file(/proc/meminfo); show_file(/proc/loadavg); show_file(/proc/interrupts); show_file(/proc/filesystems); return 0; }這里有一個(gè)容易忽略的點(diǎn)/proc文件是偽文件讀取時(shí)fseek、rewind等操作沒(méi)有實(shí)際意義每次讀取都是內(nèi)核實(shí)時(shí)生成的快照。所以不要在代碼里做「定位到文件末尾再讀回開(kāi)頭」的操作直接順序讀取即可。3.3 從 jiffies 理解內(nèi)核時(shí)間基準(zhǔn)報(bào)告在實(shí)驗(yàn)總結(jié)中記錄了一個(gè)有意思的思考為什么 Linux 的時(shí)間統(tǒng)計(jì)是基于「節(jié)拍」而不是「秒」答案是內(nèi)核使用jiffies作為全局時(shí)間基準(zhǔn)——內(nèi)核每過(guò)一個(gè)節(jié)拍通常 1/250 秒或 1/100 秒jiffies加 1。開(kāi)機(jī)時(shí)內(nèi)核讀取 RTC 硬件獲取基準(zhǔn)時(shí)間再換算成對(duì)應(yīng)的jiffies值減去 1970-01-01 UTC 的時(shí)間差。/proc/uptime中的數(shù)值就是基于jiffies換算出來(lái)的。這個(gè)知識(shí)點(diǎn)在今年的面試和課程答辯中經(jīng)常出現(xiàn)為什么 uptime 是浮點(diǎn)數(shù)為什么 loadavg 有三個(gè)值如果只答「記錄系統(tǒng)啟動(dòng)時(shí)長(zhǎng)」就太淺了。能說(shuō)出jiffies的遞增機(jī)制和系統(tǒng)節(jié)拍HZ的配置才算是真正理解了內(nèi)核時(shí)間系統(tǒng)。報(bào)告中這部分屬于加分項(xiàng)建議在答辯時(shí)主動(dòng)提。4. 軟中斷通信實(shí)驗(yàn)信號(hào)機(jī)制與進(jìn)程終止順序軟中斷通信是這份報(bào)告中代碼量最大、也最值得細(xì)讀的實(shí)驗(yàn)。實(shí)驗(yàn)要求用信號(hào)如 SIGQUIT、SIGUSR1實(shí)現(xiàn)父子進(jìn)程間的通信并觀察終止順序。核心是理解三件事fork()創(chuàng)建子進(jìn)程后的父子關(guān)系、signal()注冊(cè)信號(hào)處理函數(shù)、kill()發(fā)送信號(hào)。4.1 實(shí)驗(yàn)一一個(gè)父進(jìn)程創(chuàng)建兩個(gè)子進(jìn)程用 Ctrl\ 終止流程是這樣的父進(jìn)程創(chuàng)建兩個(gè)子進(jìn)程 P1 和 P2父進(jìn)程調(diào)用signal(SIGQUIT, stop)注冊(cè)信號(hào)處理函數(shù)然后進(jìn)入waiting()忙等待用戶在終端按Ctrl\發(fā)送 SIGQUIT父進(jìn)程收到信號(hào)后執(zhí)行stop()將wait_mark置 0退出等待隨后向 P1 發(fā)信號(hào) 16、向 P2 發(fā)信號(hào) 17兩個(gè)子進(jìn)程收到信號(hào)后打印結(jié)束信息并退出父進(jìn)程最后wait(0)回收子進(jìn)程并退出。核心代碼節(jié)選void waiting() { while (wait_mark ! 0); } void stop() { wait_mark 0; } int main() { int p1, p2; while ((p1 fork()) -1); // 創(chuàng)建第一個(gè)子進(jìn)程 sleep(1); if (p1 0) { // 父進(jìn)程分支 printf(parent process is %d \n, getpid()); while ((p2 fork()) -1); // 創(chuàng)建第二個(gè)子進(jìn)程 if (p2 0) { wait_mark 1; signal(SIGQUIT, stop); waiting(); kill(p1, 16); // 向子進(jìn)程 P1 發(fā)信號(hào) 16 kill(p2, 17); // 向子進(jìn)程 P2 發(fā)信號(hào) 17 wait(0); 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); wait_mark 1; signal(17, stop); 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); wait_mark 1; signal(16, stop); waiting(); lockf(1, 1, 0); printf(\n child P1 process %d is killed by parent %d , getpid(), getppid()); lockf(1, 0, 0); } return 0; }代碼的設(shè)計(jì)說(shuō)明fork()返回 0 表示當(dāng)前是子進(jìn)程返回大于 0 的值是子進(jìn)程 PID對(duì)父進(jìn)程而言。父進(jìn)程先通過(guò)fork()創(chuàng)建 P1sleep(1)確保 P1 完成初始化再創(chuàng)建 P2。為什么 P1 和 P2 要用不同的信號(hào)編號(hào)16 和 17因?yàn)檫@樣每個(gè)子進(jìn)程只響應(yīng)屬于自己的信號(hào)避免歧義這對(duì)應(yīng)了「軟中斷信號(hào)用于進(jìn)程間通信」的實(shí)驗(yàn)?zāi)康摹ockf(1, 1, 0)的作用是加鎖標(biāo)準(zhǔn)輸出防止兩個(gè)子進(jìn)程同時(shí)往終端打印時(shí)輸出交錯(cuò)這在并發(fā)編程里是一個(gè)典型問(wèn)題——不加鎖printf 的輸出可能互相穿插。4.2 實(shí)驗(yàn)二父、子、孫三級(jí)進(jìn)程的依次終止實(shí)驗(yàn)二不同于實(shí)驗(yàn)一的「一父兩子」結(jié)構(gòu)而是父進(jìn)程 → 子進(jìn)程 → 孫進(jìn)程的鏈?zhǔn)浇Y(jié)構(gòu)。流程是父進(jìn)程創(chuàng)建子進(jìn)程 P1P1 再創(chuàng)建孫進(jìn)程 P2按 Ctrl\ 后父進(jìn)程向 P1 發(fā)信號(hào)P1 向 P2 發(fā)信號(hào)依次終止孫、子、父。進(jìn)程樹(shù)形結(jié)構(gòu)中的終止順序是先殺最深的再殺中間的最后殺根部的。int main() { int p1, p2; while ((p1 fork()) -1); if (p1 0) { // 父進(jìn)程 wait_mark 1; signal(SIGQUIT, stop); waiting(); kill(p1, 16); wait(0); printf(\n parent process is killed \n); exit(0); } else { // 子進(jìn)程 while ((p1 fork()) -1); // 創(chuàng)建孫進(jìn)程 if (p1 0) { // 子進(jìn)程分支 printf(child P1 process %d is created by the parent %d \n, getpid(), getppid()); signal(SIGQUIT, SIG_IGN); wait_mark 1; signal(16, stop); waiting(); kill(p1, 16); wait(0); printf(\n child P1 process %d is killed by parent %d \n, getpid(), getppid()); exit(0); } else if (p1 0) { // 孫進(jìn)程分支 printf(grandson process %d is created by the parent %d \n, getpid(), getppid()); signal(SIGQUIT, SIG_IGN); wait_mark 1; signal(16, stop); waiting(); printf(\n grandson process %d is killed by parent %d \n, getpid(), getppid()); exit(0); } } return 0; }這段代碼的邏輯層次很清晰但要注意一個(gè)隱蔽的問(wèn)題子進(jìn)程和孫進(jìn)程都注冊(cè)了信號(hào) 16 的處理函數(shù)這意味著如果父進(jìn)程誤發(fā)了兩次信號(hào) 16孫進(jìn)程和子進(jìn)程可能同時(shí)退出破壞層級(jí)順序。所以實(shí)驗(yàn)二的運(yùn)行結(jié)果中「先孫再子」的順序依賴代碼執(zhí)行路徑而不是信號(hào)優(yōu)先級(jí)——父進(jìn)程是在wait(0)等子進(jìn)程結(jié)束后才 print 自己的結(jié)束信息子進(jìn)程是在wait(0)等孫進(jìn)程結(jié)束后才 print 自己的結(jié)束信息這是一個(gè)隱式的順序控制。4.3 避坑指南軟中斷實(shí)驗(yàn)中容易翻車的四個(gè)問(wèn)題1. 現(xiàn)象fork()在編譯時(shí)報(bào)錯(cuò)提示隱式聲明。原因源文件缺少#include unistd.h部分舊代碼還依賴#include sys/types.h。解決在文件頭部補(bǔ)全頭文件不要省略u(píng)nistd.h如果用了wait()還要加#include sys/wait.h。2. 現(xiàn)象程序運(yùn)行后按 Ctrl\ 沒(méi)有反應(yīng)。原因SIGQUIT的信號(hào)處理函數(shù)沒(méi)有正確注冊(cè)或者進(jìn)程處于后臺(tái)運(yùn)行狀態(tài)nohup 方式啟動(dòng)。解決確認(rèn)用signal(SIGQUIT, stop)注冊(cè)了處理函數(shù)而不是SIG_IGN確認(rèn)程序在前臺(tái)運(yùn)行Ctrl\只對(duì)前臺(tái)進(jìn)程的終端有效。3. 現(xiàn)象父子進(jìn)程的 printf 輸出交叉混亂。原因多個(gè)進(jìn)程同時(shí)搶占標(biāo)準(zhǔn)輸出沒(méi)有互斥。解決用lockf(1, 1, 0)加鎖標(biāo)準(zhǔn)輸出打印完再lockf(1, 0, 0)解鎖。這個(gè)方法比printf前的sleep更可靠。4. 現(xiàn)象父進(jìn)程 wait(0) 返回后子進(jìn)程仍未退出。原因kill(p1, 16)發(fā)送信號(hào)后子進(jìn)程可能還在waiting()忙等待中沒(méi)有及時(shí)被調(diào)度。解決在信號(hào)處理函數(shù)里將wait_mark置 0 是不夠的還要確保子進(jìn)程收到信號(hào)后能退出waiting()循環(huán)——這里的關(guān)鍵是signal(16, stop)注冊(cè)的處理函數(shù)確實(shí)被執(zhí)行??梢杂胹leep(1)或waitpid()替代忙等待使時(shí)序更穩(wěn)定。5. 進(jìn)程調(diào)度、線程同步、內(nèi)存管理與系統(tǒng)調(diào)用從模擬到真實(shí)實(shí)現(xiàn)實(shí)驗(yàn) 3 到實(shí)驗(yàn) 6 的內(nèi)容覆蓋了操作系統(tǒng)的幾個(gè)核心主題進(jìn)程調(diào)度策略、線程同步、內(nèi)存管理和系統(tǒng)調(diào)用。報(bào)告中記錄了 FCFS 與 RR 調(diào)度策略的驗(yàn)證、線程同步實(shí)驗(yàn)、內(nèi)存管理的代碼與結(jié)果解讀、以及系統(tǒng)調(diào)用的具體實(shí)現(xiàn)步驟。5.1 進(jìn)程調(diào)度實(shí)驗(yàn)FCFS 與 RR 的工程驗(yàn)證實(shí)驗(yàn) 3 要求創(chuàng)建工程并驗(yàn)證 FCFS先來(lái)先服務(wù)和 RR時(shí)間片輪轉(zhuǎn)策略。FCFS 的核心指標(biāo)是等待時(shí)間先到達(dá)的進(jìn)程先執(zhí)行短作業(yè)可能排隊(duì)很久RR 的核心是時(shí)間片每個(gè)進(jìn)程運(yùn)行一個(gè)時(shí)間片后輪轉(zhuǎn)到下一個(gè)。設(shè)計(jì)實(shí)驗(yàn)時(shí)需要構(gòu)造一組進(jìn)程分別記錄到達(dá)時(shí)間、服務(wù)時(shí)間、完成時(shí)間和周轉(zhuǎn)時(shí)間并用表格匯總。建議的驗(yàn)證方式用結(jié)構(gòu)體數(shù)組保存進(jìn)程控制塊模擬調(diào)度過(guò)程。typedef struct { int pid; // 進(jìn)程號(hào) int arrive_time; // 到達(dá)時(shí)間 int service_time; // 服務(wù)時(shí)間 int finish_time; // 完成時(shí)間 int remain_time; // 剩余時(shí)間RR 用 } pcb;FCFS 的實(shí)現(xiàn)就是遍歷進(jìn)程數(shù)組按到達(dá)時(shí)間排序后依次執(zhí)行RR 的實(shí)現(xiàn)需要維護(hù)一個(gè)就緒隊(duì)列每次執(zhí)行一個(gè)時(shí)間片如 2 個(gè)單位時(shí)間片結(jié)束后判斷剩余時(shí)間是否為 0。報(bào)告中的實(shí)驗(yàn)結(jié)論是FCFS 的平均周轉(zhuǎn)時(shí)間通常高于 RR但 RR 的響應(yīng)時(shí)間更短如果進(jìn)程的服務(wù)時(shí)間差異很大RR 的時(shí)間片要選得合理太小則上下文切換開(kāi)銷大太大則退化為 FCFS。答辯時(shí)把這個(gè)權(quán)衡講清楚比背概念有用得多。5.2 線程同步實(shí)驗(yàn)鎖、信號(hào)量與條件變量實(shí)驗(yàn) 4 的線程同步實(shí)驗(yàn)報(bào)告沒(méi)有詳細(xì)列出代碼但通常的做法是經(jīng)典的「生產(chǎn)者-消費(fèi)者」或「讀者-寫者」模型。用pthread_mutex_t保護(hù)共享資源用pthread_cond_t實(shí)現(xiàn)條件等待#include pthread.h pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond PTHREAD_COND_INITIALIZER; int buffer[10]; int count 0; void *producer(void *arg) { pthread_mutex_lock(mutex); while (count 10) { pthread_cond_wait(cond, mutex); } buffer[count] 1; pthread_cond_signal(cond); pthread_mutex_unlock(mutex); return NULL; }這個(gè)代碼的兩個(gè)關(guān)鍵點(diǎn)pthread_cond_wait必須放在 while 循環(huán)里而不是 if 里——因?yàn)榭赡艽嬖谔摷賳拘裵thread_cond_signal和pthread_mutex_unlock的順序會(huì)影響喚醒后線程的調(diào)度效率。如果忘記解鎖或者忘記加鎖程序可能死鎖或數(shù)據(jù)競(jìng)爭(zhēng)這類問(wèn)題用valgrind --toolhelgrind可以檢測(cè)但最好的辦法是寫代碼時(shí)就把「加鎖-操作-解鎖」的邊界畫(huà)清楚。5.3 內(nèi)存管理實(shí)驗(yàn)頁(yè)面置換與缺頁(yè)統(tǒng)計(jì)實(shí)驗(yàn) 5 的內(nèi)容是內(nèi)存管理重點(diǎn)是頁(yè)面置換算法。報(bào)告給出了實(shí)驗(yàn)結(jié)果展示和結(jié)果解讀但沒(méi)有把代碼全部貼出來(lái)。常見(jiàn)的設(shè)計(jì)是模擬 FIFO、LRU、OPT 三種算法統(tǒng)計(jì)各自的缺頁(yè)次數(shù)。報(bào)告中實(shí)驗(yàn) 7 的「系統(tǒng)缺頁(yè)次數(shù)統(tǒng)計(jì)」實(shí)際就是在真實(shí)系統(tǒng)中觀測(cè)缺頁(yè)行為通過(guò)讀取/proc/vmstat中的pgfault字段來(lái)統(tǒng)計(jì)缺頁(yè)次數(shù)或者通過(guò)ps -o minflt,majflt查看某個(gè)進(jìn)程的缺頁(yè)計(jì)數(shù)。一個(gè)實(shí)際可用的思路在 C 程序中分配一大塊內(nèi)存并循環(huán)訪問(wèn)在訪問(wèn)前后分別讀取/proc/vmstat的缺頁(yè)計(jì)數(shù)值差值就是觸發(fā)的新缺頁(yè)數(shù)#include stdio.h #include stdlib.h #include string.h int main() { unsigned long before, after; FILE *fp; int size 100 * 1024 * 1024; // 100MB char *buf malloc(size); fp fopen(/proc/vmstat, r); // 讀取 pgfault 字段存入 before fclose(fp); memset(buf, 1, size); // 觸發(fā)大量缺頁(yè) fp fopen(/proc/vmstat, r); // 讀取 pgfault 字段存入 after fclose(fp); printf(page faults: %lu\n, after - before); free(buf); return 0; }這里的設(shè)計(jì)說(shuō)明memset一個(gè) 100MB 的緩沖區(qū)如果系統(tǒng)內(nèi)存緊張會(huì)產(chǎn)生大量主缺頁(yè)major fault和次缺頁(yè)minor fault。實(shí)際操作中建議把before和after的讀取封裝成函數(shù)避免代碼重復(fù)。5.4 系統(tǒng)調(diào)用實(shí)驗(yàn)從用戶態(tài)到內(nèi)核態(tài)的通道實(shí)驗(yàn) 6 的系統(tǒng)調(diào)用實(shí)驗(yàn)報(bào)告中的核心是編寫用戶態(tài)程序調(diào)用syscall或直接調(diào)用open、read、write等 API運(yùn)行后觀察程序的系統(tǒng)調(diào)用軌跡。通常的做法是用strace追蹤strace -c ./test_program會(huì)統(tǒng)計(jì)每個(gè)系統(tǒng)調(diào)用的次數(shù)、耗時(shí)和錯(cuò)誤數(shù)。這在實(shí)驗(yàn)報(bào)告中是加分項(xiàng)因?yàn)槟苤庇^展示read、write、mmap、futex這些系統(tǒng)調(diào)用的真實(shí)行為。如果課程要求新增一個(gè)系統(tǒng)調(diào)用部分高校的實(shí)驗(yàn)內(nèi)容是給內(nèi)核添加一個(gè)系統(tǒng)調(diào)用通常的做法是修改系統(tǒng)調(diào)用號(hào)表、添加內(nèi)核函數(shù)實(shí)現(xiàn)、重新編譯內(nèi)核。這個(gè)操作復(fù)雜且危險(xiǎn)可能導(dǎo)致系統(tǒng)起不來(lái)建議在虛擬機(jī)里做并做好快照。報(bào)告中的實(shí)驗(yàn) 6 沒(méi)有走這么深停留在用戶態(tài)調(diào)用層面但如果你能在答辯中說(shuō)出strace的結(jié)果含義就已經(jīng)超過(guò)了大多數(shù)同學(xué)。6. 報(bào)告驗(yàn)收與答辯如何把 95 分實(shí)驗(yàn)講成自己的東西實(shí)驗(yàn)報(bào)告的驗(yàn)收通常分兩部分文檔審查和現(xiàn)場(chǎng)答辯。文檔審查看的是格式規(guī)范、實(shí)驗(yàn)步驟完整性、實(shí)驗(yàn)結(jié)果是否有截圖、代碼是否可復(fù)現(xiàn)現(xiàn)場(chǎng)答辯看的是隨機(jī)抽查的某個(gè)實(shí)驗(yàn)的核心機(jī)制。根據(jù)這份報(bào)告的經(jīng)驗(yàn)我把驗(yàn)收作答的要點(diǎn)整理成一張核對(duì)表檢查項(xiàng)合格標(biāo)準(zhǔn)常見(jiàn)扣分點(diǎn)實(shí)驗(yàn)?zāi)康拿鞔_列出本次實(shí)驗(yàn)要理解什么概念從網(wǎng)上抄一段「了解 Linux」就算完實(shí)驗(yàn)步驟能復(fù)現(xiàn)命令和代碼對(duì)應(yīng)只有代碼沒(méi)有運(yùn)行步驟實(shí)驗(yàn)結(jié)果截圖或輸出且有解讀貼了圖但沒(méi)解釋為什么是這個(gè)輸出實(shí)驗(yàn)總結(jié)記錄遇到的問(wèn)題和解決辦法直接跳過(guò)或抄教材原文答辯時(shí)最容易翻車的兩個(gè)問(wèn)題一是「這個(gè)程序如果去掉 sleep(1) 會(huì)怎樣」二是「為什么用信號(hào) 16 而不是 SIGUSR1」。前者考察對(duì)并發(fā)時(shí)序的理解——去掉sleep(1)后父進(jìn)程可能在子進(jìn)程完成初始化前就fork()第二個(gè)子進(jìn)程導(dǎo)致進(jìn)程關(guān)系錯(cuò)亂后者考察對(duì) Linux 信號(hào)編號(hào)的掌握——SIGUSR1 在部分架構(gòu)上是信號(hào) 10SIGUSR2 是信號(hào) 12但代碼中選擇 16 和 17 屬于自定義信號(hào)需要確保內(nèi)核支持。這個(gè)問(wèn)題的回答思路是信號(hào)編號(hào)對(duì)應(yīng)關(guān)系依賴平臺(tái)課程實(shí)驗(yàn)中固定使用 16 和 17 是為了簡(jiǎn)單明確但實(shí)際工程中建議使用SIGUSR1并配合sigaction()注冊(cè)這樣可讀性和可移植性都更好。此外答辯時(shí)有一個(gè)實(shí)用技巧提前準(zhǔn)備 3 個(gè)「為什么」。比如軟中斷實(shí)驗(yàn)的代碼里為什么要用wait_mark忙等待而不直接pause()因?yàn)槊Φ却恍枰~外引入信號(hào)邏輯直觀但 CPU 空轉(zhuǎn)如果換成pause()可以讓出 CPU但需要處理「信號(hào)到達(dá)時(shí)進(jìn)程處于可中斷睡眠」的狀態(tài)轉(zhuǎn)換。你能主動(dòng)說(shuō)出這個(gè)權(quán)衡說(shuō)明不是只抄了代碼而是真正理解了設(shè)計(jì)取舍。提示如果被問(wèn)到「實(shí)驗(yàn)結(jié)果和預(yù)期不一致怎么辦」不要慌張。先把實(shí)際輸出貼出來(lái)指出哪一步和預(yù)期不同然后給出排查方向——多數(shù)情況是wait()回收順序問(wèn)題或信號(hào)編號(hào)沖突。從那以后我每次做完實(shí)驗(yàn)都會(huì)強(qiáng)制自己走一遍「去掉一個(gè)關(guān)鍵代碼再跑一次」的流程。比如軟中斷實(shí)驗(yàn)去掉lockf觀察輸出是否錯(cuò)亂調(diào)度實(shí)驗(yàn)把時(shí)間片改成 1觀察周轉(zhuǎn)時(shí)間變化。這樣做的好處是任何「為什么這樣寫」的問(wèn)題都能用自己的實(shí)驗(yàn)結(jié)果來(lái)回答而不是背答案。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取