--- tags: linux2022 --- # 2022q1 Homework1 (lab0) contributed by < `SmallHanley` > > [作業說明](https://hackmd.io/@sysprog/linux2022-lab0) > [GitHub](https://github.com/SmallHanley/lab0-c) ## 開發環境 ```bash $ uname -r 5.13.0-28-generic $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 142 Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz Stepping: 10 CPU MHz: 1800.000 CPU max MHz: 3400.0000 CPU min MHz: 400.0000 BogoMIPS: 3600.00 Virtualisation: VT-x L1d cache: 128 KiB L1i cache: 128 KiB L2 cache: 1 MiB L3 cache: 6 MiB NUMA node0 CPU(s): 0-7 ``` ## 實作 queue.c ### q_new() * `malloe` 一個 `struct list_head` 大小的空間,代表這個新串列的 `head` 節點,並用 `head` 指標指向此空間。 * 檢查 `head` 是否為 `NULL`,如果 `malloc` 失敗回傳 `NULL`。 * 呼叫 `INIT_LIST_HEAD(head)` 使得 `head` 節點的 ` prev` 和 `next` 指標皆指向自己,完成雙向環狀串列初始化。 ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (!head) { return NULL; } INIT_LIST_HEAD(head); return head; } ``` ### q_free() * 檢查 `l` 是否為 `NULL`。 * 呼叫 `list_for_each_entry_safe (elm, tmp, l, list)` 走訪串列 `l` 中的每個結構體 `element_t`,每次迭代中依序執行移除節點、釋放 `value` 空間、釋放結構體 `element_t`。 * 最後要釋放串列的 `head` 節點。 * 這裡使用 `list_for_each_entry_safe()` 而不是 `list_for_each_entry()` 是因為操作會牽涉到移除 `current node`,前者會先暫存下個節點的位址。 ```c void q_free(struct list_head *l) { element_t *elm, *tmp; if (l) { list_for_each_entry_safe (elm, tmp, l, list) { list_del(&elm->list); free(elm->value); free(elm); } free(l); } } ``` :::warning 不要只張貼程式碼,你應該說明並探討後續的改進空間。 :notes: jserv ::: ==TODO== ### q_insert_head() * 檢查 `head` 是否為 `NULL`,如果是則回傳 `false`。 * `malloc` 一個空間給欲新插入的結構體 `elm`,如果 `elm` 為 `NULL` 代表 `malloc` 失敗,回傳 `false`。 * `malloc` 一個 `strlen(s) + 1` 位元組的空間給 `val`,此空間會作為新結構體 `elm` 的 `value` 指向的空間。如果 `val` 為 `NULL`,釋放 `elm` 並回傳 `false`。 * 接著使用 `memcpy` 將 `s` 的資料複製至 `val`,並執行 `elm->value = val`。 * 最後再將新的結構體的 `list` 插入至 `head` 串列中,並回傳 `true`。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) { return false; } element_t *elm = malloc(sizeof(element_t)); if (!elm) { return false; } size_t len = strlen(s) + 1; char *val = malloc(sizeof(char) * len); if (!val) { free(elm); return false; } memcpy(val, s, len); elm->value = val; list_add(&elm->list, head); return true; } ``` ### q_insert_tail() * 作法和 `q_insert_head()` 類似,不過最後是呼叫 `list_add_tail()` 插入。 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) { return false; } element_t *elm = malloc(sizeof(element_t)); if (!elm) { return false; } size_t len = strlen(s) + 1; char *val = malloc(sizeof(char) * len); if (!val) { free(elm); return false; } memcpy(val, s, len); elm->value = val; list_add_tail(&elm->list, head); return true; } ``` ### q_remove_head() * 檢查 `queue` 是否為 `NULL` 或是為空,是則回傳 `NULL`。 * 呼叫 `list_first_entry(head, element_t, list)` 取得 head 結構體的位址。 * 呼叫 `list_del_init(&elm->list)` 移除該節點。 * 將該結構體 `value` 的內容複製到 `sp`。 * 原本是使用 `memcpy()` 複製,不過使用 [valgrind](https://valgrind.org/) 發現此處會發生 `Invalid read of size`,原因是當 `bufsize` 大於 `elm->value` 的 `size` 會複製到不該存取的空間,於是改成 `strncpy`,會有額外檢查機制。`q_remove_tail()` 同理。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) { return NULL; } element_t *elm = list_first_entry(head, element_t, list); list_del_init(&elm->list); if (sp) { strncpy(sp, elm->value, bufsize); sp[bufsize - 1] = '\0'; } return elm; } ``` ### q_remove_tail() * 與 `q_remove_head()` 類似,不過使用 `list_last_entry(head, element_t, list)` 取得 tail 結構體的位址。 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) { return NULL; } element_t *elm = list_last_entry(head, element_t, list); list_del_init(&elm->list); if (sp) { strncpy(sp, elm->value, bufsize); sp[bufsize - 1] = '\0'; } return elm; } ``` ### q_size() * 採用作業說明的方式。 * **後續改進:** * 可以用一個空間來存放串列的 `size`,每當串列有更動時就更新這個空間,需要討論的是這個空間要放在哪裡。 ==TODO== * 除此之外,如果是用整數來儲存,就需要額外討論 overflow 的問題,串列的 `size` 必須設限。 ```c int q_size(struct list_head *head) { if (!head) return 0; int len = 0; struct list_head *li; list_for_each (li, head) len++; return len; } ``` ### q_delete_mid() * 檢查 `queue` 是否為 `NULL` 或是為空,是則回傳 `false`。 * 找尋中間節點的部分是參考 [你所不知道的 C 語言: linked list 和非連續記憶體 - 案例探討: Leetcode 2095. Delete the Middle Node of a Linked List](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-Leetcode-2095-Delete-the-Middle-Node-of-a-Linked-List) 同學更新的共筆,比較不一樣的地方是,這裡使用的是雙向串列,不用額外的空間去暫存上一個節點或是使用指標的指標。 * 最後再將相關空間釋放。 ```c bool q_delete_mid(struct list_head *head) { // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/ if (!head || list_empty(head)) { return false; } struct list_head *slow = head->next; for (struct list_head *fast = head->next; fast != head && fast->next != head; fast = fast->next->next) { slow = slow->next; } element_t *del = list_entry(slow, element_t, list); list_del(slow); free(del->value); free(del); return true; } ``` ### q_delete_dup() * 檢查 `head` 是否為 `NULL`,是則回傳 `false`。 * 用一個 bool 值 `has_dup` 代表有沒有發現 duplicates。 * 呼叫 `list_for_each_safe (node, tmp, head)` 走訪每一個節點,並判斷此節點和下一個節點的 `value` 是否相同,是則將 `has_dup` 設為 `true`,然後從串列中移除此節點並釋放相關空間。 * 若 `value` 不同,而 `has_dup` 依然是 `true`,代表此節點是連續相同值的節點中的最後一個,將 `has_dup` 設回 `false` 並釋放相關空間。 ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head) { return false; } struct list_head *node, *tmp; bool has_dup = false; list_for_each_safe (node, tmp, head) { if (node->next != head && !strcmp(list_entry(node, element_t, list)->value, list_entry(node->next, element_t, list)->value)) { has_dup = true; element_t *del = list_entry(node, element_t, list); list_del(node); free(del->value); free(del); } else if (has_dup) { has_dup = false; element_t *del = list_entry(node, element_t, list); list_del(node); free(del->value); free(del); } } return true; } ``` ### q_swap() * 我採用的方法是,先用 `list_for_each (node, head)` 走訪節點 (注意:這裡不會走訪每一個節點),雖然會牽涉到刪除節點,不過我還是使用 `list_for_each`,以下會有說明。 * 每次迭代中會先檢查 `node->next` 是否等於 `head`,考慮到串列節點個數若是奇數,最後一個節點落單不需要再做交換。 * 接著會用 `tmp` 指標暫存下一個節點的位址,然後先後執行 `list_del(node)` 和 `list_add(node, tmp)` 來完成交換。 * 最後 `list_for_each()` 的 `node = node->next` 會將 `node` 指標指向下一組的第一個節點 (假設兩兩一組),所以只會走訪奇數編號的節點 (假設從 `head->next` 的節點從 1 向上編號)。 ```c void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ struct list_head *node; list_for_each (node, head) { if (node->next == head) { break; } struct list_head *tmp = node->next; list_del(node); list_add(node, tmp); } } ``` ### q_reverse() 一開始是用 `list_for_each_safe` 走訪每個節點,並將該節點的 `next` 和 `prev` 指標指向的位址互換,程式碼如下: ```c void q_reverse(struct list_head *head) { struct list_head *node, *tmp; list_for_each_safe (node, tmp, head) { struct list_head *prev = node->prev; node->prev = node->next; node->next = prev; } } ``` 執行 `mack check` 後對應的輸出結果如下: ``` l = [gerbil bear dolphin meerkat bear] cmd> # Reverse it cmd> reverse l = [gerbil] ``` 會發現串列中只剩下 `gerbil` 這個節點,於是我追蹤程式,繪製成圖。 為方便講解,假設串列中只有三個節點,`value` 分別為 `gerbil`、`bear` 和 `dolphin`,`l = [gerbil bear dolphin]`,如下圖: ```graphviz digraph G { rankdir=LR; node[shape=record]; e1 [label="head | {<prev>prev | <next>next}"]; e2 [label="gerbil | {<prev>prev | <next>next}"]; e3 [label="bear | {<prev>prev | <next>next}"]; e4 [label="dolphin | {<prev>prev | <next>next}"]; e1:next:e -> e2:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e2:next:e -> e3:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e3:next:e -> e4:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e4:next:e -> e1:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; } ``` 按照上述程式碼,`reverse` 結果如下: ```graphviz digraph G { rankdir=LR; node[shape=record]; e1 [label="head | {<prev>prev | <next>next}"]; e2 [label="gerbil | {<prev>prev | <next>next}"]; e3 [label="bear | {<prev>prev | <next>next}"]; e4 [label="dolphin | {<prev>prev | <next>next}"]; e1:next -> e2 e1:prev -> e4 e2:next -> e1 e2:prev -> e3 e3:next -> e2 e3:prev -> e4 e4:next -> e3 e4:prev -> e1 } ``` :::success 不得不說這張圖真難畫,不知道有沒有比較好的畫法? > 請愛用 [edotor](https://edotor.net/) > :notes: jserv ::: 可以發現 `head` 節點的 `prev` 和 `next` 指標沒有修改到,`next` 指標依舊指到 `gerbil` 結構體的節點,而 `gerbil` 節點的 `next` 指標因為 reverse,指回 `head` 節點。 `list_for_each_safe` 走訪完後,需要再將 `head` 節點的 `prev` 和 `next` 指標指向的位址交換。除此之外,一開始需要檢查 queue 是否為 NULL 或是 empty,對應程式碼修改如下: ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) { return; } struct list_head *node, *tmp; list_for_each_safe (node, tmp, head) { struct list_head *prev = node->prev; node->prev = node->next; node->next = prev; } tmp = head->next; head->next = head->prev; head->prev = tmp; } ``` ### q_sort() 我採用遞迴版本的 merge sort,參考 [linux2021-quiz2](https://hackmd.io/@sysprog/linux2021-quiz2): ```c void merge_sort_list(struct list_head *head) { if (list_empty(head) || list_is_singular(head)) { return; } struct list_head *slow = head->next; for (struct list_head *fast = head->next; fast != head && fast->next != head; fast = fast->next->next) { slow = slow->next; } LIST_HEAD(sorted); LIST_HEAD(left); list_cut_position(&left, head, slow); merge_sort_list(&left); merge_sort_list(head); merge(&left, head, &sorted); INIT_LIST_HEAD(head); list_splice_tail(&sorted, head); } ``` 其中,找尋中間節點方式是採用與 `q_delete_mid()` 一樣的方式: ```c struct list_head *slow = head->next; for (struct list_head *fast = head->next; fast != head && fast->next != head; fast = fast->next->next) { slow = slow->next; } ``` 執行 trace-04 的命令: ``` ./qtest -v 3 -f traces/trace-04-ops.cmd ``` 會發現在 `merge_sort_list()` 的地方出現 Segmentation fault: ``` Program received signal SIGSEGV, Segmentation fault. 0x000055555555ab54 in merge_sort_list (head=head@entry=0x7fffff7ff040) at queue.c:280 280 LIST_HEAD(sorted); ``` 用 [GDB](https://www.sourceware.org/gdb/) 的 `backtrace (bt)` 查看各個 frame,發現遞迴深度異常,造成堆疊溢出: ``` (gdb) bt #0 0x000055555555ab54 in merge_sort_list (head=head@entry=0x7fffff7ff040) at queue.c:280 #1 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff090) at queue.c:283 #2 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff0e0) at queue.c:283 #3 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff130) at queue.c:283 #4 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff180) at queue.c:283 #5 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff1d0) at queue.c:283 #6 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff220) at queue.c:283 #7 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff270) at queue.c:283 #8 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff2c0) at queue.c:283 #9 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff310) at queue.c:283 #10 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff360) at queue.c:283 #11 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff3b0) at queue.c:283 #12 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff400) at queue.c:283 #13 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff450) at queue.c:283 #14 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff4a0) at queue.c:283 #15 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff4f0) at queue.c:283 #16 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff540) at queue.c:283 #17 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff590) at queue.c:283 #18 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff5e0) at queue.c:283 #19 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffff7ff630) at queue.c:283 . . . ``` 進一步查看各個 frame 的 local variables,發現 `slow` 的結果為 `<optimised out>`: ``` (gdb) bt -full -20 . . . #104783 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffffffd8f0) at queue.c:283 slow = <optimised out> sorted = {prev = 0x7fffffffd890, next = 0x7fffffffd890} left = {prev = 0x7fffffffd8a0, next = 0x7fffffffd8a0} #104784 0x000055555555ab9f in merge_sort_list (head=head@entry=0x7fffffffd940) at queue.c:283 slow = <optimised out> sorted = {prev = 0x7fffffffd8e0, next = 0x7fffffffd8e0} left = {prev = 0x555555566e48, next = 0x555555566e48} #104785 0x000055555555ab9f in merge_sort_list (head=head@entry=0x555555566fb0) at queue.c:283 slow = <optimised out> sorted = {prev = 0x7fffffffd930, next = 0x7fffffffd930} left = {prev = 0x555555567108, next = 0x555555567108} #104786 0x000055555555ac18 in q_sort (head=0x555555566fb0) at queue.c:300 ``` > -full Print the values of the local variables also. This can be combined with the optional count to limit the number of frames shown. > -n Print only the outermost n frames, where n is a positive number. 查閱 GDB 文件 [Debugging with GDB](https://www.sourceware.org/gdb/current/onlinedocs/gdb.html),其中有提到關於 optimized-out 的變數可以關閉編譯器最佳化重新編譯: > If you need to display the values of such optimized-out arguments, either deduce that from other variables whose values depend on the one you are interested in, or recompile without optimizations. ``` (gdb) bt -full -20 . . . #87320 0x000055555555c2b8 in merge_sort_list (head=0x7fffffffd910) at queue.c:283 slow = 0x555555568df8 sorted = {prev = 0x7fffffffd8a0, next = 0x7fffffffd8a0} left = {prev = 0x7fffffffd8b0, next = 0x7fffffffd8b0} #87321 0x000055555555c2b8 in merge_sort_list (head=0x7fffffffd970) at queue.c:283 slow = 0x555555568df8 sorted = {prev = 0x7fffffffd900, next = 0x7fffffffd900} left = {prev = 0x7fffffffd910, next = 0x7fffffffd910} #87322 0x000055555555c2b8 in merge_sort_list (head=0x7fffffffd9d0) at queue.c:283 slow = 0x555555568e48 sorted = {prev = 0x7fffffffd960, next = 0x7fffffffd960} left = {prev = 0x555555568e48, next = 0x555555568e48} #87323 0x000055555555c2b8 in merge_sort_list (head=0x555555568fb0) at queue.c:283 slow = 0x555555569108 sorted = {prev = 0x7fffffffd9c0, next = 0x7fffffffd9c0} left = {prev = 0x555555569108, next = 0x555555569108} #87324 0x000055555555c347 in q_sort (head=0x555555568fb0) at queue.c:300 ``` 會發現 `slow` 指標在遞迴到 frame `#87321` 以後 (frame number $\leq$ 87321) 都維持指向 `0x555555568df8` 這個位址,表示尋找中間節點有問題。 回頭分析尋找中間節點的過程,發現當串列的 size = 2 時,會發生問題,假設 sub_list 的兩個節點 `value` 分別為 `"1"` 和 `"2"` (以 node~1~、node~2~ 表示),示意圖如下,一開始 `fast` 和 `slow` 指標都指向 `head->next` (即 node~1~): ```graphviz digraph G { rankdir=LR; node[shape=record]; e1 [label="head | {<prev>prev | <next>next}"]; e2 [label="1 | {<prev>prev | <next>next}"]; e3 [label="2 | {<prev>prev | <next>next}"]; e1:next:e -> e2:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e2:next:e -> e3:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e3:next:s -> e1:prev:s[arrowhead=normal, arrowtail=normal, dir=both]; node[shape=none] "fast, slow" -> e2; head -> e1; } ``` 進行一次迭代以後,`fast` 指標指向 `head` 節點, `slow` 指標指向 node~2~: ```graphviz digraph G { rankdir=LR; node[shape=record]; e1 [label="head | {<prev>prev | <next>next}"]; e2 [label="1 | {<prev>prev | <next>next}"]; e3 [label="2 | {<prev>prev | <next>next}"]; e1:next:e -> e2:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e2:next:e -> e3:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e3:next:s -> e1:prev:s[arrowhead=normal, arrowtail=normal, dir=both]; node[shape=none] "fast, head" -> e1; slow -> e3; } ``` 此時 `fast == head`,跳出迴圈,中間節點為 `slow` 指標指向的節點 (即 node~2~)。接著,執行以下程式碼: ```c LIST_HEAD(sorted); LIST_HEAD(left); list_cut_position(&left, head, slow); merge_sort_list(&left); ``` 其中,`list_cut_position()` 的實作如下: ```c static inline void list_cut_position(struct list_head *head_to, struct list_head *head_from, struct list_head *node) { struct list_head *head_from_first = head_from->next; if (list_empty(head_from)) return; if (head_from == node) { INIT_LIST_HEAD(head_to); return; } head_from->next = node->next; head_from->next->prev = head_from; head_to->prev = node; node->next = head_to; head_to->next = head_from_first; head_to->next->prev = head_to; } ``` `list_cut_position()` 會將 `head_from->next` 節點到 node 節點 (包含) 接至 `head_to` 串列,原串列中剩餘的節點會接到 `head from` 串列,執行後結果如下: ```graphviz digraph G { rankdir=LR; node[shape=record]; e1 [label="left | {<prev>prev | <next>next}"]; e2 [label="1 | {<prev>prev | <next>next}"]; e3 [label="2 | {<prev>prev | <next>next}"]; e4 [label=" | {<prev>prev | <next>next}"]; e1:next:e -> e2:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e2:next:e -> e3:prev:w[arrowhead=normal, arrowtail=normal, dir=both]; e3:next:s -> e1:prev:s[arrowhead=normal, arrowtail=normal, dir=both]; e4:next:s -> e4:prev:s[arrowhead=normal, arrowtail=normal, dir=both]; node[shape=none] slow -> e3 head -> e4 } ``` 分割位置後 `left` 串列的 size 依然是 2,呼叫 `merge_sort_list()` 後,會回到之前的循環,造成遞迴不會中止,堆疊溢出。 要解決此問題,需要重新思考找尋中間節點的方法,使得 `slow` 指標指向 node~1~,以下是修改後的程式碼: ```c struct list_head *fast = head->next, *slow; list_for_each (slow, head) { if (fast->next == head || fast->next->next == head) { break; } fast = fast->next->next; } ``` 完整程式碼如下: ```c void merge(struct list_head *head_a, struct list_head *head_b, struct list_head *sorted) { if (list_empty(head_a)) { list_splice_tail(head_b, sorted); return; } if (list_empty(head_b)) { list_splice_tail(head_a, sorted); return; } while (!list_empty(head_a) && !list_empty(head_b)) { char *lv = list_entry(head_a->next, element_t, list)->value; char *rv = list_entry(head_b->next, element_t, list)->value; struct list_head *tmp = strcmp(lv, rv) <= 0 ? head_a->next : head_b->next; list_del(tmp); list_add_tail(tmp, sorted); } list_splice_tail(list_empty(head_a) ? head_b : head_a, sorted); } void merge_sort_list(struct list_head *head) { if (list_empty(head) || list_is_singular(head)) { return; } struct list_head *fast = head->next, *slow; list_for_each (slow, head) { if (fast->next == head || fast->next->next == head) { break; } fast = fast->next->next; } LIST_HEAD(sorted); LIST_HEAD(left); list_cut_position(&left, head, slow); merge_sort_list(&left); merge_sort_list(head); merge(&left, head, &sorted); INIT_LIST_HEAD(head); list_splice_tail(&sorted, head); } void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) { return; } merge_sort_list(head); } ``` :::warning 將 `q_sort` 原本的 `q_size(head) < 2` 敘述改為 `list_is_singular`,善用現有的 API :notes: jserv ::: > 還有 `list_empty(head)` ## 引入 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) ### 整合至 [lab0-c](https://github.com/SmallHanley/lab0-c) 在 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c#L222) 第 222 行使用到 `likely` 這個巨集,是被定義在 [include/linux/compiler.h](https://elixir.bootlin.com/linux/latest/source/include/linux/compiler.h#L77),需要對其進行改。同理,`unlikely` 也要一併修改: ```c # define likely(x) __builtin_expect(!!(x), 1) # define unlikely(x) __builtin_expect(!!(x), 0) ``` 關於這個 built-in function `long __builtin_expect (long exp, long c)` 可以參見 [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)。 我使用 [Compiler Explorer](https://godbolt.org/z/KKqPs74qT) 簡單測試 `likely()` 和 `unlikely()` 是否有不同的編譯結果,前者在 RISC-V rv32gc 會被編譯成 `beq a5, zero, <label>`,後者會被編譯成 `bne a5, zero, <label>`,測試程式如下: ```c #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) void test_likely(volatile int x) { if (likely(x)) { x = 1; } else { x = 0; } } void test_unlikely(volatile int x) { if (unlikely(x)) { x = 1; } else { x = 0; } } ``` 接著,在 [第 56 行](https://github.com/torvalds/linux/blob/master/lib/list_sort.c#L56) 宣告一個 `count` 變數,type 為 `u8`,被定義在 [tools/include/linux/types](https://elixir.bootlin.com/linux/latest/source/tools/include/linux/types.h#L42),用來計算 `pending list` 的個數,這裡我更改為 `uint8_t`。 而在 lib/list_sort.c 中使用到 `__attribute__` 這個關鍵字,如下: ```c __attribute__((nonnull(2, 3))) void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp) ``` 在 [GNU C and C++](https://gcc.gnu.org/) 中,可以用來描述函式的特定性質 (參見 [Declaring Attributes of Functions](https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html))。而這裡使用到的是 `nonnull` 這個 attribute (參見 [Function-Attributes-nonnull](https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html)) > The nonnull attribute specifies that some function parameters should be non-null pointers. 可以根據指引在 Makefile 新增 `-Wnonnull` flag。 在 queue.c 中增加有關 linux list_sort 的程式碼: ```c static int value_cmp(void *priv, const struct list_head *a, const struct list_head *b) { char *av = list_entry(a, element_t, list)->value; char *bv = list_entry(b, element_t, list)->value; return strcmp(av, bv); } void q_linux_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) { return; } list_sort(NULL, head, value_cmp); } ``` 另外,我參考 [laneser](https://hackmd.io/@laneser/linux2022_lab0) 的作法,增加 `linuxsort` option 來切換 sort 的實作,當 `linuxsort = 0` 時採用 `q_sort()` 的方式,當 `linuxsort = 1` 時採用 `q_linux_sort()` 的方式。 :::success 我嘗試引入 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 到專案,想請問相關的程式碼是不是同樣要以 [GPLv2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html) 釋出? > 是的,你要在 `README.md` 標注授權條款,這二者不衝突 > :notes: jserv ::: ### lib/list_sort.c 實作分析 在探討 lib/list_sort.c 相關實作時,除了單純 trace code 以外,也需要了解為什麼 Linux 核心會採用這段程式碼,開發者是如何思考以及做一些取捨。我們可以看 github commit history,lib/list_sort.c 最近一次演算法上的改動是 May 15, 2019, [commit b5c56e0c](https://github.com/torvalds/linux/commit/b5c56e0cdd62979dd538e5363b06be5bdf735a09), **lib/list_sort: optimize number of calls to comparison function** 引入,其中引用了三篇論文: [1] Bottom-up Mergesort: A Detailed Analysis Wolfgang Panny, Helmut Prodinger Algorithmica 14(4):340--354, October 1995 https://doi.org/10.1007/BF01294131 https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.6.5260 [2] The cost distribution of queue-mergesort, optimal mergesorts, and power-of-two rules Wei-Mei Chen, Hsien-Kuei Hwang, Gen-Huey Chen Journal of Algorithms 30(2); Pages 423--448, February 1999 https://doi.org/10.1006/jagm.1998.0986 https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.5380 [3] Queue-Mergesort Mordecai J. Golin, Robert Sedgewick Information Processing Letters, 48(5):253--259, 10 December 1993 https://doi.org/10.1016/0020-0190(93)90088-q https://sci-hub.tw/10.1016/0020-0190(93)90088-Q 對於 comparison 次數的探討,我們可以寫成以下形式: \begin{gather*} nlog_2 n - Kn + O(1) \end{gather*} 其中,以下 merge sort 的變形 (variants),領導係數 (leading coefficient) 皆為 $log_2 (n!)$,探討的著重點在於一次項係數 K。 開發者探討了 merge sort 的三種實作方式,分別為 **top-down mergesort**、**bottom-up mergesort** 和 **queue-mergesort**,以及開發者提出的方式,以下簡述不同的實作方式: **1. Top-down mergesort** * 有最少的 average case、worst case 的 comparison 次數。 * 需要使用遞迴或是額外空間作為 user stack。 * 需要事先知道整個 list 的大小。 下圖例子是 balanced power-of-2 rule dividing strategy: ```graphviz digraph G { splines=line node[shape=box, height=.1];sorted_1 sorted_2 sorted_3 sorted_4 sorted_5 sorted_6 sorted_7 sorted_8; node[shape=record, height=.1];input result divide_41 divide_42 divide_21 divide_22 divide_23 divide_24 merge_21 merge_22 merge_23 merge_24 merge_41 merge_42; node[shape=none];merge_pad input[label="<f0>2|<f1>5|<f2>4|<f3>6|<f4>8|<f5>1|<f6>7|<f7>3"] result[label="<f0>1|<f1>2|<f2>3|<f3>4|<f4>5|<f5>6|<f6>7|<f7>8"] divide_41[label="<f1>2|<f2>5|<f3>4|<f4>6"] divide_42[label="<f1>8|<f2>1|<f3>7|<f4>3"] divide_21[label="<f0>2|<f1>5"] divide_22[label="<f0>4|<f1>6"] divide_23[label="<f0>8|<f1>1"] divide_24[label="<f0>7|<f1>3"] sorted_1[label="1"] sorted_2[label="2"] sorted_3[label="3"] sorted_4[label="4"] sorted_5[label="5"] sorted_6[label="6"] sorted_7[label="7"] sorted_8[label="8"] merge_21[label="<f0>2|<f1>5"] merge_22[label="<f0>4|<f1>6"] merge_23[label="<f0>1|<f1>8"] merge_24[label="<f0>3|<f1>7"] merge_41[label="<f1>2|<f2>4|<f3>5|<f4>6"] merge_42[label="<f1>1|<f2>3|<f3>7|<f4>8"] merge_pad[label=""] input -> divide_41 input -> divide_42 divide_41 -> divide_21 divide_41 -> divide_22 divide_42 -> divide_23 divide_42 -> divide_24 divide_21:f0 -> sorted_2 divide_21:f1 -> sorted_5 divide_22:f0 -> sorted_4 divide_22:f1 -> sorted_6 divide_23:f0 -> sorted_8 divide_23:f1 -> sorted_1 divide_24:f0 -> sorted_7 divide_24:f1 -> sorted_3 sorted_1; sorted_2; sorted_3; sorted_4; sorted_5; sorted_6; sorted_7; sorted_8; sorted_2 -> merge_21:f0 sorted_5 -> merge_21:f1 sorted_4 -> merge_22:f0 sorted_6 -> merge_22:f1 sorted_8 -> merge_23:f1 sorted_1 -> merge_23:f0 sorted_7 -> merge_24:f1 sorted_3 -> merge_24:f0 merge_22 -> merge_pad[style=invis] merge_23 -> merge_pad[style=invis] merge_pad -> result[style=invis] merge_21 -> merge_41 merge_22 -> merge_41 merge_23 -> merge_42 merge_24 -> merge_42 merge_41 -> result merge_42 -> result } ``` **2. Bottom-up mergesort** * 在這幾種變形中需要最多的 comparison 次數。 ```graphviz digraph G { splines=line node[shape=record, height=.1];input result merge_21 merge_22 merge_23 merge_24 merge_41 merge_42; input[label="<f0>2|<f1>5|<f2>4|<f3>6|<f4>8|<f5>1|<f6>7|<f7>3"] result[label="<f0>1|<f1>2|<f2>3|<f3>4|<f4>5|<f5>6|<f6>7|<f7>8"] merge_21[label="<f0>2|<f1>5"] merge_22[label="<f0>4|<f1>6"] merge_23[label="<f0>1|<f1>8"] merge_24[label="<f0>3|<f1>7"] merge_41[label="<f1>2|<f2>4|<f3>5|<f4>6"] merge_42[label="<f1>1|<f2>3|<f3>7|<f4>8"] input:f0 -> merge_21:f0 input:f1 -> merge_21:f1:n input:f2 -> merge_22:f0 input:f3 -> merge_22:f1 input:f4 -> merge_23:f1 input:f5 -> merge_23:f0 input:f6 -> merge_24:f1 input:f7 -> merge_24:f0:n merge_21:f0 -> merge_41:f1 merge_21:f1 -> merge_41:f3 merge_22:f0 -> merge_41:f2 merge_22:f1 -> merge_41:f4 merge_23:f0 -> merge_42:f1 merge_23:f1 -> merge_42:f4 merge_24:f0 -> merge_42:f2 merge_24:f1 -> merge_42:f3 merge_41:f1 -> result:f1 merge_41:f2 -> result:f3 merge_41:f3 -> result:f4 merge_41:f4 -> result:f5 merge_42:f1 -> result:f0 merge_42:f2 -> result:f2 merge_42:f3 -> result:f6 merge_42:f4 -> result:f7 } ``` **3. Queue-mergesort** * 特別適合用於鏈結串列的排序。 * queue-mergesort comparison 的次數少於 bottom-up mergesort,略高於 top-down mergesort。 * 可以以 top-down 或是 bottom-up 的方式實作。 * 透過 get front、put back 操作,因此排序完的結果會是 unstable。 根據 [3] 的演算法,pseudo code 如下 ```c queue-mergesort(Q): while (Q.size != 1) do Q.put(Merge(Q.get, Q.get)) ``` **4. lib/list_sort.c** 如果查看更之前的版本 [commit 043b3f7b](https://github.com/torvalds/linux/commit/043b3f7b6388fca6be86ca82979f66c5723a0d10) 會發現是用 bottom-up mergesort 實作。==TODO== ### 效能比較 以下分別對 My sort 和 Linux sort 進行 100 萬筆資料的排序,共 5 次,考慮到整個排序的瓶頸是在呼叫比較函式的次數,以下分別測量時間和比較次數,發現我使用的 top-down merge 的確比較次數較少: :::success 其實這裡說瓶頸是在呼叫比較函式的次數不太正確,後續實驗也發現 compare 次數少,不見得執行時間短,也需要考慮 cache locality (以下三者都是深度優先,可能對於 cache 的友善程度不同?待實驗),以及 function call 的成本,像是 top-down mergesort 我使用遞迴法,雖然 compare 次數少,但是執行時間最久。 ::: **My sort** | test | time | compare | |:----:| ----- |:--------:| | 1 | 1.681 | 18674748 | | 2 | 1.688 | 18674652 | | 3 | 1.666 | 18674308 | | 4 | 1.667 | 18673658 | | 5 | 1.670 | 18673899 | **Linux sort** | test | time | compare | |:----:| ----- |:--------:| | 1 | 1.394 | 18686507 | | 2 | 1.415 | 18687977 | | 3 | 1.432 | 18686862 | | 4 | 1.397 | 18686900 | | 5 | 1.397 | 18686296 | 後來我又加測更之前的 bottom-up mergesort 版本: **Linux sort ([commit 043b3f7b](https://github.com/torvalds/linux/commit/043b3f7b6388fca6be86ca82979f66c5723a0d10))** | test | time | compare | |:----:| ----- |:--------:| | 1 | 1.357 | 18716830 | | 2 | 1.375 | 18716517 | | 3 | 1.365 | 18715850 | | 4 | 1.357 | 18715780 | | 5 | 1.351 | 18716069 | 發現確實如開發者說的,bottom-up mergesort 的 compare 次數最高,不過所花的時間是三者最少的。看到這裡可能還看不到開發者的考量點,於是我接著進行以下實驗,分別測試兩個 linux sort 的版本 (即 2:1 balanced merge 和 bottom-up mergesort),以及兩種不同大小的測資,非別為 1048580,略大於 2 的冪,以及 1048576,2 的冪: **2:1 balanced merge ([commit b5c56e0c](https://github.com/torvalds/linux/commit/b5c56e0cdd62979dd538e5363b06be5bdf735a09))** | test size 1048580 | time | compare | test size 1048576 | time | compare | |:-----------------:| ----- |:--------:|:-----------------:| ----- |:--------:| | 1 | 1.473 | 19645491 | 1 | 1.491 | 19644674 | | 2 | 1.489 | 19645794 | 2 | 1.475 | 19646095 | | 3 | 1.520 | 19644566 | 3 | 1.471 | 19645472 | | 4 | 1.506 | 19645249 | 4 | 1.511 | 19644989 | | 5 | 1.490 | 19646700 | 5 | 1.494 | 19645982 | **Bottom-up mergesort ([commit 043b3f7b](https://github.com/torvalds/linux/commit/043b3f7b6388fca6be86ca82979f66c5723a0d10))** | test size 1048580 | time | compare | test size 1048576 | time | compare | |:-----------------:| ----- |:--------:|:-----------------:| ----- |:--------:| | 1 | 1.546 | 20250025 | 1 | 1.419 | 19645530 | | 2 | 1.570 | 20676398 | 2 | 1.416 | 19645425 | | 3 | 1.537 | 20414941 | 3 | 1.417 | 19645713 | | 4 | 1.586 | 20556979 | 4 | 1.435 | 19646198 | | 5 | 1.544 | 20606780 | 5 | 1.442 | 19645194 | 可以觀察到 bottom-up mergesort 在 best case (即 2 的冪) 表現確實比較優異,不過一旦排序的大小略高於 2 的冪 (這裡只多 4),compare 的次數會增加近百萬,執行時間也會大幅增加,這就是開發者要這樣修改的原因,並且可以增加程式的可預測性。 至於為什不用 top-down mergesort,前面有討論過缺點,需要事先知道整個鏈結串列的大小,且對於空間使用的可預測性低。而 Queue-mergesort 則是 unstable sort,lib/list_sort.c 是 linux list 排序的界面,無法預期使用者的用途,若使用者的用途要求排序好的串列必須要是 stable,則會有非預期結果。 :::spoiler 測試 script ```bash #!/bin/bash make clean make GCOV=1 ./qtest -f traces/trace-test-sort.cmd gcov queue.c sed -n 326p queue.c.gcov ``` ::: ## 在 qtest 提供新的命令 shuffle 這裡根據 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 的第二種演算法實作,pseudo code 如下: ``` -- To shuffle an array a of n elements (indices 0..n-1): for i from 0 to n−2 do j ← random integer such that i ≤ j < n exchange a[i] and a[j] ``` 在 `queue.c` 增加 `q_shuffle()` 的實作,這裡先用沒有優化的版本,有待後續改進: ```c void q_shuffle(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) { return; } int size = q_size(head); srand(time(NULL)); struct list_head *left, *right; for (int i = 0; i < size; i++) { int j = rand() % (size - i) + i; if (i != j) { left = right = head->next; int t = i; while (t--) { left = left->next; } while (j--) { right = right->next; } struct list_head *posl = left->prev, *posr = right->prev; list_del(right); list_add(right, posl); if (j - i > 1) { list_del(left); list_add(left, posr); } } } } ``` ## 運用 [Valgrind](https://valgrind.org/) 排除 `qtest` 實作的記憶體錯誤 嘗試先觀察 `traces/trace-eg.cmd` 的命令,並得到以下對應輸出,發現 [linenoise.c:1224](https://github.com/SmallHanley/lab0-c/blob/master/linenoise.c#L1224) 呼叫 `malloc()` 以及 [linenoise.c:1236](https://github.com/SmallHanley/lab0-c/blob/master/linenoise.c#L1236) 呼叫 `strdup()` ,沒有對應的記憶體回收機制: ```bash $ valgrind -q --leak-check=full ./qtest -f traces/trace-eg.cmd . . . ==17974== 5 bytes in 1 blocks are still reachable in loss record 1 of 3 ==17974== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==17974== by 0x4A4E50E: strdup (strdup.c:42) ==17974== by 0x110C1E: linenoiseHistoryAdd (linenoise.c:1236) ==17974== by 0x1117B1: linenoiseHistoryLoad (linenoise.c:1325) ==17974== by 0x10C79F: main (qtest.c:979) ==17974== ==17974== 140 bytes in 19 blocks are still reachable in loss record 2 of 3 ==17974== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==17974== by 0x4A4E50E: strdup (strdup.c:42) ==17974== by 0x110B92: linenoiseHistoryAdd (linenoise.c:1236) ==17974== by 0x1117B1: linenoiseHistoryLoad (linenoise.c:1325) ==17974== by 0x10C79F: main (qtest.c:979) ==17974== ==17974== 160 bytes in 1 blocks are still reachable in loss record 3 of 3 ==17974== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==17974== by 0x110BDE: linenoiseHistoryAdd (linenoise.c:1224) ==17974== by 0x1117B1: linenoiseHistoryLoad (linenoise.c:1325) ==17974== by 0x10C79F: main (qtest.c:979) ==17974== ``` 不過睡一覺起來做一些操作後再重現原本測試,發現一樣是執行 `valgrind -q --leak-check=full ./qtest -f traces/trace-eg.cmd` 這段命令,卻沒有產生 memory leak 資訊。 後來我再重新檢視後者的操作中與前者有什麼不同之處,發現我在後者的操作中有刪除掉 `.cmd_history` 這個檔案。經過測試,的確在有 `.cmd_history` 這個檔案並且該檔案有內容時會產生 memory leakage。 `.cmd_history` 這的檔案在 [console.h:6](https://github.com/SmallHanley/lab0-c/blob/master/console.h#L6) 用 `HISTORY_FILE` 定義: ```c #define HISTORY_FILE ".cmd_history" ``` 並且在 [qtest.c:979](https://github.com/SmallHanley/lab0-c/blob/master/qtest.c#L979) `main` 函式有呼叫 [linenoise.c:1309](https://github.com/SmallHanley/lab0-c/blob/master/linenoise.c#L1309) `linenoiseHistoryLoad(HISTORY_FILE)`,並且在該函式中,如果 history file (即 `.cmd_history`) 有命令時,會呼叫 [linenoise.c:1215](https://github.com/SmallHanley/lab0-c/blob/master/linenoise.c#L1215) `linenoiseHistoryAdd(buf)`,並且有呼叫 `malloc` 配置一空間給 `char **history` (用來暫存字元陣列的陣列),以及呼叫 `strdup` 將 `.cmd_history` 檔案讀取到的命令放進 `history` 陣列中,這與 valgrind 產生出的資訊相符,的確有配置的空間需要釋放。 進一步找尋適當的位置將所配置的空間釋放,以解決 memory leak 的問題,發現的確有一函式 [freeHistory()](https://github.com/SmallHanley/lab0-c/blob/master/linenoise.c#L1190) 負責處理上述提到的 `malloc` 以及 `strdup` 的記憶體釋放。而在 [linenoiseAtExit()](https://github.com/SmallHanley/lab0-c/blob/master/linenoise.c#L1202) 有呼叫到 `freeHistory()` (該函式還會呼叫 disableRawMode(),跟 terminal interface 有關,尚未研究),其中與 `disableRawMode()` 對應的是 [enableRawMode()](https://github.com/SmallHanley/lab0-c/blob/master/linenoise.c#L237),會發現該函式有將 `linenoiseAtExit()` 註冊 [atexit(3)](https://man7.org/linux/man-pages/man3/atexit.3.html),被註冊的函式會在 normal process termination 時以逆序被呼叫。如果追蹤哪些函式會呼叫 `enableRawMode()`,會發現有 `linenoiseRaw()` 跟 `linenoisePrintKeyCodes()` 兩個函式。前者在 [linenoise()](https://github.com/SmallHanley/lab0-c/blob/master/linenoise.c#L1147) 會呼叫,後者沒有被任何函式呼叫。 接著,查看 `console.c` [run_console()](https://github.com/SmallHanley/lab0-c/blob/master/console.c#L641) 會發現 `has_infile` `true` 或是 `false` (即有無 `-f` flag) 有不同的行為,前者直接透過 `cmd_select()` 進行相關直譯動作,並未呼叫與 `linenoise` 相關的函式,但是不管有無 `-f` flag,都會在 `qtest` 的 `main` 函式呼叫 `linenoiseHistoryLoad()`,因此當有 infile 時,會產生 memory leak。透過實驗也證實相同的命令在有 infile 會產生 memory leak,沒有 infile 時則不會。 對應測試命令檔案如下: :::spoiler traces/trace-memleak1.cmd ```bash new ih hello quit ``` ::: <br> 綜合上述,當有 `.cmd_history` 這個檔案且非空,並且有 infile 時會產生 memory leak。對應的解決方式是將 `qtest` 的 `main` 函式中的 `linenoiseHistoryLoad()` 移至 `run_console()`,當有 infile 時才呼叫該函式。對應修改參見 [commit 6db5d4b0](https://github.com/SmallHanley/lab0-c/commit/6db5d4b04fcdfbdab7d940e8a3510a3ea091bc47)。 ### Massif 視覺化 以下為 `traces/trace-eg.cmd` 命令檔案測試結果,比較修改前後的記憶體使用情形,發現修改前記憶體使用沒有歸零,修改完後有歸零。 * Before ![](https://i.imgur.com/bzjsfZ5.png) * After ![](https://i.imgur.com/wyNjfY5.png) ## Reference * [Debugging with GDB](https://www.sourceware.org/gdb/current/onlinedocs/gdb.html) * [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) * [Declaring Attributes of Functions](https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html) * [Function-Attributes-nonnull](https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html) * [laneser 共筆](https://hackmd.io/@laneser/linux2022_lab0) * [gcov—a Test Coverage Program](https://gcc.gnu.org/onlinedocs/gcc/Gcov.html)