2025q1 Homework1 (lab0)
contributed by <alanhc
>
作業書寫規範:
- 無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
- 文字訊息 (尤其是程式執行結果) 請避免用圖片來表示,否則不好搜尋和分類
- 共筆書寫請考慮到日後協作,避免過多的個人色彩,用詞儘量中性
- 不要在筆記內加入
[TOC]
: 筆記左上方已有 Table of Contents (TOC) 功能,不需要畫蛇添足
- 不要變更預設的 CSS 也不要加入任何佈景主題: 這是「開發紀錄」,用於評分和接受同儕的檢閱
- 當在筆記中貼入程式碼時,避免非必要的行號,也就是該手動將
c=
或 cpp=
變更為 c
或 cpp
。行號只在後續討論明確需要行號時,才要出現,否則維持精簡的展現。可留意「你所不知道的 C 語言: linked list 和非連續記憶體」裡頭程式碼展現的方式
- HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用
diff
標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」
- 留意科技詞彙的使用,請參見「資訊科技詞彙翻譯」及「詞彙對照表」
- 不要濫用
:::info
, :::success
, :::warning
等標示,儘量用清晰的文字書寫。:::danger
則僅限授課教師作為批注使用
- 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景
- 在中文敘述中,使用全形標點符號,例如該用「,」,而非 ","。注意書名號的使用,即
〈
和 〉
,非「小於」和「大於」符號
- 避免使用不必要的 emoji 字元
加油!
- 由於寫到後面幾週心態有點崩,這些東西怎麼之前都不知道,有幾句話想做精神喊話:
- 你不必非常出色,只要在很長的時間內,保持比其他人聰明一點點就夠了 - 查理蒙格
- 誠實的面對自己 - Jserv
- 青春很貴,你也知道實習會發生什麼事,公司不會指派重要的工作給你,他們只會指派低風險的工作,你學習到的東西並不會比你現在多。你該學習的不是看到事情要完蛋了就去避免失敗,而是應該學習如何處理與承受失敗,你才能變得比以前更強大。
Goal
C 語言進階議題與實作能力
- 掌握 C 語言深層機制:像是 setjmp/longjmp、signal 處理、記憶體管理(malloc/free 等),都是系統級程式設計不可或缺的能力。
- 理解變數參數處理(如 va_list 等):讓你能撰寫更通用的 API。
- Linux 核心資料結構實作:例如雙向鏈結串列 (list_head) 與 queue.c 的高效操作,並強調 O(1) 的時間複雜度需求。
系統設計與除錯觀念
- 記憶體錯誤偵測工具:如 Valgrind、ASan(Address Sanitizer)不只幫助除錯,也是訓練你養成可靠程式開發的工具。
- Cppcheck & 靜態分析訓練:培養你「在編譯前就能預測錯誤」的能力。
- 強化程式風格與一致性:透過 clang-format 與 Git hook 強化開發流程的規範。
效能與安全驗證導向的實作訓練
- 使用 dudect 進行時間複雜度的實驗性驗證:這比單純的 Big-O 理論更實際,老師可能希望你們跳脫紙上談兵,開始關心實際效能差異。
- CERN 的安全編碼建議:移除如 gets/sprintf/strcpy 等危險函式,建立「預防性安全意識」。
開發工具與工作流程導入
-
Git 實務操作:不只是基本操作,而是要學會 branch 管理、hook、自動化測試整合,甚至在 pre-push 時驗證程式碼品質。
-
良好 commit message 的建立:結合 git-good-commit,希望你們習慣撰寫清楚、具描述性的提交訊息——這對開源貢獻或團隊協作都極其重要。
-
Makefile 模組化:訓練你寫出可維護、可擴充的建構系統。
-
這們課的學習內容非常多及扎實,為了記憶方便及長久能靈活運用,先試著猜測這個作業的task目標與其意義
-
學習目標
- 機率統計
- DSA
- 目的: Linux 底層很多指標,好的資料結構可用於
- DevOps/CICD
- Git pre-commit hook
- Linter
- clang-format: 一一致的程式風格
- GNU Aspell: 拼字檢查
- Code Quality
- Cppcheck: 靜態程式碼分析
- Valgrind: 動態程式分析
- Testing
- 除錯
- 創新
File
程式結構與功能
- Makefile:
定義了構建規則和測試目標(如 check 和 valgrind)。
支持多平台構建(如 macOS 和 Linux)。
- harness.h:
提供自定義內存分配函數(如 test_malloc 和 test_free)。
支持檢測內存分配錯誤和限制內存分配模式。
實現了命令行解析和執行。
提供了命令添加功能(如 add_cmd)。
初始化測試環境(如隨機數種子、命令行歷史)。
提供交互式命令行和測試功能。
快速開始
熟悉隊列接口:
查看 queue.h,理解需要實現的函數和數據結構。
構建與運行測試:
使用 make 構建項目,並運行 qtest 測試程序。
增量開發與測試:
修改 queue.c,逐步實現功能,並使用 driver.py 驗證。
內存檢查:
使用 valgrind 確保內存分配和釋放正確。
Lab
Links
環境
開發紀錄
間接指標 (indirect pointer)
Queue.[ch]
q_new: 建立新的「空」佇列;
- 根據 cppreference, 要記憶體要使用malloc:
void *malloc( size_t size );
- 可使用
list.h
裡面的 INIT_LIST_HEAD
- inline: 告訴編譯器將這個函式展開,直接將函式內容插入呼叫它的地方,而不是執行標準的函式呼叫(call 和 return)。
關鍵字 |
作用 |
為何使用? |
static |
限制作用範圍 |
只在當前編譯單元內可見,避免命名衝突 |
inline |
提高執行效率 |
內聯展開,減少函式呼叫開銷 |
void |
無返回值 |
只執行初始化操作,無需返回值 |
- 解釋:INIT_LIST_HEAD將雙向指標的next及prev都指向 list的head
q_free: 釋放佇列所佔用的記憶體;
- 其list_for_each_entry_safe在list.h被定義
#define list_for_each_entry_safe(entry, safe, head, member) \
- entry → 當前遍歷的節點(對應 element_t *)
- safe → 儲存下一個節點的指標,確保刪除當前節點後仍能繼續遍
- head → 指向 list_head 的頭節點
- member → 結構內的 list_head 成員變數(在 element_t 內就是 list)
- 遍歷到直到回到head
- queue.h 裡面的 q_release_element釋放
- test_free 是被定義在 harness.c ,有安全釋放的技巧
- noallocate_mode: 是否允許釋放記憶體
- MAGICFOOTER: 驗證記憶體是否完整
- magic_header、find_footer 標記MAGICFREE代表已經釋放
- free() 釋放
- 更新分配計數 allocated_count
q_insert_head: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則);
- 用malloc初始化記憶體記憶體
- 用strdup 產生字串副本
- list_add 將指標加入List
- list_add 由 list.h定義
q_insert_tail: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則);
q_remove_head: 在佇列開頭 (head) 移去 (remove) 給定的節點;
q_remove_tail: 在佇列尾端 (tail) 移去 (remove) 給定的節點;
- 由於是雙向Linked List因此可藉由q_remove_head反推
q_size: 計算目前佇列的節點總量;
- 如果head=NULL, 長度0
- 使用
list.h
定義好的marco list_for_each 去迭代,讓len++
q_delete_mid: 移走佇列中間的節點,詳見 LeetCode 2095. Delete the Middle Node of a Linked List
- 左右指標相遇即爲中間點
- 元素個數 奇數 及 偶數 要不同處理方式
- 當 left == right 或 left->next == right 時,代表 right 指向的就是中間節點
- 奇數個元素時:最後 left == right,right 就是要刪除的節點。
- 偶數個元素時:最後 left->next == right,選擇 right 為要刪除的節點。
q_delete_dup: 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 LeetCode 82. Remove Duplicates from Sorted List II
- 解釋流程:
- head → A → A → B → C → C → D → NULL
- 刪除重複元素的過程如下:
- A 和 A 相同,標記 cut。
- B 不等於 A,所以 list_cut_position 把 A → A 移到 pending。
- C 和 C 相同,標記 cut。
- D 不等於 C,所以 list_cut_position 把 C → C 移到 pending。
- 遍歷 pending,釋放 A → A → C → C 的記憶體。
- LIST_HEAD(pending); :暫存重複元素
- if (&safe->list != head && !strcmp(safe->value, it->value)) 找出重複元素
- Detect duplicated elements:將重複元素切到pending
q_swap: 交換佇列中鄰近的節點,詳見 LeetCode 24. Swap Nodes in Pairs
q_reverse: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點;
- list_for_each_safe: 安全遍歷的marco, safe代表安全儲存的下個節點
- list_move: 將 迭代的it移到head
- TODO: 如果是雙向 應該可以直接翻轉
q_reverseK: 類似 q_reverse,但指定每 k 個節點為一組,進行反向順序排列,詳見 LeetCode 25. Reverse Nodes in k-Group
- Goal: 每k個翻轉一次,最後不足k不翻轉

q_sort: 以遞增順序來排序鏈結串列的節點,可參閱 Linked List Sort 得知實作手法;
q_ascend 及 q_descend: 參閱 LeetCode 2487. Remove Nodes From Linked List,注意節點間的順序
- 從尾端開始,移除比右邊小或相等 的節點
- 13-14 找前一個節點
- 15-21 移除較小的節點
q_merge: 合併所有已排序的佇列,並合而為一個新的已排序佇列,詳見 LeetCode 23. Merge k Sorted Lists
- 2-merge 優化:小 queue 優先合併,大 queue 先不動,避免「大吃小」,讓合併的 cost 分攤得更平均。
- pending:用來存放還沒合併的 queue。
- empty:用來存放被合併完的 queue_contex_t(可視為空)。
- 18-23: 每當 pending 有超過 3 個 queue,檢查前三個 queue 的 size。
- 24-25: 若 y 已經大於等於 z 的 2 倍,暫不合併,直接 break。
- 27-35: 合併
- 如果 x 比 z 小,就把 x merge 到 y。
- 否則就把 y merge 到 z。
- 被 merge 掉的 queue_contex_t(空殼)丟到 empty,n–。
- 收尾
- 40-47: 最後 pending 只剩 1~2 個 queue,直接硬合併剩下的。
- 為何有效率?
- 跟傳統的「直接從 k 個 queue 依序合併」相比,這個 2-merge 策略可以讓每次合併時的兩個 queue size 差距較小,減少單次 merge 的成本(不會讓小 queue 一下子被 merge 到超大 queue 裡面)。
- 例子解釋
答案:
- 比較合併
- x 的大小為 3,z 的大小為 2。
- 因為 x 比 z 大,所以我們將 x 和 y 合併。
- 合併後,我們將合併結果放入 empty 中,並從 pending 中移除 x 和 y。
- 因此
q1 + q2 -> [1] -> [2] -> [4] -> [5] (這是合併結果)
pending: [q3]
empty: [1] -> [2] -> [4] -> [5]
- 第二次合併
- 現在 pending 中只有 q3,所以 pending 中剩下的只有一個 queue,不需要再進行進一步的合併。
運用 Address Sanitizer 除錯
make SANITIZER=1
運用 Valgrind 排除 qtest 實作的記憶體錯誤
運用 Valgrind Massif 觀察 simulation 之記憶體使用情形

提供新的命令 shuffle
Background
Definition:
虛無假說(Null Hypothesis):用符號表示為 H₀,指的是「我們希望用資料來推翻的初始假設」。
對應的對立假說(Alternative Hypothesis,H₁ 或 Hₐ),就是你想證明的新觀點。
- 實際例子 1:藥物實驗
問題:這個新藥物真的能有效降低血壓嗎?
- 虛無假說 H₀:這個新藥「對血壓沒有影響」。
- 對立假說 H₁:這個新藥「能有效降低血壓」。
然後我們收集數據(比如 100 個人服藥後的血壓變化),計算 p 值,來決定是否要「拒絕虛無假說」。
檢定流程簡圖
- 設定 (虛無假說)與 (對立假說)
- 選擇適當的檢定方式(t 檢定、卡方檢定等)
- 設定顯著水準 (例如 0.05)
- 計算 值
- 如果 → 拒絕 ,接受
常見誤解
❌ 「接受虛無假說」≠「虛無假說正確」
✅ 正確說法應該是:「目前沒有足夠證據拒絕虛無假說」
想驗證什麼?
這個洗牌演算法真的有把每種排列弄得一樣機會出現嗎?
這就是你的虛無假說(H₀):
H₀: 每一種排列出現的機率相同(符合均勻分布)
對立假說(H₁)則是:
H₁: 至少有一種排列出現的機率不同(不均勻)
為何使用 Pearson's Chi-squared Test?
- Chi-squared test 是用來比較「實際出現次數 vs. 預期次數」的一種方法,非常適合做這種「分布是否均勻」的檢定。
假設你跑洗牌 10,000 次,陣列長度是 3:
所有可能排列為
[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]
每一種應該出現約 1,667 次(= 10000 / 6)
你實際記錄每一種出現的次數,代入 Chi-squared test 公式:

- 如果洗牌是公平的,實際出現次數應該接近理論值(均勻)
- Chi-squared test 幫你比較這兩者差異
若差異太大(p 值太小),代表可能不是公平洗牌 → 拒絕虛無假說
若差異合理(p 值夠大),代表沒足夠證據說這個 shuffle 有問題 → 不拒絕虛無假說
補充
什麼情況不能用這方法?
當可能的排列太多(像長度為 10 的陣列會有 3628800 種),你幾乎無法跑夠多次來覆蓋所有排列。 → 這時可以轉而分析「位置分布是否均勻」等簡化的統計特徵。
實驗
#cmd 並不是保留字,也不需要在其他地方定義。它是 C 預處理器中的一個特殊語法,稱為字符串化操作符(stringizing operator)。當你在宏中使用 # 時,預處理器會將後面的參數轉換為字符串。
- console.c
- 呼叫
- 其中 operation 是一個function pointer 在console.h被定義
Fisher-Yates algorithm
PRNG (Pseudo Random Number Generators)
測試亂數均勻分佈
https://hackmd.io/@sysprog/linux2025-lab0/%2F%40sysprog%2Flinux2025-lab0-d#測試程式
https://github.com/HenryChaing/lab0-c/blob/master/queue.c
https://github.com/Dennis40816/lab0-c/commit/0d63c0e8e3cc2d4e06e7374b396a07bf26cb54b4
研讀論文並改進 dudect
Bakcground
- 使用一些樣本來測試程式複雜度是否是常數時間,可使用Student’s t-distribution
Student’s t-distribution
- 解釋:Student’s t-distribution 是一種機率分布,用來描述「樣本平均值與母體平均值之間的差異」在標準化後的行為,當母體標準差未知,且樣本數較小(例如 n < 30)時特別有用。
- 適用:樣本數不多的情況下進行估計與假設檢定
小整理 
Dude, is my code constant time?
展開會變成
用gcc -E -P看
主要測試func.
分析記憶體問題 - valgrind
Memory Leak
- Definitely lost(明確遺失)
- 定義:你配置了記憶體(malloc),但之後再也沒有指標指向它,也沒釋放。
- 代表你真的忘記 free(),而且這段記憶體永遠找不回來。
- Indirectly lost(間接遺失)
- 定義:有一個記憶體區塊被 malloc,但它是透過另一塊記憶體間接存取;而當「父記憶體」也 lost 時,這塊記憶體也失去參考。
- Still reachable(仍可存取)
- 記憶體仍有變數指向,還能 free,但你沒 free。
- 常發生在 global 變數或 static buffer 沒手動釋放。
- Possibly lost(可能遺失)
- 定義:記憶體指標有點奇怪,Valgrind 無法確定是不是 lost。
- 例如:偏移了原本指標、複製錯誤或多次 free()。

Invalid memory acc
- Invalid memory access 有時不會立即造成 segmentation fault,所以不會有 core dump 可分析
分析記憶體使用狀況
- Massif
可分析
Heap blocks
Heap administration blocks
Stack sizes.
添加cmd
- q_test
- main → run_console → cmd_select → interpret_cmd → parse_args
- 因此新增cmd步驟,以hello 為例
- 在console.c新增function
- 在init_cmd新增
ADD_COMMAND(hello, "Print hello message", "");
網頁伺服器
- linenoise.c
- linenoise()->line_raw()->line_edit() 裡面的while(1)
- 用 read()會阻塞
- e.g. nread = read(l.ifd, &c, 1);
- CS:APP RIO 套件
- RIO
- Unbuffered input and output functions. : no application-level buffering.
- Buffered input functions: buffered RIO input functions are thread-safey
- 《CS:APP》設計的一個 I/O 套件,用來解決「Unix I/O 在網路程式或多執行緒環境中容易出現的短讀(short count)與不安全問題」。
- RIO 原理
- Unbuffered I/O(無緩衝)
- 函式: rio_readn() 與 rio_writen()
- 功能: 直接在使用者記憶體與檔案描述符間傳輸資料(類似 read/write),但會自動處理短讀/短寫(short count)。
- 場景: 適合傳送/接收 binary 資料(尤其是網路 socket),如 Web 伺服器的檔案傳送。
- 考量點:
- 使用者無需處理迴圈讀寫。
- 自動處理 read()/write() 被 signal 中斷(EINTR)的情況。
- 遇 EOF 才返回 short count,否則會讀滿為止。
- Buffered I/O(有緩衝)
- 與其他比較

- 為何需要 RIO?
- 網路讀寫的不可預測性:你可能只收到一部分封包,所以必須「堅持讀到完整資料」。
- C 標準函式庫有 static buffer,非 thread-safe。
- 低階 Unix I/O 不緩衝、也不處理 short count,要自己寫迴圈處理。
- RIO 把這些都包好,保證讀寫的 robustness。
解釋 select 系統呼叫在本程式的使用方式
-
什麼是select?
- select() 是一種 multiplexing I/O(多工 I/O) 技術,可以 同時監控多個檔案描述符(file descriptor)是否準備好做 I/O 操作。
- 常見用途
- 製作 簡易 TCP server
- 建立 聊天室服務
- 撰寫 shell 或交互式工具
- 取代 read() 阻塞操作,提高反應性
- 技術重點
- fd_set 是個 位元集合(bit mask),由 FD_ZERO、FD_SET、FD_ISSET 來操作。
- select() 是一個 阻塞式系統呼叫,直到:
- 有指定的 fd 準備好(可讀/可寫)
- 或者發生錯誤
- 或者 timeout
- 比較

- 例子
- 等待 5 秒內是否有標準輸入可讀,如果有,就馬上回應。
-
console.c
> do_web
-
web.c > web_eventmux
- 3-11 設定要監聽哪些fd
- 12-14:
- 如果有輸入或連線進來,select() 會喚醒(否則會阻塞)
- 若發生錯誤(如訊號中斷),回傳 -1
- 16-20:
- 使用 FD_ISSET() 判斷是否是 server_fd 被觸發
- 使用 accept() 接收該連線,得到新的 socket web_connfd
- 23-30
- 呼叫 web_recv() 讀取 client 的請求內容(可能是 HTTP GET)
- 把接收到的訊息儲存到 buf 中回傳
- 33-34: 表示是使用者輸入觸發的,但函式這裡並沒有真的讀入使用者的輸入內容
透過 Massif 視覺化 simulation 過程中的記憶體使用
-
閱讀與分析這張 Heap 記憶體消耗圖 (Memory Chart)
-
X 軸:時間刻度 time in i
- 單位 i = CPU 指令數 (instructions executed),Massif 以「程式執行到第幾條指令」作為時間基準。
- 數字範圍常呈指數級,例如從 0 → 1e11 → 2e11。
-
Y 軸:Heap 大小
- 右側附加刻度顯示對應的 KiB / MiB。
- 例:圖頂寫 Peak of 1.1 MiB at snapshot #1,表示 最高峰的有效佔用 (in‑use) 為 1.1 MiB,出現在 snapshot #1。
-
不可用address senitizer編譯過的qtest

-
_IO_file_doallocate:一開始有印一些東西,屬於這
-
doit: dudect/fixture.c來測試的func,會隨著測資越來越大佔用記憶體越多
