# 2025q1 Homework1 (lab0) contributed by <[eric895888](https://github.com/eric895888/lab0-c)> {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ ldd --version ldd (Ubuntu GLIBC 2.35-0ubuntu3.9) 2.35 Copyright (C) 2024 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Written by Roland McGrath and Ulrich Drepper. $ 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): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: 11th Gen Intel(R) Core(TM) i5-11300H @ 3.10GHz CPU family: 6 Model: 140 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 1 CPU max MHz: 4400.0000 CPU min MHz: 400.0000 BogoMIPS: 6220.80 ``` ## queue.c 開發過程 #### q_new 在實作過程中發現到`new_node`要先檢查 `head` 是否為 `NULL`,原本沒有檢查是否成功配置記憶體位置給`new_node`會導致執行錯誤 。 ```c if (!new_node) { return NULL; } ``` #### q_free 先依序訪問各個節點並將其刪除且釋放記憶體,一開始忘記最後加上`free(head)`導致鏈結串列已空但仍有記憶體佔用的情況發生。 ```c list_for_each_entry_safe (curr, safe, head, list) q_release_element(curr); ``` #### q_insert_head 確保記憶體分配成功後才進行插入節點`node`到`head`後方,使用 `strdup()` 複製字串,避免修改原字串導致錯誤,再更新雙向鏈結串列的 `next` 和 `prev` 指標,確保鏈結完整,使用的是list.h裡的`list_add()`去完成。 #### q_insert_tail 跟上面的區別在於更新雙向鏈結串列的連接不同改成插入節點 `node` 到 `head` 前方,使用的是 list.h 裡的`list_add_tail()`去完成。 #### q_remove_head 一開始判斷是否為空,後來利用 `container_of()` 找出 head 的 `next` 節點,把`node->value`的值存 bufsize 給sp,而sp的最後一個字元改成結束符號'\0'。 ```c if (sp && node->value) { strncpy(sp, node->value, bufsize ); sp[bufsize - 1] = '\0'; } ``` 刪除節點的方式是利用list.h裡`list_del()`改變前後節點的連結。 #### q_remove_tail 一開始判斷是否為空,後來利用 `container_of()` 找出 `head` 的 `next` 節點,其餘與`q_insert_tail()` 作法相同。 #### q_size 針對雙向連結串列走訪一次判斷是不是又回到 `head` ,並使用 `count` 紀錄個數。 #### q_delete_mid 這邊採用的是快慢指標的方式去尋找中間節點,再利用 `list_del` 刪除鏈結串列找到的中間節點及`q_release_element()` 釋放那個節點的記憶體。 ```c struct list_head *slow = head->next, *fast = head->next; //first element while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } ``` #### q_delete_dup 用 `list_for_each_safe()` 來走訪鏈表,其中 `curr` 指向當前節點,`safe` 指向下一個節點,以確保在刪除節點時不影響遍歷。在循環中,若 `curr` 和 `safe` 的值相同則刪除 `curr`,並標記 `dup = true` 表示當前值有重複。如果 `dup` 為 `true`,代表前一個元素是重複的,因此刪除 `node`,直到遇到不同的值時,將 `dup` 重置為 `false`。最後,成功執行後返回 `true`,表示刪除了重複元素。 但是原始 leecode 中的題目只會給排序好的鏈結串列作為輸入,我的作法目前也只能刪除連續的重複值。 #### q_swap 做到 `q_reverseK()` 才發現 `q_swap()` 相當於 `q_reverseK(head, 2)`。 #### q_reverse 如 head <-> A <-> B <-> C <-> D <-> head,`curr`初始指向`head`,針對每一個節點的`next`及`prev`做反轉。 ```c truct list_head *prev = head->prev, *curr = head, *next = head->next; while (next != head) { next = curr->next; curr->next = prev; curr->prev = next; prev = curr; curr = next; } ``` #### q_reverseK 用`len` 計算是否等於k再運用`list_cut_position()`,切下長度為k的連結串列存到`splited_list_head`中,再把`splited_list_head`反轉,透過`list_splice()`去原本的位置。 #### q_sort 使用另外撰寫的`merge_sort()`先拆分成一半分成左右區塊再繼續遞迴呼叫`merge_sort()`繼續拆分,之後使用`two_list_merge()`進行合併再依照 descend 的布林值決定是否降序排列。 #### q_ascend 對於任意一個在佇列中的節點,其前面的節點必定要小於自己,且其後面的節點必定大於自己。 #### q_descend 對於任意一個在佇列中的節點,其前面的節點必定要大於自己,且其後面的節點必定小於自己。 #### q_merge 會使用到 `queue_contex_t` 這個結構,看了許多資料才知道使用這個結構來做這題會更方便實作,使用`list_for_each_entry()` 走訪鏈表中每個 `queue_contex_t` ,並將每個佇列 `(curr->q)` 中的元素移動到 `tmp` 中,最後呼叫`q_sort()`對`tmp` 排序,並放回 head 所指向的第一個佇列。 ```c queue_contex_t *first = list_first_entry(head, queue_contex_t, chain); queue_contex_t *curr; list_for_each_entry(curr, head, chain){ list_splice_init(curr->q, &tmp); } q_sort(&tmp, descend); list_splice_init(&tmp, first->q); ``` ## 以 Valgrind 分析記憶體問題 學習使用 Valgrind,搭配 Massif 可以量測你的程式用了多少的 heap memory。 產生範例 test-massif.cmd。 ```c new ih head 100000 it tail 100000 reverse sort size quit ``` 例如下方使用 valgrind,之後會產生像這樣格式的 massif.out.43407 檔案。 ``` valgrind --tool=massif ./qtest -f traces/test-massif.cmd ``` 安裝圖形化顯示工具 massif-visualizer 方便觀看及分析。 ``` sudo apt install massif-visualizer ``` 顯示執行結果 ```bash massif-visualizer massif.out.43407 ``` ![螢幕快照 2025-04-07 12-00-50](https://hackmd.io/_uploads/B1k-ApgRJx.png) 可以搭配 massif.out.43407 檔案內的文字紀錄搭配視覺化圖片得知, heap 的峰值主要是在 ih head, it tail產生資料。 ## tiny-web-server 在查看Linux manual pageg 時會看到像是 select(2) 函數中代了數字,參考 [Linux manual page](https://man7.org/linux/man-pages/man7/man-pages.7.html) 可以代表的意義。 #### 研讀 select(2) [select(2)](https://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫允許程式監控多個 file descriptors ,「非阻塞」地進行讀、寫或錯誤檢查,例如監控 socket。 ```c int select(int nfds, fd_set *_Nullable restrict readfds, fd_set *_Nullable restrict writefds, fd_set *_Nullable restrict exceptfds, struct timeval *_Nullable restrict timeout); ``` nfds: 要監控的 file descriptors 數量上限,實際上是最大值 + 1,例如如果你要監控 fd = 4,那 nfds 就應該設為 5。 readfds:指向 fd_set 結構,代表哪些是否可讀?可為 NULL。 writefds:指向 fd_set 結構,代表哪些是否可寫入?可為 NULL。 exceptfds:指向 fd_set 結構,是否有異常狀況?可為 NULL。 回傳值0:有文件描述符準備好了,-1:錯誤。 File descriptor set: FD_ZERO(&set) 清空集合。 FD_SET(fd, &set) 把 fd 加入集合。 FD_CLR(fd, &set) 把 fd 從集合中移除。 FD_ISSET(fd, &set) 查某 fd 是否在集合中。 timeval: 設成 NULL:永遠阻塞直到某個 fd 準備好,設成 {0, 0}:非阻塞查詢(立即回傳)。 #### 研讀 signal(7) [signal(7)](https://man7.org/linux/man-pages/man7/signal.7.html) 用來通知某個程序(process)發生了某件事情,產生什麼行為。 Signal 可分為兩大類:標準信號(Standard Signals)與即時信號(Real-Time Signals)。 * 標準信號是的 POSIX 信號,如 SIGINT(中斷)、SIGTERM(終止)、SIGSEGV(記憶體存取錯誤)、SIGKILL(強制終止)等,其特點是不能攜帶附加資料,且同一種信號同時僅會儲存一筆(可能會被覆蓋。 * 標即時信號則是 POSIX Real-Time 擴充的一部分,編號通常從 SIGRTMIN 開始,它們支援排隊傳送,且可以攜帶額外資料,例如使用 sigqueue() 傳送。 當 signal 發生時,系統會依據 signal 的「處置方式(disposition)」來決定如何反應,包括預設終止(Term)、忽略(Ign)、產生核心轉儲(Core)、停止(Stop)或繼續執行(Cont)。使用者可透過 signal() 或 sigaction() 安裝 signal handler,進一步自訂 signal 的處理行為,Signal 可由多種方式發送,如 kill()、raise()、pthread_kill() 等,並可透過 mask 機制來阻擋、延後處理或同步接收 signal。 #### 解決 favicon.ico 的問題 參考 [作業敘述](https://hackmd.io/@sysprog/linux2025-lab0-c#%E8%A7%A3%E6%B1%BA-faviconico-%E7%9A%84%E5%95%8F%E9%A1%8C) 修改解決 favicon.ico 的問題讓網頁也能夠使用web指令了 ![螢幕快照 2025-04-08 00-06-06](https://hackmd.io/_uploads/B1nWdOZCke.png) 但是使用curl指令時會產生以下的問題,回傳時會把它當成瀏覽器回應把整個html都回傳到了終端機。 ```bash <html><head><style>body{font-family: monospace; font-size: 13px;}td {padding: 1.5px 6px;}</style><link rel="shortcut icon" href="#"><h2>Success!</h2></head><body><table> ``` 一開始會想到可能是依照header去判斷是誰負責請求,首先先宣告一個全域變數負責存 header 資訊 `char user_agent_buf[MAXLINE] = ""; ` 然後在`parse_request()` 中的 `while ()` 中新增了一段程式碼。 ```c if (strncmp(buf, "User-Agent: ", 12) == 0) { strncpy(user_agent_buf, buf + 12, MAXLINE - 1); user_agent_buf[MAXLINE - 1] = '\0'; char *crlf = strchr(user_agent_buf, '\r'); if (crlf) *crlf = '\0'; printf("User-Agent: %s\n", user_agent_buf); //print test } ``` 送出指令的時候可以看到上排的是使用 curl 的 header 而下排則是使用瀏覽器的 header。 ``` cmd> User-Agent: curl/7.81.0 cmd> User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 ``` 而 buffer 則依照 user_agent_buf 內儲存的內容不同來決定那一種回傳格式。 ```c char *buffer; if (strstr(user_agent_buf, "curl")) { buffer = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"; } else { buffer = "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n\r\n" "<html><head><style>" "body{font-family: monospace; font-size: 13px;}" "td {padding: 1.5px 6px;}" "</style><link rel=\"shortcut icon\" href=\"#\">" "<h2>Success!</h2>" "</head><body><table>\n"; } ``` 這樣子網頁跟終端機都能正常顯示回傳了。 ## 在 qtest 提供新的命令 shuffle 利用 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) 在實作 shuffle。 ```c void q_shuffle(struct list_head *head) { if (!head || list_is_singular(head)) return; int len = q_size(head); struct list_head *pos, *tmp; for (pos = head->prev, tmp = pos->prev; pos != head && len; pos = tmp, tmp = pos->prev, len--) { int r = rand() % len; struct list_head *pick = head->next; for (int i = 0; i < r; i++) pick = pick->next; if (pick != pos) swap(pos, pick); } } ``` 以課程提供的 python 程式碼進行測試以及顯示出來的圖表,以圖表和數值來看大致符合 uniform distribution,需要進一步繼續用其他評估方式分析。 ``` Expectation: 41666 Observation: {'1234': 41573, '1243': 41666, '1324': 41613, '1342': 41539, '1423': 41612, '1432': 41696, '2134': 41556, '2143': 41783, '2314': 41658, '2341': 41850, '2413': 41712, '2431': 41913, '3124': 41715, '3142': 41870, '3214': 41904, '3241': 41321, '3412': 41873, '3421': 41933, '4123': 41387, '4132': 41490, '4213': 41530, '4231': 41766, '4312': 41594, '4321': 41446} chi square sum: 16.295252724043586 ``` ![螢幕快照 2025-04-09 21-45-28](https://hackmd.io/_uploads/rJqM5eVAye.png) ## 論文〈Dude, is my code constant time?〉 ## 研讀 Linux 核心原始程式碼的 lib/list_sort.c #### `__attribute__((nonnull(2,3)))` `__attribute__` 屬於 function attribute,nonnull(2,3) 表示第二、第三的輸入參數必定不為 NULL。 參考 [2023 年 Linux 核心設計/實作課程作業 —— lab0 (E)](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-e#L01-lab0) 內的數學證明去理解 sort 的過程,及2019年的 [commit b5c56e0c](https://github.com/torvalds/linux/commit/b5c56e0cdd62979dd538e5363b06be5bdf735a09) 開發者探討了 merge sort 的三種實作方式,分別為 top-down mergesort、bottom-up mergesort 和 queue-mergesort。 2025年1月的 [Commit e420460](https://github.com/torvalds/linux/commit/e420460ba443e8ad1cd71568c50b6e09d13fb106) 時候特別強調了函數必須滿足的數學性質,特別是反對稱性和遞移性,提出了曾有 [[1]](https://lore.kernel.org/lkml/20240701205639.117194-1-visitorckw@gmail.com)[[2]](https://lore.kernel.org/lkml/20241203202228.1274403-1-visitorckw@gmail.com)[[3]](https://lore.kernel.org/lkml/20241209134226.1939163-1-visitorckw@gmail.com)[[4]](https://lore.kernel.org/lkml/20241209145728.1975311-1-visitorckw@gmail.com ) 違反了遞移性導致排序結果發生錯誤。 ```c * The comparison function must adhere to specific mathematical properties * to ensure correct and stable sorting: * - Antisymmetry: cmp(@a, @b) must return the opposite sign of * cmp(@b, @a). * - Transitivity: if cmp(@a, @b) <= 0 and cmp(@b, @c) <= 0, then * cmp(@a, @c) <= 0. ``` 從上述資料學習到要搭配 commit message 去理解過去修改的原因及過程,例如我前面作業部份都是修改幾個函式再 commit 所有異動,這會導致閱讀的人不便。 #### 將 list_sort.c 以及 list_sort.h 加入 lab0-c 看到 likely 時不知道是什麼所以參考[[Linux Kernel慢慢學]likely and unlikely macro](https://meetonfriday.com/posts/cecba4ef/), 提到 likely 或 unlikely 的敘述來幫助編譯器做最佳化,`__built_expect()` 是 gcc 的內建 function,用來將 branch 的相關資訊提供給編譯器,以便對程式碼改變分支順序來進行優化,使用 likely 表示這段敘述 (x) 為 true 的機率比較大。 ```c # define likely(x) __builtin_expect(!!(x), 1) # define unlikely(x) __builtin_expect(!!(x), 0) ``` 接著把list_sort.c 以及 list_sort.h的內容移進lab0-c,並修改部份內容讓程式能夠執行 qtest.c 的部份則是根據 sort 的 do_sort() 多一個給 list_sort 並新增 ADD_COMMAND ```c ADD_COMMAND(lsort, "Use Linux kernel sorting algorithm", ""); ``` 再來`list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp)` priv 的部份可以帶入NULL。 而 cmp 的部份則是在 qtest.c 中新增一個函式判斷 list_head *a 和 list_head *b 指向的值。 根據 list_sort.c 的註解。 ```c /* The comparison function @cmp must return > 0 if @a should sort after * @b ("@a > @b" if you want an ascending sort), and <= 0 if @a should * sort before @b *or* their original order should be preserved. It is * always called with the element that came first in the input in @a, * and list_sort is a stable sort, so it is not necessary to distinguish * the @a < @b and @a == @b cases.*/ ``` * return > 0 (a 排在 b 的後面)。 * return <=0 (a 排在 b 的前面或保留原本排列)。 * list_sort 是一個 stable sort,故不用區分小於0和等於0的狀況。 ```c int cmp(void *priv, const struct list_head *a, const struct list_head *b) { element_t *a_ele = list_entry(a, element_t, list); element_t *b_ele = list_entry(b, element_t, list); return strcmp(a_ele->value, b_ele->value) < 0 ? 0 : 1; } ``` #### 使用perf進行效能分析 參考[Linux 效能分析工具: Perf](https://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) ``` perf stat --repeat 5 -e instructions,cycles ./qtest -f ./traces/trace-sort.cmd ``` trace-sort.cmd 裡面的內容 ```c option fail 0 option malloc 0 new ih RAND 500000 time sort (另一份是lsort) time ``` | q_sort | time | | -------- | -------- | | 1 | 0.766 | | 2 | 0.764 | | 3 | 0.764 | | 4 | 0.771 | | 5 | 0.776 | | instructions | cycles | | -------- | -------- | | 4,778,363,882 | 5,267,510,435 | | list_sort | time | | -------- | -------- | | 1 | 0.594 | | 2 | 0.594 | | 3 | 0.603 | | 4 | 0.595 | | 5 | 0.594 | | instructions | cycles | | -------- | -------- | | 4,705,154,742 | 4,419,841,615 | 可以看到 list_sort 的 cycle 明顯比較少。 git commit 時目前會遇到以下錯誤,因此無法上傳,原因在於3/11號lab0-c新增的fmtscan.c功能,似乎也有其他人遇到。 ``` Running static analysis... tools/fmtscan.c:664:26: warning: Either the condition 'nextch2!=256' is redundant or isxdigit() argument nr 1 can have invalid value. The value is 256 but the valid values are '0:255'. [invalidFunctionArg] if (isxdigit(nextch2)) { ^ tools/fmtscan.c:657:17: note: Assuming that condition 'nextch2!=256' is not redundant if (LIKELY(nextch2 != PARSER_EOF)) { ^ tools/fmtscan.c:664:26: note: Invalid argument if (isxdigit(nextch2)) { ^ nofile:0:0: information: Cppcheck cannot find all the include files (use --check-config for details) [missingInclude] Fail to pass static analysis. ```