# 2025q1 Homework1 (lab0) contributed by < `BrotherHong` > ## Reviewed by BennyWang1007 1. 在 `q_insert_head`、`q_insert_tail` 中,修正未完全釋放記憶體的 commit,考慮呼叫 `free()` 的條件是 `!e`,也就是 `e` 為 NULL,此時沒有必要呼叫 `free()`。\ (根據 ISO C 標準 (ISO/IEC 9899:1999, §7.20.3.2), If ptr is a null pointer, no action occurs.) 2. 在 `q_sort` 中,判斷選擇哪個鏈結串列時,可以考慮不要使用乘法。 3. 筆記和 commit message 寫得很詳盡,很不錯。 4. 缺乏的部分如 tiny web server 等,有空可以慢慢補上。 {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 16 On-line CPU(s) list: 0-15 Vendor ID: GenuineIntel Model name: 13th Gen Intel(R) Core(TM) i7-13620H CPU family: 6 Model: 186 Thread(s) per core: 2 Core(s) per socket: 10 Socket(s): 1 Stepping: 2 CPU(s) scaling MHz: 10% CPU max MHz: 4900.0000 CPU min MHz: 400.0000 BogoMIPS: 5836.80 Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 416 KiB (10 instances) L1i: 448 KiB (10 instances) L2: 9.5 MiB (7 instances) L3: 24 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-15 ``` ## 針對佇列操作的程式碼實作 ### q_new > commit [726f122](https://github.com/BrotherHong/lab0-c/commit/726f1229caa88983711ba474ba521dd417ff8c21) * 建立一個空的 queue * 需要注意的點是在要 `head` 的記憶體空間時有可能會因為失敗而回傳 `NULL` ,這是我以前時常沒注意到的地方 ```c if (head) { INIT_LIST_HEAD(head); } ``` ### q_free > commit [4962106](https://github.com/BrotherHong/lab0-c/commit/4962106b2728de6342df467d3a47a530f86ffb4a) * 釋放掉所有被 `queue` 使用到的記憶體 * 若 `head` 本身為 `NULL` 直接結束 * 使用 `list.h` 中的 `list_for_each_entry_safe` 將所有 `element_t` 用到的記憶體釋放掉 * 最後釋放掉 `head` 本身使用的記憶體 ### q_insert_head, q_insert_tail > commit [f61b5c5](https://github.com/BrotherHong/lab0-c/commit/f61b5c5cffc6acca35962b57730f74d79f38fa79), [cf81496](https://github.com/BrotherHong/lab0-c/commit/cf81496d9fffba981e82ea87071ed470e2237279) * 插入一個 `element` 到 `queue` 的頭/尾 * 若 `head` 本身為 `NULL` 直接回傳 `false` * 配置記憶體給 `element` 用,並初始化 `list` * 若配置失敗則回傳 `false` * 配置記憶體給字串 `value` 用 * 若配置失敗則**先釋放** `element` 的記憶體,再回傳 `false` * 若以上記憶體配置都成功,將字串複製進 `value` 中 * 最後將 `list` 放入 `queue` 完成插入操作 * 程式碼片段:配置記憶體給字串,並將字串複製進 `value` * 此份程式碼沒有考慮到要先將 `element` 用到的空間先釋放掉再回傳 * 若在配置記憶體給字串時失敗,在程式結束會顯示尚有記憶體空間未被釋放 ```c size_t len = strlen(s); if (!(e->value = malloc(sizeof(char) * (len + 1)))) return false; strncpy(e->value, s, len); e->value[len] = '\0'; ``` * 之後重新把 `insert` 操作重寫並簡化了字串複製操作 * 修正未完全釋放記憶體 (commit [a173291](https://github.com/BrotherHong/lab0-c/commit/a173291fade44ae2f04e57efebe7a5664a229546)) * 使用 `strdup` 取代自行配置空間並用 `strcpy` 複製字串 (commit [e0cff8c](https://github.com/BrotherHong/lab0-c/commit/e0cff8cd97fb793dbc6a0dd585891d6af7aa1e03)) ```c e->value = strdup(s); if (!e->value) { free(e); return false; } ``` ### q_remove_head, q_remove_tail > commit [a439e04](https://github.com/BrotherHong/lab0-c/commit/a439e043243123bb993830d005d6585917a0dc07), [71211e2](https://github.com/BrotherHong/lab0-c/commit/71211e26d6c07ba1434e21dca606f4e5f6e3df21) * 從 `queue` 的頭/尾移除一個 `element` ,並回傳被 **remove** 的 `element` * 檢查 `queue` 是否為 `NULL` 或 `empty` * 若 `sp` 為 `non-NULL`,將 `value` 複製至 `sp` * 最後,從 `queue` 中 **remove** 該 `element` 並回傳 ### q_size > commit [60c86c8](https://github.com/BrotherHong/lab0-c/commit/60c86c828430dcd9dc0401b067b165fc3af03637) * 計算 `queue` 中有多少個 `element` 存在 * 迭代過每個元素,同時使用 `len` 來記錄數量 ### q_delete_mid > commit [32977aa](https://github.com/BrotherHong/lab0-c/commit/32977aa703ba464978bd6c23bce00fbab742e366) * 刪除 `queue` 中位於中間的 `element` * 更精確地說,若 `queue` 的長度為 `n` 則刪除第 `⌊n / 2⌋` 個 `element` (**0-based indexing**) * 換個想法,對於奇數數量元素的 `queue` 要刪除的點就是中間那個;偶數數量則是切半後右半邊最靠近中間的點 * 奇數例如:`0 1 2 3 4` 刪除的是 `2` * 偶數例如:`0 1 2 3 4 5` 刪除的是 `3` * 這邊使用的方式是利用雙向鏈結串列的好處,使用 `front` 和 `back` 指標分別由前和後同時往中間移動,直到兩者指向**同個元素**或**相鄰**為止 * 找到中間的元素後,將它從 `queue` 中移除並釋放與其相關的記憶體 * 程式碼片段:尋找中間的元素 * `while` 迴圈結束後,`back` 會指向我們要找的點 ```c struct list_head *front = head->next, *back = head->prev; while (front != back && front->next != back) { front = front->next; back = back->prev; } ``` ### q_delete_dup > commit [576aa75](https://github.com/BrotherHong/lab0-c/commit/576aa75a34faff172b1edd4d4e17bce7ee761adc) * 將 `queue` 中所有連續且 `value` 相同的元素全部刪除 * 例如:`1 2 2 4 3 4 4 4 5` 會變成 `1 4 3 5` * 首先建立一個 local 的 `list` `tmp_head`,用來記錄要保留的元素 * 再來從 `queue` 的前端往後找 `value` 相同的元素直到不相同為止 * 令 `node` 指向第一個元素,並使用 `last` 指標來記錄連續相同元素的尾端**的下一個元素** * 例如:`1 1 1 2(<last) 3` * 若 `node->next == last` 代表沒有重複發生 * 將 `node` 放入 `tmp_head` * 若 `node->next != last` 代表有連續重複的元素 * 持續從前端刪除元素直到遇到 `last` * 當 `head` 為空時表示已經將所有連續重複的元素都移除並存在 `tmp_head` 裡面,最後利用 `list_splice` 把所有元素移至 `head` 完成操作 ### q_swap > commit [03e540e](https://github.com/BrotherHong/lab0-c/commit/03e540e7dc0c389871aff6687a4ea15cf805edec) * 把 `queue` 中由前往後兩兩相鄰的元素不重疊地互換位置,多餘的則維持原樣 * 比如說,`1 2 3 4 5 6 7` 經過操作後會變成 `2 1 4 3 6 5 7` * 這邊使用指標的指標 `node` 來指向**指向**兩兩相鄰的第一個節點 * 後來想想使用指標也能有同樣效果,也能少宣告一個指標變數 * 用 `node1` 和 `node2` 分別表示相鄰的左和右節點 * 將 `node1` 用 `list_del` 把它從 `queue` 中移除,再把它插入在 `node2` 後面 * 如此就完成一組的交換,下一組的左節點即是目前 `node1` 的下一個節點 * 重複以上操作即可完成 `q_swap` 的要求 ### q_reverse > commit [6853cdb](https://github.com/BrotherHong/lab0-c/commit/6853cdb5cf4bc0280d67855b17f305a720e5dce2) * 把 `queue` 中的元素順序反轉 * 將每個節點(包含`head`)的 `next` 和 `prev` 分別指向 `prev` 和 `next` 指到的節點就能達成 * 這邊利用 `list.h` 中的 `list_for_each_safe` 會保存下一個節點的特性,直接對每個節點做上述的操作 * 最後還要對 `head` 也做一樣的操作,因為迴圈不包含它 ### q_reverseK > commit [b0d2f49](https://github.com/BrotherHong/lab0-c/commit/b0d2f49f2fcec84ac06b995e472a13d121693b8a) * 對 `queue` 中的元素由前往後每 `K` 個一組並反轉其順序,不足 `K` 個則維持原樣 * 對 `reverse` 的想法:由後往前依序將每個元素插入尾端即能完成反轉 * 實作:建立一個迴圈 * 從 `queue` 的前端開始找 `K` 個元素,並依照由後往前的順序移除並插入 `queue` 的尾端 * 若遇到最後不足 `K` 個元素,則依照原順序移除並插入尾端 * 細節的處理方法 * 如何知道哪些元素尚未被操作過? * 一開始利用 `last` 紀錄尾端的元素 * 若操作到 `last` 則代表已到尾端 ### q_sort > commit [18b65b8](https://github.com/BrotherHong/lab0-c/commit/18b65b82d305f84761a853d68abe1d4f11355495) * 依照 ascending/descending 排序 `queue` 的元素 * 以 `merge sort` 來實現 * 用 `fast-slow pointer` 找到中間點,把左半邊放到 `list_left` ,右半邊繼續放在 `head` * 遞迴排序完左右半邊後合併 * 有另外新增 `q_merge_two` 將兩個 `list` 合併 ### q_ascend q_descend > commit [0314025](https://github.com/BrotherHong/lab0-c/commit/0314025106b138c4bde6e31e7b6a480924e9a62d) * 刪除所有**右方存在小於/大於自己**的元素,使 `queue` 中的元素呈現單調**遞增/遞減**的樣子 * 假想 `queue` 為一個 stack,令前端為 stack 的底部,`node` 指向 stack 的 top * 讓 `node` 從第一個元素持續往尾端前進,每次移動時就像是往 stack push 一個元素 * 在放入 stack 之前,持續檢查 top 的元素是否**大於/小於**自己,若成立則將元素 pop 出來,也就是將此元素 delete 掉 * 持續將元素放入 stack 並遵照上述操作直到所有元素都被操作過 ### q_merge > commit [bbbce6f](https://github.com/BrotherHong/lab0-c/commit/bbbce6fe3d2232ac9a9160ddb05fa4fd62653476) * 將 `k` 個 `sorted list` 合併成一個 `sorted list` * 透過 function 參數的 `head` 找出該鏈結串列所有節點所在的 `queue_context` * 利用已寫好的 sub-function `q_merge_two` 一個一個合併到第一個 `list` 中 ## 在 `qtest` 提供新的命令 `shuffle` > commit [7b9debb](https://github.com/BrotherHong/lab0-c/commit/7b9debb7a6c1e409cca06b053f2c311443b51502) ### 實作 Fisher-Yates shuffle ```c void q_shuffle(struct list_head *head) { int len = q_size(head); struct list_head *new = head->prev; while (len) { int random = rand() % len; struct list_head *old = head->next; while (random--) { old = old->next; } /* Swap value */ element_t *old_entry = list_entry(old, element_t, list); element_t *new_entry = list_entry(new, element_t, list); char *tmp = old_entry->value; old_entry->value = new_entry->value; new_entry->value = tmp; len--; new = new->prev; } } ``` ### 新增指令到 `qtest` ```c ADD_COMMAND(shuffle, "Shuffle the nodes of the queue", ""); ``` ### 統計方法驗證 `shuffle` 執行[測試程式](https://hackmd.io/@sysprog/linux2025-lab0/%2F%40sysprog%2Flinux2025-lab0-d#%E6%B8%AC%E8%A9%A6%E7%A8%8B%E5%BC%8F)得到以下結果 ```sh Expectation: 41666 Observation: {'1234': 41553, '1243': 41674, '1324': 41688, '1342': 41988, '1423': 41841, '1432': 41575, '2134': 41667, '2143': 41670, '2314': 41819, '2341': 41410, '2413': 41431, '2431': 41861, '3124': 41531, '3142': 41824, '3214': 41577, '3241': 41697, '3412': 41595, '3421': 41622, '4123': 41437, '4132': 41476, '4213': 41477, '4231': 41821, '4312': 42022, '4321': 41744} chi square sum: 16.27883646138338 ``` 使用 `matplotlib` 繪製成圖表如下 ![shuffle](https://hackmd.io/_uploads/Bky5Crhike.png) 想要知道這個 shuffle 的結果是否是 Uniform distribution,所以進行以下假設檢定: * $H_0$ (虛無假設):此 shuffle 的每一種方式的機率皆相同,遵照 Uniform distribution * $H_1$ (對立假設):此 shuffle 的機率至少有一個不同 1. 由以上程式計算已知 $\chi^2 = 16.27883646138338$ 2. 自由度 $df=23$ (總共 24 種排列方式且所有方式的機率總和為1) 3. 令**顯著水準** $\alpha = 0.05$,並透過[查表計算](https://datatab.net/tutorial/chi-square-distribution)得到 $p=0.8431 \gt 0.05$ 4. 結論:結果並不顯著,因此不拒絕虛無假設 $H_0$ ,換句話說,沒有證據說明此 shuffle 不是 Uniform distribution ## 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉 ### 簡介 論文提出了一種方法可以檢測一支程式是否以**常數時間** (constant time) 執行,並且此種方法是基於**洩漏檢測** (leakage detection) 的方式,有別於以往許多方法需要以模擬硬體的方式來實現 (如:`ctgrind`,`ctverif`)。 ### 實現方法:`TIMING LEAKAGE DETECTION` > Our approach in a nutshell is to perform a leakage detection test on the execution time. 簡單來說,論文使用的方法就是對程式的**執行時間** (execution time) 做洩漏檢測。 >[!Note]問題 :thinking_face: >為何對執行時間做洩漏檢測就能判斷是否是以常數時間來執行? >怎樣的實驗結果才算是時間洩漏 (timing leakage)? >時間洩漏 (timing leakage) 的定義是什麼? 實現方法可以分為三個步驟: 1. 測量執行時間 (Measure execution time) a. Classes definition b. Cycle counter c. Environmental condition 2. 資料後處理 (Apply post-processing) a. Cropping b. Higher-order preprocessing 3. 統計分析 (Apply statistical test) a. t-test b. Non-parametric tests > TODO... ### `qtest` 的 simulation 實作 主要執行測試的起點在 `fixture.c` 中的 `test_const` 方法 * 一些 macro 的值如下 * `TEST_TRIES = 10` * `ENOUGH_MEASURE = 10000` * `N_MEASURES = 150` * `DROP_SIZE = 20` ```c static bool test_const(char *text, int mode) { bool result = false; t = malloc(sizeof(t_context_t)); for (int cnt = 0; cnt < TEST_TRIES; ++cnt) { printf("Testing %s...(%d/%d)\n\n", text, cnt, TEST_TRIES); init_once(); for (int i = 0; i < ENOUGH_MEASURE / (N_MEASURES - DROP_SIZE * 2) + 1; ++i) result = doit(mode); printf("\033[A\033[2K\033[A\033[2K"); if (result) break; } free(t); return result; } ``` * 各個變數用途 * `mode` : 分辨測試的操作是哪一個 (insert/remove head/tail) * `t` : 用來儲存 t-test 所需要的資料 * 每一次 TEST_TRY 都會在 `init_once` 被初始化 * `result` : 紀錄測試結果是否為 constant time * 運作流程 * `test_const` 提供了 `TEST_TRIES` 次機會來檢測該指定操作的執行時間是否是 constant time * 每一次機會都會在 `doit` 這個方法中測量(measure)並檢驗結果**數次**,將最終(?)結果存在 `result` >這邊的數次是寫成 `ENOUGH_MEASURE / (N_MEASURES - DROP_SIZE * 2) + 1` 次,直接展開的話會是 `10000 - (150 / 20 * 2) + 1 = 9986` 次,目前還不知到為何會這樣設定 :thinking_face: >[!Note]最終結果? >`result = doit(mode);` 這行程式碼表示只要迴圈最後一圈的結果為 `true` 就好了? * 只要有一次檢驗結果為 `true` 即代表此操作程式**可能是**常數執行時間 >在這邊只要至少有一次過, `make test` 就會讓你拿到分 * `fixture.c` : **doit** 方法 * **prepare_inputs**:預處理 input 資料 * **measure**:實際測量執行時間,並將執行前後的 `cpu cycles` 分別存在 `before_ticks` 和 `after_ticks` * **differentiate**:計算 `before_ticks` 和 `after_ticks` 的差來得到執行時間 `exec_times` * **update_statistics**:使用測量出來的 `execc_times` 更新統計分析所需的相關參數 * **report**:根據統計分析回報此次檢驗是否可能是 constant time ```c static bool doit(int mode) { /* Allocate memory */ // ... prepare_inputs(input_data, classes); bool ret = measure(before_ticks, after_ticks, input_data, mode); differentiate(exec_times, before_ticks, after_ticks); update_statistics(exec_times, classes); ret &= report(); /* Deallocate memory */ // ... return ret; } ``` * `constant.c` : `measure` 方法 ```c for (size_t i = DROP_SIZE; i < N_MEASURES - DROP_SIZE; i++) { char *s = get_random_string(); dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * CHUNK_SIZE) % 10000); int before_size = q_size(l); before_ticks[i] = cpucycles(); dut_insert_head(s, 1); after_ticks[i] = cpucycles(); int after_size = q_size(l); dut_free(); if (before_size != after_size - 1) return false; } ``` ## 研讀並嘗試引入 `lib/list_sort.c` 到 `lab0-c` ## Rio 套件 > [book](https://csapp.cs.cmu.edu/2e/ch10-preview.pdf) ### Unix I/O > A Unix file is a sequence of $m$ bytes: > $$ > B_0, B_1, \dots, B_k, \dots, B_{m-1} > $$ 所有 I/O 裝置,像是網路、硬碟、終端機都被當程式一個 file,所有對裝置的 input/output,都是透過對檔案做 reading/writing 來達成的。 * `open` ```c #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(char *filename, int flags, mode_t mode); ``` * `close` ```c #include <unistd.h> int close(int fd); ``` * `read` `write` 回傳值為實際讀取/寫入 bytes 數量 * 有幾種情況會使回傳值與要求的數量不同 (*short counts*) * 讀到 EOF * 從終端機讀取時,一次只會讀一行 * 讀取和寫入 networking socket 時 (內部 buffering 限制、網路延遲過長) > size_t 被定義為 unsigned int > ssize_t 是 signed size ,被定義為 int ```c #include <unistd.h> ssize_t read(int fd, void *buf, size_t n); ssize_t write(int fd, const void *buf, size_t n); ``` 如果想要建造一個可依賴的網路應用程式 (如:Web servers),就必須處理好每次 `read` `write` 遇到的 *short counts* 。 ### Rio (Robust I/O) Package 可以自動化解決 *short counts* 的問題 RIO 提供兩種 functions * *Unbuffered input and output functions* * 可直接在 memory 和 file 之間轉換資料 * `rio_readn`、`rio_writen` * *Buffered input functions*