編程中文講義:從進(jìn)程線程到內(nèi)存同步的實(shí)戰(zhàn)指南)
簡(jiǎn)介面向系統(tǒng)編程學(xué)習(xí)者的UIUC CS241中文講義翻譯項(xiàng)目基于美國(guó)伊利諾伊大學(xué)厄巴納-香檳分校經(jīng)典課程整理而成適合需要系統(tǒng)理解進(jìn)程、線程、虛擬內(nèi)存、同步與并發(fā)等底層機(jī)制的開(kāi)發(fā)者參考。該資源為ApacheCN開(kāi)源社區(qū)維護(hù)的校對(duì)版包含Markdown原文與靜態(tài)網(wǎng)頁(yè)兩種閱讀形態(tài)便于本地或在線學(xué)習(xí)。壓縮包共137個(gè)文件、約3.5MB其中93個(gè)md格式的講義正文是核心內(nèi)容其余為網(wǎng)站部署所需的html、css、js、圖片及Dockerfile等支持文件結(jié)構(gòu)清晰、體量輕巧。目前已有194人學(xué)習(xí)下載。通過(guò)這套講義讀者能獲得成體系的中文授課筆記既能按章節(jié)順序?yàn)g覽也可借助Docker一鍵啟動(dòng)閱讀環(huán)境在校對(duì)協(xié)作中持續(xù)修訂適合作為自學(xué)系統(tǒng)編程的伴隨資料。1. 系統(tǒng)編程自學(xué)為什么繞不開(kāi)這份中文講義CS241 是 UIUC 計(jì)算機(jī)系一門典型的系統(tǒng)編程課課程核心不是講操作系統(tǒng)內(nèi)核源碼而是逼你用 C 語(yǔ)言把進(jìn)程、線程、內(nèi)存、同步這些概念親手實(shí)現(xiàn)一遍。這份《UIUC CS241 系統(tǒng)編程中文講義》就是把原版課程筆記和實(shí)驗(yàn)引導(dǎo)翻譯成中文幫非英語(yǔ)母語(yǔ)的讀者把精力從查詞挪回理解本身。系統(tǒng)編程的門檻不在語(yǔ)法而在「想清楚程序在機(jī)器里到底怎么跑」——fork 之后父子進(jìn)程各占哪份內(nèi)存死鎖是搶鎖順序錯(cuò)了還是信號(hào)沒(méi)發(fā)對(duì)malloc 返回的指針為什么有時(shí)比請(qǐng)求多出 16 字節(jié)。這些問(wèn)題靠讀英文原版文檔不是不行但認(rèn)知負(fù)擔(dān)會(huì)翻倍。這份講義適合兩類人一類是剛啃完 C 語(yǔ)言、想往底層走的在校生另一類是寫業(yè)務(wù)代碼寫膩了、想搞明白程序真正運(yùn)行原理的在職工程師。它的價(jià)值不是替代教材而是給你一條已經(jīng)有人踩過(guò)坑的路線圖。2. 從 CS241 課程設(shè)計(jì)看中文講義的翻譯價(jià)值為什么原版實(shí)驗(yàn)題比理論更值得讀2.1 CS241 的課程目錄到底在教什么原版 CS241 的講義結(jié)構(gòu)大致沿一條主線展開(kāi)從 C 語(yǔ)言的內(nèi)存模型出發(fā)逐步覆蓋文件系統(tǒng)、進(jìn)程控制、線程同步、網(wǎng)絡(luò)并發(fā)和 shell 實(shí)現(xiàn)。這不是普通的教學(xué)大綱排布而是按系統(tǒng)程序員實(shí)際工作流的順序設(shè)計(jì)的。第一周講完指針和結(jié)構(gòu)體內(nèi)存布局第二周就開(kāi)始碰文件描述符和重定向第三周直接讓寫一個(gè) mini shell第四周進(jìn)入 pthread 和互斥鎖。這種節(jié)奏對(duì)初學(xué)者偏快但對(duì)已經(jīng)在職的人反而是優(yōu)勢(shì)——因?yàn)槊恳恢艿膬?nèi)容都能直接映射到生產(chǎn)環(huán)境的一個(gè)具體問(wèn)題。中文講義的價(jià)值恰恰體現(xiàn)在這些課程筆記的翻譯密度上。原版講義很多是圖表加注釋的形式文字不多但每句話都?jí)褐R(shí)點(diǎn)。翻譯者如果只是逐句直譯會(huì)損失掉大量上下文線索。好的翻譯必須把「為什么這里要強(qiáng)調(diào) off_t 是 64 位」「為什么 read 返回值要強(qiáng)制檢查」這類隱含邏輯補(bǔ)出來(lái)。從我看到的幾份流傳版本來(lái)看中文講義在關(guān)鍵實(shí)驗(yàn)題目后都附了譯者補(bǔ)充的「實(shí)現(xiàn)提示」這部分是原版沒(méi)有的也是整份講義里含金量最高的地方。系統(tǒng)編程的另一個(gè)難點(diǎn)是工具鏈。原版課程實(shí)驗(yàn)基于特定版本的 Linux 和 gcc中文講義如果只翻譯文字不處理命令差異讀者很容易卡在校驗(yàn)?zāi)_本跑不過(guò)的問(wèn)題上。好的中文版本會(huì)在每個(gè)實(shí)驗(yàn)開(kāi)頭補(bǔ)一小節(jié)「環(huán)境對(duì)齊說(shuō)明」把 makefile 里常見(jiàn)的坑、valgrind 版本差異、以及 gcc 的 -fsanitize 參數(shù)變化都標(biāo)注出來(lái)。這些細(xì)節(jié)決定了學(xué)習(xí)者能否在本地環(huán)境把課程實(shí)驗(yàn)順利跑通。2.2 中文講義相比英文原版的三個(gè)信息增量第一個(gè)增量是錯(cuò)誤信息的解讀。原版講義里對(duì)于段錯(cuò)誤、死鎖這類問(wèn)題往往只給一兩句提示中文版普遍會(huì)展開(kāi)成典型的排查路徑先查什么、用什么工具、輸出怎么看、最常見(jiàn)的原因排序。比如 CS241 的 malloc 實(shí)驗(yàn)最常見(jiàn)的段錯(cuò)誤不是指針?biāo)沐e(cuò)而是忘了給返回指針對(duì)齊到 16 字節(jié)。原版只會(huì)在測(cè)試腳本里報(bào) failed中文講義則會(huì)告訴你先檢查對(duì)齊宏是否生效再看 metadata 結(jié)構(gòu)體是否占了 8 字節(jié)整數(shù)倍。第二個(gè)增量是作業(yè)代碼的逐段注釋。課程實(shí)驗(yàn)的 skeleton 代碼很多地方故意留出空位讓你填但填進(jìn)去的前提是理解調(diào)用者和被調(diào)用者的契約。中文講義在關(guān)鍵位置補(bǔ)了「這個(gè)函數(shù)由測(cè)試框架調(diào)用必須保證線程安全」「這里返回的指針必須能被 free說(shuō)明不能指向棧變量」這類注釋直接降低理解成本。第三個(gè)增量是概念辨析。系統(tǒng)編程里有很多一對(duì)容易混淆的詞組比如「進(jìn)程和線程的區(qū)別」其實(shí)總是伴隨著「fork 和 pthread_create 的返回值類型為何不同」。中文講義把這些辨析集中標(biāo)注配合實(shí)驗(yàn)代碼里的實(shí)際用法比單純看理論描述有效得多。3. 用中文講義跑通第一個(gè)實(shí)驗(yàn)進(jìn)程控制與并發(fā)同步的代碼逐行拆解3.1 環(huán)境準(zhǔn)備與實(shí)驗(yàn)文件的最小結(jié)構(gòu)CS241 的課程實(shí)驗(yàn)通常會(huì)給一個(gè)壓縮包里面包含測(cè)試腳本和骨架代碼。我建議在使用這份中文講義時(shí)先不要急著改代碼先把測(cè)試腳本跑通確認(rèn)環(huán)境屬性正確。課程實(shí)驗(yàn)對(duì)環(huán)境的依賴主要集中在兩塊一是編輯器不能把 tab 擴(kuò)展成空格因?yàn)?makefile 對(duì)縮進(jìn)敏感二是系統(tǒng)必須支持 pthread 庫(kù)這需要安裝 libpthread 相關(guān)的開(kāi)發(fā)包一般現(xiàn)代 Linux 發(fā)行版默認(rèn)自帶但 Docker 精簡(jiǎn)鏡像里常常缺失。檢查的命令很簡(jiǎn)單直接在終端執(zhí)行# 檢查 pthread 庫(kù)是否存在同時(shí)確認(rèn) gcc 編譯器版本滿足課程要求 gcc -pthread -o /tmp/test_pthread /dev/null 21 \ echo pthread OK || echo pthread MISSING gcc --version | head -n1這段命令的作用是先編譯一個(gè)空程序來(lái)驗(yàn)證 pthread 庫(kù)能不能鏈接通過(guò)再輸出 gcc 版本。-pthread參數(shù)有兩個(gè)作用一是讓預(yù)處理器定義_REENTRANT宏二是讓鏈接器自動(dòng)加上 libpthread 庫(kù)。如果這個(gè)命令報(bào)錯(cuò)說(shuō)明系統(tǒng)缺開(kāi)發(fā)包需要用發(fā)行版的包管理器安裝常見(jiàn)的是build-essential或libc6-dev。這步排查完成之后再進(jìn)入實(shí)驗(yàn)代碼會(huì)少很多干擾因素。3.2 fork 與進(jìn)程管理的核心實(shí)驗(yàn)理解地址空間副本CS241 關(guān)于進(jìn)程的第一個(gè)實(shí)驗(yàn)通常圍繞fork展開(kāi)。fork的語(yǔ)義是創(chuàng)建當(dāng)前進(jìn)程的一個(gè)幾乎完全相同的副本副本之間僅 PID 不同。但 DDL 里經(jīng)??嫉囊粋€(gè)點(diǎn)是「fork 之后變量是共享的還是獨(dú)立的」答案是獨(dú)立——子進(jìn)程拿到的是父進(jìn)程地址空間的完整拷貝之后各改各的互不影響。下面這段代碼對(duì)應(yīng)課程講義中關(guān)于 fork 行為的一個(gè)典型驗(yàn)證實(shí)驗(yàn)#include stdio.h #include unistd.h #include sys/wait.h int main() { int counter 0; pid_t pid fork(); if (pid 0) { perror(fork failed); return 1; } else if (pid 0) { // 子進(jìn)程分支這里對(duì) counter 的修改不會(huì)影響父進(jìn)程 counter 10; printf(Child: counter %d, counter %p\n, counter, (void *)counter); return 0; } else { // 父進(jìn)程分支等待子進(jìn)程結(jié)束再查看自己這邊的 counter wait(NULL); printf(Parent: counter %d, counter %p\n, counter, (void *)counter); return 0; } }這段代碼的行為值得注意父子進(jìn)程輸出的counter地址可能完全相同但值不同。原因是虛擬內(nèi)存機(jī)制——父子進(jìn)程各自的頁(yè)表指向不同的物理頁(yè)幀邏輯地址一樣物理地址不同。這里有個(gè)常見(jiàn)的認(rèn)知誤區(qū)看到地址相同就以為是共享內(nèi)存其實(shí)不是。CS241 的實(shí)驗(yàn)通常會(huì)在此基礎(chǔ)上增加一個(gè)步驟——讓你在 fork 之后用mmap創(chuàng)建共享映射此時(shí)同樣的邏輯地址才會(huì)指向同一塊物理內(nèi)存修改才會(huì)互相可見(jiàn)。參數(shù)說(shuō)明wait(NULL)的作用是讓父進(jìn)程阻塞直到子進(jìn)程退出如果不寫這一步父進(jìn)程可能在子進(jìn)程執(zhí)行完之前就打印出結(jié)果造成輸出順序混亂。perror用來(lái)打印錯(cuò)誤原因字符串在 fork 調(diào)用失敗時(shí)這是最直接的報(bào)錯(cuò)方式。這段實(shí)驗(yàn)的意義在于讓你親手驗(yàn)證「寫時(shí)復(fù)制copy-on-write」的存在——fork 之后并不真正復(fù)制所有內(nèi)存頁(yè)只有發(fā)生寫入時(shí)才復(fù)制。所以counter 10這行代碼在子進(jìn)程里觸發(fā)了一次缺頁(yè)中斷內(nèi)核才開(kāi)始拷貝頁(yè)面。3.3 線程同步實(shí)驗(yàn)從互斥鎖到條件變量CS241 的并發(fā)實(shí)驗(yàn)是課程中段的重頭戲。實(shí)驗(yàn)要求通常是用 pthread 創(chuàng)建多個(gè)線程對(duì)一份共享數(shù)據(jù)執(zhí)行累加操作然后觀察不加鎖、加鎖、用原子操作三種方式的差異。講義中會(huì)對(duì)鎖的實(shí)現(xiàn)思路做詳細(xì)說(shuō)明并引導(dǎo)你思考「為什么自旋鎖在單核 CPU 上是災(zāi)難」。下面這段代碼演示了用互斥鎖保護(hù)共享計(jì)數(shù)器的常見(jiàn)寫法#include pthread.h #include stdio.h #define THREAD_COUNT 4 #define INCREMENTS 100000 static int counter 0; static pthread_mutex_t lock PTHREAD_MUTEX_INITIALIZER; void *worker(void *arg) { // 每個(gè)線程連續(xù)對(duì) counter 執(zhí)行自增操作lock 保證原子性 for (int i 0; i INCREMENTS; i) { pthread_mutex_lock(lock); counter; pthread_mutex_unlock(lock); } return NULL; } int main() { pthread_t threads[THREAD_COUNT]; for (int i 0; i THREAD_COUNT; i) { pthread_create(threads[i], NULL, worker, NULL); } for (int i 0; i THREAD_COUNT; i) { pthread_join(threads[i], NULL); } printf(Final counter: %d\n, counter); return 0; }這段代碼的邏輯非常直白每個(gè)線程循環(huán) 10 萬(wàn)次每次都對(duì)共享 counter 執(zhí)行 lock、自增、unlock。如果去掉鎖最終結(jié)果一定小于 400000因?yàn)閏ounter在匯編層面是 read-modify-write 三步線程切換可能發(fā)生在任意一步之間導(dǎo)致丟失更新。鎖的作用是讓這三步成為一個(gè)不可分割的臨界區(qū)。但這里有個(gè)性能問(wèn)題每次自增都需要一次系統(tǒng)調(diào)用或用戶態(tài) futex 操作鎖競(jìng)爭(zhēng)嚴(yán)重時(shí)吞吐量會(huì)很難看。講義在這個(gè)實(shí)驗(yàn)之后會(huì)引申出無(wú)鎖編程和原子操作__atomic_add_fetch是內(nèi)建函數(shù)可以直接替代鎖但它的語(yǔ)義需要底層硬件支持在 x86 上對(duì)應(yīng) LOCK XADD 指令。中文講義在講解這段時(shí)通常補(bǔ)兩個(gè)知識(shí)點(diǎn)一是pthread_mutex_t初始化的兩種方式——靜態(tài)初始化宏P(guān)THREAD_MUTEX_INITIALIZER和動(dòng)態(tài)pthread_mutex_init的區(qū)別二是鎖的銷毀問(wèn)題用靜態(tài)初始化的鎖在進(jìn)程退出前是否需要調(diào)用pthread_mutex_destroy嚴(yán)格來(lái)說(shuō)如果是動(dòng)態(tài)分配的鎖不銷毀會(huì)泄漏內(nèi)核資源。這些屬于「不做也不會(huì)立刻崩做了才穩(wěn)」的細(xì)節(jié)。4. 把 malloc 實(shí)驗(yàn)吃透內(nèi)存分配器實(shí)現(xiàn)與隱藏的坑4.1 實(shí)驗(yàn)?zāi)繕?biāo)用 sbrk 實(shí)現(xiàn)一個(gè) first-fit 分配器CS241 的內(nèi)存實(shí)驗(yàn)是整門課里最能打的一個(gè)——要求你用sbrk或mmap實(shí)現(xiàn)malloc、free、realloc和calloc。這個(gè)實(shí)驗(yàn)不要求性能極致但要求行為正確不能踩到已分配的內(nèi)存、不能重復(fù)釋放、不能內(nèi)存泄漏。講義提供的實(shí)現(xiàn)思路通常是隱式空閑鏈表即每個(gè)內(nèi)存塊在頭部放一個(gè) metadata 結(jié)構(gòu)體記錄塊大小和是否空閑。這種設(shè)計(jì)簡(jiǎn)單但存在碎片問(wèn)題這也是后續(xù)討論的切入點(diǎn)。一個(gè)最簡(jiǎn)的 malloc 實(shí)現(xiàn)如下著重看 metadata 和內(nèi)存對(duì)齊的處理#include stdint.h #include unistd.h typedef struct block { size_t size; // 數(shù)據(jù)區(qū)大小不含 metadata int free; // 1 表示空閑 struct block *next; // 下一個(gè)塊 } block_t; #define ALIGN8(x) (((x) 7) ~7) block_t *head NULL; // 鏈表頭指針初始為空 block_t *find_free_block(size_t size) { // 遍歷鏈表找到第一塊足夠大的空閑塊first-fit for (block_t *b head; b ! NULL; b b-next) { if (b-free b-size size) { return b; } } return NULL; } block_t *extend_heap(size_t size) { // 用 sbrk 申請(qǐng)新內(nèi)存并包裝成 block_t 結(jié)構(gòu) block_t *b sbrk(0); // 獲取當(dāng)前程序堆頂 void *request sbrk(sizeof(block_t) size); if (request (void *)-1) { return NULL; } b-size size; b-free 0; b-next NULL; return b; } void *my_malloc(size_t size) { if (size 0) { return NULL; } size ALIGN8(size); // 向上對(duì)齊到 8 字節(jié) block_t *b find_free_block(size); if (b ! NULL) { b-free 0; } else { b extend_heap(size); if (b NULL) { return NULL; } } // 返回 metadata 之后的數(shù)據(jù)區(qū)起始地址 return (void *)(b 1); }這段代碼是對(duì)課程講義中第一階段實(shí)現(xiàn)的簡(jiǎn)化。它的核心邏輯是需要內(nèi)存時(shí)先在現(xiàn)有空閑鏈表里找找不到就調(diào)用sbrk向內(nèi)核申請(qǐng)更多的堆空間。這里有幾個(gè)必須注意的細(xì)節(jié)。ALIGN8宏的作用是保證每次分配的數(shù)據(jù)區(qū)大小都是 8 的倍數(shù)這不是強(qiáng)迫癥而是因?yàn)?CPU 對(duì)非對(duì)齊內(nèi)存訪問(wèn)會(huì)有性能懲罰在部分架構(gòu)上直接報(bào)總線錯(cuò)誤。b 1是 C 語(yǔ)言的指針?biāo)阈g(shù)相當(dāng)于(char *)b sizeof(block_t)即跳過(guò) metadata 區(qū)。調(diào)用者拿到的指針必須能通過(guò)free((void *)ptr)唯一對(duì)應(yīng)到 block_t 頭部所以free實(shí)現(xiàn)的第一步就是把指針往回退一個(gè) block_t 大小。4.2 realloc 的邊界行為與 free 的合并問(wèn)題課后的實(shí)驗(yàn)題里最容易翻車的不是 malloc 本身而是 realloc 和 free 的邊界情況。realloc 的簽名是void *realloc(void *ptr, size_t size)它要求如果 ptr 為 NULL行為等價(jià)于 malloc如果 size 為 0 且 ptr 非 NULL行為等價(jià)于 free 并返回 NULL如果原地空間足夠大可以直接擴(kuò)大當(dāng)前塊并返回原指針?lè)駝t必須新分配一塊、拷貝數(shù)據(jù)、釋放舊塊。很多人漏掉的是最后一步的「拷貝大小取 min(舊大小, 新大小)」如果盲目拷貝舊塊的全部 size可能越界讀到相鄰塊的數(shù)據(jù)。講義都會(huì)強(qiáng)調(diào)這一點(diǎn)但寫代碼時(shí)人很容易圖快直接 memcpy。free 的合并問(wèn)題同樣隱蔽。當(dāng)釋放一個(gè)內(nèi)存塊時(shí)如果相鄰的下一個(gè)塊也是空閑的應(yīng)該合并成一個(gè)大塊否則碎片會(huì)越積越多最后明明總空閑空間足夠卻分配不出連續(xù)的大塊。合并邏輯的難點(diǎn)在于單向鏈表只能向后合并無(wú)法向前合并——你需要通過(guò)遍歷找到前一個(gè)塊或者改用雙向鏈表。課程的標(biāo)準(zhǔn)實(shí)現(xiàn)是雙向鏈表每個(gè) block_t 加一個(gè)prev指針。中文講義在這一點(diǎn)上花了不少篇幅解釋「邊界標(biāo)記」的做法即每個(gè)塊尾部也存一個(gè) size這樣釋放時(shí)可以快速判斷前一塊是否空閑。這個(gè)技巧在實(shí)際項(xiàng)目中很常見(jiàn)但課程實(shí)現(xiàn)為了簡(jiǎn)單通常只做向后合并。關(guān)于 free 還有一個(gè)語(yǔ)義坑傳給 free 的指針必須是之前 malloc 返回的指針不能是塊中間位置的指針也不能是棧變量的地址。檢測(cè)這種錯(cuò)誤的方法是 glibc 的malloc_usable_size或者 valgrind但課程實(shí)驗(yàn)的測(cè)試腳本通常用一堆非法輸入來(lái)暴力測(cè)試比如free((void *)0x1)、free(ptr 1)、realloc(ptr, -1)。處理這些異常輸入的正確姿勢(shì)是統(tǒng)一判斷if (ptr NULL) return;但如果傳進(jìn)來(lái)的是非法地址程序本身也無(wú)法判斷只能靠運(yùn)行時(shí)崩。這也是為什么 malloc 實(shí)驗(yàn)的正確性測(cè)試通常配 valgrind 運(yùn)行而不是直接跑裸程序。5. 常見(jiàn)問(wèn)題排查讀講義做 CS241 實(shí)驗(yàn)時(shí)會(huì)踩的 5 個(gè)坑5.1 實(shí)驗(yàn)文件結(jié)構(gòu)混亂導(dǎo)致 make 失敗現(xiàn)象按講義步驟解壓實(shí)驗(yàn)包后進(jìn)入目錄執(zhí)行make報(bào)出一堆 undefined reference 錯(cuò)誤或者找不到頭文件。原因不是編譯器問(wèn)題而是實(shí)驗(yàn)包依賴的目錄結(jié)構(gòu)不對(duì)。很多中文講義會(huì)把多個(gè)實(shí)驗(yàn)的文件打散在章節(jié)里讀者手動(dòng)復(fù)制時(shí)漏掉了公共頭文件目錄。解決先執(zhí)行find . -name *.h查看所有頭文件的位置再和講義開(kāi)頭的「文件結(jié)構(gòu)」部分對(duì)照確認(rèn)common/或include/被加到了編譯器搜索路徑中。一般 makefile 里會(huì)有-I參數(shù)缺失時(shí)手動(dòng)指定即可。5.2 本地系統(tǒng)是 macOS實(shí)驗(yàn)代碼編譯通過(guò)但運(yùn)行崩潰現(xiàn)象在 macOS 上編譯 CS241 代碼沒(méi)問(wèn)題但一運(yùn)行就段錯(cuò)誤或輸出結(jié)果和 Linux 不一致。原因macOS 的 C 運(yùn)行庫(kù)和 Linux 的 glibc 在行為上有本質(zhì)差異最明顯的是fork后的信號(hào)處理語(yǔ)義和sbrk的線程安全性。另一個(gè)坑是內(nèi)存對(duì)齊macOS 在 Apple Silicon 上 malloc 默認(rèn)按 16 字節(jié)對(duì)齊而課程實(shí)驗(yàn)的 metadata 設(shè)計(jì)按 8 字節(jié)對(duì)齊兩者混用會(huì)直接產(chǎn)生不可預(yù)期行為。解決不要只在 macOS 上跑課程代碼用 Docker 起一個(gè) ubuntu 容器作為標(biāo)準(zhǔn)環(huán)境。這是血淚經(jīng)驗(yàn)系統(tǒng)編程實(shí)驗(yàn)的任何異常行為都先懷疑環(huán)境差異再懷疑代碼邏輯。5.3 valgrind 報(bào)錯(cuò)但課程測(cè)試腳本顯示全部通過(guò)現(xiàn)象測(cè)試腳本的邏輯斷言全過(guò)但 valgrind 報(bào)告 memory leak 或 invalid write。原因測(cè)試腳本只驗(yàn)證了分配器對(duì)外接口的正確性沒(méi)檢測(cè)內(nèi)部?jī)?nèi)存布局的完整性。比如 free 后沒(méi)有把塊的free標(biāo)志置 1但也不影響后續(xù) malloc 的正常分配就會(huì)漏過(guò)測(cè)試卻留下隱患。解決把 valgrind 的輸出當(dāng)作硬指標(biāo)definitely lost大于 0 字節(jié)就算實(shí)驗(yàn)失敗。同時(shí)不要把 valgrind 的運(yùn)行參數(shù)只寫成默認(rèn)的--leak-checkyes建議加上--track-originsyes它會(huì)告訴你未初始化值的來(lái)源是哪個(gè)函數(shù)哪一行排查時(shí)省很多事。5.4 多線程測(cè)試時(shí)概率性卡死現(xiàn)象線程實(shí)驗(yàn)的測(cè)試程序運(yùn)行 100 次偶爾 1 次卡住不動(dòng)CtrlC 才能終止。原因典型的死鎖場(chǎng)景——兩把鎖的加鎖順序不一致線程 A 持有 lock1 等待 lock2線程 B 持有 lock2 等待 lock1。課程實(shí)驗(yàn)通常只是簡(jiǎn)單計(jì)數(shù)器卡死不常見(jiàn)但如果你按講義提示自己寫了讀寫鎖就很容易在寫者優(yōu)先策略里漏掉一個(gè) signal造成寫者線程永遠(yuǎn)等不到條件變量。解決先在代碼里搜所有加鎖順序是否一致再用gdb掛上卡住的進(jìn)程執(zhí)行thread apply all bt查看每個(gè)線程的棧兩個(gè)線程分別停在pthread_mutex_lock和pthread_cond_wait時(shí)基本就斷案了。5.5 對(duì)齊宏換成 16 字節(jié)后 malloc 測(cè)試反而報(bào)錯(cuò)現(xiàn)象為了提高性能把講義里的 8 字節(jié)對(duì)齊改成 16 字節(jié)結(jié)果測(cè)試腳本報(bào)出越界訪問(wèn)。原因不是對(duì)齊方向錯(cuò)了而是 metadata 的大小沒(méi)跟著調(diào)整。如果 block_t 的大小不是 16 的倍數(shù)(b 1)返回的地址依然不是 16 字節(jié)對(duì)齊。解決在 block_t 結(jié)構(gòu)體里顯式加__attribute__((aligned(16)))或者用sizeof(block_t)對(duì)齊到 16。這個(gè)坑的根源是 C 語(yǔ)言結(jié)構(gòu)體的 tail padding 問(wèn)題結(jié)構(gòu)體大小受最大成員對(duì)齊影響不能想當(dāng)然。6. 把這份講義吃透的進(jìn)階用法從讀筆記到做自己的工具集走到這一步說(shuō)明你已經(jīng)不是單純?cè)谒?shí)驗(yàn)了。中文講義的終點(diǎn)不應(yīng)該是課程結(jié)業(yè)而是讓你具備自己設(shè)計(jì)小工具的能力。我的建議是按三條線去加深第一條線是把課程里的 mini shell 擴(kuò)展成一個(gè)真正能用的工具加上作業(yè)控制和管道錯(cuò)誤處理第二條線是把 malloc 實(shí)驗(yàn)換成真實(shí)項(xiàng)目里的內(nèi)存池設(shè)計(jì)用課程學(xué)的 block 結(jié)構(gòu)做對(duì)象池提前分配釋放減少碎片第三條線是用課程里信號(hào)處理的思路去排查線上程序的卡死問(wèn)題比如常見(jiàn)的「進(jìn)程 hang 住」第一反應(yīng)就是用kill -QUIT觸發(fā) thread dump而不是直接重啟。驗(yàn)證自己是否真的吸收了講義的方法只有一個(gè)——不看任何參考從零實(shí)現(xiàn)一遍課程里最難的實(shí)驗(yàn)。如果你能做到說(shuō)明課程的知識(shí)已經(jīng)變成你自己的東西。如果卡住了回頭翻講義時(shí)重點(diǎn)看自己卡住處的「譯者提示」。這份中文講義在翻譯之外最值得學(xué)習(xí)的就是這些提示背后的問(wèn)題意識(shí)。我的一個(gè)習(xí)慣是看完一章后把講義里的英文術(shù)語(yǔ)和 C 標(biāo)準(zhǔn)接口挑出來(lái)逐個(gè)查 man page 并寫一個(gè)小例子驗(yàn)證。這個(gè)過(guò)程很慢但每做一次那些 API 就從「看著眼熟」變成「用著順手」。系統(tǒng)編程的硬功夫就是這么磨出來(lái)的沒(méi)有捷徑但這份中文講義把彎路的數(shù)量砍掉了一大半。希望這些經(jīng)驗(yàn)對(duì)你的學(xué)習(xí)路有幫助。本文還有配套的精品資源點(diǎn)擊獲取