Lambert
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 2024q1 Homework1 (lab0) contributed by < [LambertWSJ](https://github.com/LambertWSJ) > ==[作業說明](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-a)== # 實作 Queue 介面 ## `q_insert_head` / `q_insert_tail` 將節點插入到 queue 的頭尾。可以利用 doubly linked list 的特性簡化成傳入 queue 頭尾的節點,之後再用 list_add 插入新節點。 `list_add(node, head)`: node 插入到 head 後面 ```c static inline bool q_insert_node(struct list_head *head, char *s) { element_t *el = malloc(sizeof(element_t)); if (!el) return false; el->value = strdup(s); if (!el->value) { free(el); return false; } list_add(&el->list, head); return true; } ``` 假設 queue 有 aa, bb, cc 三個節點,要新增的節點為 dd,如下所示 ```graphviz digraph { rankdir=LR node[shape=box] head node1[label=aa] node2[label=bb] node3[label=cc] node4[label=dd] subgraph cluster1 { rankdir=LR color=none head->node1 node1->node2 node2->node3 } } ``` 插入開頭 `list_add(dd, head)`,將 dd 插入到 head 後方,效果相當於 `list_add_tail(dd, aa)` ```graphviz digraph { rankdir=LR node[shape=box] head node1[label=aa] node2[label=bb] node3[label=cc] node4[label=dd] subgraph cluster1 { rankdir=LR color=none head->node4 node4->node1 node1->node2 node2->node3 } } ``` 由於大部分的操作相同,所以只需要 `q_insert_node` 來處裡插入頭尾的函式即可。 ```c /* Insert an element at head of queue */ bool q_insert_head(struct list_head *head, char *s) { return q_insert_node(head, s); } /* Insert an element at tail of queue */ bool q_insert_tail(struct list_head *head, char *s) { return q_insert_node(head->prev, s); } ``` ## `q_remove_head` / `q_remove_tail` 從 queue 的頭尾移除節點也能夠簡化成只使用一個函式來處理,利用雙向串列的特性傳入頭跟尾的 list_head 即可,從串列拿掉 `node` 後,再把 `el->value` 複製到 sp 上用來比對答案。 ```c static inline element_t *q_remove_node(struct list_head *node, char *sp, size_t bufsize) { if (list_empty(node)) return NULL; element_t *el = list_entry(node, element_t, list); list_del(node); if (sp && el->value) { size_t len = strlen(el->value) + 1; len = len < bufsize ? len : bufsize; strncpy(sp, el->value, len); sp[len - 1] = '\0'; } return el; } element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { return q_remove_node(head->next, sp, bufsize); } element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { return q_remove_node(head->prev, sp, bufsize); } ``` ## `q_reverse` / `q_reverseK` - `q_reverse` 串列反轉的操作較為簡單,只需要將每個節點移動到 head 前面即可達成。這裡要使用 safe 版的 for 迴圈,確保節點在移動後, node 可以取得下一個節點,否則永遠會停留在第一個跟第二個節點,不斷的在循環。 ```c void q_reverse(struct list_head *head) { struct list_head *node, *safe; list_for_each_safe (node, safe, head) { list_move(node, head); } } ``` 如果在 safe 版的 for 迴圈中使用 list_move_tail 會發生什麼事? 那只會不斷的把當前的節點,往後面移動,那麼 node 將永遠都到不了 head,一樣會遇到無窮迴圈。 上述的迴圈可以輕鬆改寫成遞迴,不過反轉的節點太多會導致 stack overflow ```c static void do_recur_reverse(struct list_head *node, struct list_head *head) { if (node == head) return; struct list_head *safe = node->next; list_move(node, head); do_recur_reverse(safe, head); } void q_reverse_recur(struct list_head *head) { do_recur_reverse(head->next, head); } ``` 除了單向的實做,亦可以利用雙向鍊結串列的特性,頭尾兩兩交換,概念類似一樣的技巧用在陣列反轉。 list_for_each_safe 由上到下的判斷式,依序考慮下面兩種 case 1. 節點數量為偶數個時,則必然會遇到 cur 的下一個節點就會是 last,表示其它節點已經兩兩交換完畢,剩下彼此還沒交換位置。 2. 節點數量為奇數個時,則 cur 與 last 必然會重疊,重疊時表示所有節點都已交換完畢,可以結束迴圈。 ```c void q_reverse_bidir(struct list_head *head) { struct list_head *cur, *safe; struct list_head *last = head->prev, *last_safe; list_for_each_safe (cur, safe, head) { last_safe = last->prev; if (cur->next == last) { list_swap(cur, last); break; } else if (cur == last) break; list_swap(cur, last); last = last_safe; } } ``` 其中 `list_swap` 的實做如下,考慮到可能是前後節點交換,所以 e2 和 e1 交換完後,可以檢查 prev 是不是 e1,如果是的話表示 e1 與 e2 是鄰近節點,e2 搬動完後也就完成交換了,不必再做一次 `list_move`。 ```c static void list_swap(struct list_head *e1, struct list_head *e2) { struct list_head *prev = e2->prev; list_move(e2, e1); if (prev != e1) list_move(e1, prev); } ``` - [ ] console.c 三種不同的實做要拿來測試的話,可以在加入 option 變數來決定使用哪個實做來進行測試 ```c int rev_opt = 0; void init_cmd() { /* ... */ add_param("rev_opt", &rev_opt, "chose reverse implemntation" ", 0 for list_move method(default)" ", 1 for bi-direction reverse" ", 2 for recursive reverse(may stack overflow)", NULL); } ``` - [ ] qtest.c 有了 option 後,就能夠透過 rev_opt 來查表選擇實做,這裡使用 typedef 簡化函式指標,用來提昇程式碼的可讀性 ```diff typedef void (*queue_op)(struct list_head *head); queue_op rev_tlb[] = {q_reverse, q_reverse_bidir, q_reverse_recur}; queue_op reverse_op = q_reverse; static bool do_reverse(int argc, char *argv[]) { /* .... */ + if (rev_opt < 3) + reverse_op = rev_tlb[rev_opt]; + else { + printf( + "unknown reverse option: %d\n" + "rev_opt only support follow option number\n" + "0 for list_move method(default)\n" + "1 for bi-direction reverse\n" + "2 for recursive reverse(may stack overflow)\n", + rev_opt); + trigger_exception("Please chose option carefully"); + } + if (current && exception_setup(true)) - q_reverse(current->q); + reverse_op(current->q); } ``` - `q_reverseK` 題目大意為每 k 個節點就將串列反轉。 實做時,卡在 reverse 後,怎麼指向下個 head? 去年參考 [komark06](https://hackmd.io/@komark06/SyCUIEYpj) 的寫法後,才知道指向 safe 的前一個節點當做 head。 直觀的做法就是 `anchor` 來記錄要反轉的子串列的頭,再用另一個變數去紀錄迴圈是不是走了 k 次,是的話就將走過的這一段串列反轉,反轉的作法是將走過 k 的一段串列切割出來給 `q_reverse` 反轉,完畢後再放回去。 `anchor` 接上反轉後的子串列,要將 `anchor` 更新到 `safe` 的前一個節點,當作下一輪反轉的子串列的 `list_head`。 有了不同的反轉實做,就能夠將 reverse_op 帶入到 q_reverseK。 ```diff void q_reverseK(struct list_head *head, int k) { if (!head || k <= 1) { return; } struct list_head *node, *safe, *anchor; LIST_HEAD(rev_head); anchor = head; int i = 0; list_for_each_safe (node, safe, head) { i++; if (i == k) { list_cut_position(&rev_head, anchor, node); - q_reverse(&rev_head); + reverse_op(&rev_head); list_splice_init(&rev_head, anchor); anchor = safe->prev; i = 0; } } } ``` ## `q_ascend` / `q_descend` 題目大意為刪掉串列的解點,使串列的值從右手邊觀察是嚴格遞增。單向串列的方法是透過遞迴讓 head 走訪到最後一個節點再比大小,因為題目要嚴格遞增,從最後一個節點開始記錄最大值,如果節點的值比最大值還要小就刪掉,比最大值還大就紀錄節點的內涵值,遞迴結束後就可以得到題目要求的串列。 我們可以利用雙向串列的特點,反過來走訪串列,並重複上述比大小的過程,就不用寫成遞迴了,也避開了 stack overflow 的風險。 由於是從從右手邊觀察,又要刪掉節點,所以用 macro 把迴圈簡化成 `list_for_each_safe_reverse` ```c #define list_for_each_safe_reverse(node, safe, head) \ for (node = head->prev, safe = node->prev; node != head; \ node = safe, safe = node->prev) int q_descend(struct list_head *head) { if (list_empty(head)) return 0; char *maxv = "\0"; int sz = 0; struct list_head *cur, *safe; list_for_each_safe_reverse(cur, safe, head) { sz++; element_t *entry = list_entry(cur, element_t, list); if (strcmp(entry->value, maxv) > 0) { maxv = entry->value; } else { list_del(cur); q_release_element(entry); sz--; } } return sz; } ``` 與 `q_descend` 解法一樣,差別在於改成從左手邊觀察,所以只要把迴圈換成 `list_for_each_safe` 即可。 ```c int q_ascend(struct list_head *head) { if (list_empty(head)) return 0; char *maxv = "\0"; int sz = 0; struct list_head *cur, *safe; list_for_each_safe (cur, safe, head) { sz++; element_t *entry = list_entry(cur, element_t, list); if (strcmp(entry->value, maxv) > 0) { maxv = entry->value; } else { list_del(cur); q_release_element(entry); sz--; } } return sz; } ``` ## `q_swap` 將串列的節點兩兩交換,使用 for each 走訪串列,將下個節點移動到當前節點的尾,如果下個節點是 head 就停止,head 不是用來交換的而是用來找到串列的本體。 ```c void q_swap(struct list_head *head) { struct list_head *node; list_for_each (node, head) { if (node->next == head) break; list_move_tail(node->next, node); } } ``` ```graphviz digraph { rankdir=LR node[shape=box] head node1 node2 node3 node4 node5 cur_node[shape=oval,label="node"] cur_node->node1 subgraph cluster1 { rankdir=LR color=none head->node1 node1->node2 node2->node3 node3->node4 node4->node5 } } ``` 呼叫 `list_move(node->next, node)`,node2 跟 node1 交換 ```graphviz digraph { rankdir=LR node[shape=box] head node1 node2 node3 node4 node5 cur_node[shape=oval,label="node"] cur_node->node1 subgraph cluster1 { rankdir=LR color=none head->node2 node2->node1 node1->node3 node3->node4 node4->node5 } } ``` 下一輪迴圈 node 指向 node3,依此類推將 node3 跟 node4 交換,直到 node 的下個節點是 head 就停下。 ```graphviz digraph { rankdir=LR node[shape=box] head node1 node2 node3 node4 node5 cur_node[shape=oval,label="node"] cur_node->node3 subgraph cluster1 { rankdir=LR color=none head->node2 node2->node1 node1->node3 node3->node4 node4->node5 } } ``` 實做 q_reverse 的過程中,也順手實做了 `list_swap`,可以用來替代 `list_move_tail`,暫時沒想到好的函式名稱,先命名為 `q_swap2` ```c void q_swap2(struct list_head *head) { struct list_head *node; list_for_each (node, head) { if (node->next == head) break; list_swap(node->next, node); } } ``` ## `q_delete_mid` 刪除 queue 中間的節點,可以使用快慢指標的技巧來尋找中間的節點再刪除即可。 ```c bool q_delete_mid(struct list_head *head) { struct list_head *fast = head->next, *node; list_for_each (node, head) { if (fast->next == head || fast->next->next == head) break; fast = fast->next->next; } list_del(node); q_release_element(list_entry(node, element_t, list)); return true; } ``` ## `q_delete_dup` 從單向串列的解法改寫過來,如果遇到是重複的就刪除掉節點,並將 dup 設為 true,如果當前節點是連續節點的最後一個,就用 dup 來確認是不是最後一個連續節點,是的話就刪除,由於刪除會拿掉節點,所以要用 safe 版的 for each 迴圈。 ```c bool q_delete_mid(struct list_head *head) { struct list_head *fast = head->next, *node; list_for_each (node, head) { if (fast->next == head || fast->next->next == head) break; fast = fast->next->next; } list_del(node); q_release_element(list_entry(node, element_t, list)); return true; } ``` ## `q_sort` 按照註解描述會有遞增與遞減排序的 list,因此要加入對應的 compare function 到排序演算法。 排序的實做選擇合併排序,除了實做上較容易外,合併排序屬於穩定排序,在最差的情況下的時間複雜度扔有 $O(logN)$ 的表現。 參考 [2021 的測驗題](https://hackmd.io/@sysprog/linux2021-quiz2) 修改的遞迴版 merge sort。 ```c= void q_sort(struct list_head *head, bool descend) { 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); q_sort(&left, descend); q_sort(head, descend); merge_two_lists(&left, head, &sorted, descend); list_splice_tail(&sorted, head); } ``` 5 ~ 10 行:divide 階段,將串列分割成左右兩半。用快慢指標的技巧找出找出左半邊串列的結尾 `slow`。 11 ~ 13 行:將 `head` 到 `slow` 的串列用 `list_cut_position` 切來並用 `left` 作為開頭的 list_head,此時 head 為右半邊的串列開頭。 ```graphviz digraph { rankdir=LR node[shape=box] head left slow node1 node2 node3 node4 node5 node6 slow->node3 subgraph cluster1 { rankdir=LR color=none head->node2 node2->node1 node1->node3 node3->node4 node4->node5 node5->node6 } } ``` `list_cut_position(&left, head, slow)` 分割完後如下圖所示 ```graphviz digraph { rankdir=LR node[shape=box] head left node1 node2 node3 node4 node5 node6 subgraph cluster1 { rankdir=LR color=none head->node4 node4->node5 node5->node6 // node6->head } subgraph cluster2 { rankdir=LR color=none left->node1 node1->node2 node2->node3 // node3->sorted } } ``` 14 ~ 18:呼叫 q_sort 將串列分割成 sorted lists 後,用 `merge_tow_lists` 把兩個串列合併到 `sorted` 上。 ### `merge_two_lists` `merge_two_lists` 則參考過去使用間接指標的實做,改成 list API 的版本。只要 `l1` 和 `l2` 還有節點,就用 compare function 決定下個要合併的節點移動到 `sorted` 後面,當其中一個串列合併完了就把令一個串列拼接在 sorted 後面。 依據 `descend` 判斷遞增與遞減,可以建表來紀錄兩者對應的比較函式,用以取代三元表達式(`?:`)的分支判斷。 ```c static int (*sort_cmp[])(struct list_head *l1, struct list_head *l2) = { [true] = dec_cmp, [false] = asc_cmp, }; static void merge_two_lists(struct list_head *l1, struct list_head *l2, struct list_head *sorted, bool descend) { struct list_head *next, **node; int (*cmp)(struct list_head *l1, struct list_head *l2) = sort_cmp[descend]; for (node = NULL; !list_empty(l1) && !list_empty(l2); *node = next) { node = cmp(l1, l2) ? &l1->next : &l2->next; next = (*node)->next; list_move_tail(*node, sorted); } list_splice_tail_init(list_empty(l1) ? l2 : l1, sorted); } ``` 為什麼最後要使用 `list_splic_tail_init` 而不是 `list_splic_tail` ? `list_splice_tail(list, head)` 的註解表示 `list` 開頭的 head 會被加入到 `head` 這個串列,所以後面要使用到 list 的話要初始化它的 head。 >All nodes from @list are added to to the end of the list of @head. It is similar to list_add_tail but for multiple nodes. The @list head is not modified and has to be initialized to be used as a valid list head/node again. 假設最後要和 sorted 合併的串列為 l2,而 l2 在 q_sort 的傳入的開頭為 head `qsort` $\to$ `merge_two_lists(&left, head, &sorted, descend)` ```graphviz digraph { rankdir=LR node[shape=box] head node1 node2 node3 node4 node5 sorted node6 subgraph cluster1 { rankdir=LR color=none head->node4 node4->node5 node5->node6 } subgraph cluster2 { rankdir=LR color=none sorted->node1 node1->node2 node2->node3 } } ``` 呼叫 `list_splice_tail(head, sorted)`,`head` 並沒有被拿掉,而是留在 `sorted` 串列裡面。 這導致之後呼叫 `merge_two_lists` 的時候,會陷入無窮迴圈,迴圈判斷式 `list_empty(head)` 永遠為 false,因為 `head->next` 永遠指不到自己。 ```graphviz digraph { rankdir=LR node[shape=box] head node1 node2 node3 node4 node5 sorted node6 subgraph cluster2 { rankdir=LR color=none sorted->node1 node1->node2 node2->node3 node3->node4 node4->node5 node5->node6 node6->sorted head->node4 head->node6 } } ``` 比較函式使用 strcmp 來比較字串,依據手冊的 [strcmp](https://man7.org/linux/man-pages/man3/strcmp.3.html) 提到回傳值的三種情況 >• 0, if the s1 and s2 are equal; >• a negative value if s1 is less than s2; >• a positive value if s1 is greater than s2. 若 descend 為 true,字串要由大到小排序,也就是 s2 會優先加入到 `sorted` 串列裡面,因此 l2_val 比 l1_val 大時要回傳 1,反之則回傳 0,ascend 也依此類推。 ```c static int dec_cmp(struct list_head *l1, struct list_head *l2) { char *l1_val = list_first_entry(l1, element_t, list)->value; char *l2_val = list_first_entry(l2, element_t, list)->value; return strcmp(l1_val, l2_val) <= 0 ? 0 : 1; } static int asc_cmp(struct list_head *l1, struct list_head *l2) { char *l1_val = list_first_entry(l1, element_t, list)->value; char *l2_val = list_first_entry(l2, element_t, list)->value; return strcmp(l1_val, l2_val) > 0 ? 0 : 1; } ``` 觀察上述比較函式的實,最後扔然要做分支判斷,該如何消除? 判斷式以 **0** 為基準,且 `strcmp` 為回傳**有號數**,可以利用 sign bit 即 MSB 來判斷數值是否為負數,在使用 not operator(`!`)轉成 0 和 1,以減少 merge sort 的分支數量。 ```diff +#define MSB(val) ((val) & 0x80000000) static int dec_cmp(struct list_head *l1, struct list_head *l2) { char *l1_val = list_first_entry(l1, element_t, list)->value; char *l2_val = list_first_entry(l2, element_t, list)->value; - return strcmp(l1_val, l2_val) <= 0 ? 0 : 1; + return !MSB(strcmp(l1_val, l2_val)); } static int asc_cmp(struct list_head *l1, struct list_head *l2) { char *l1_val = list_first_entry(l1, element_t, list)->value; char *l2_val = list_first_entry(l2, element_t, list)->value; - return strcmp(l1_val, l2_val) > 0 ? 0 : 1; + return !!MSB(strcmp(l1_val, l2_val)); } ``` 為了之後引入 kernel 的 list_sort,顯然不能使用只有兩個參數的比較函式,合併時也不能僅用 true 和 false 來決定下個要合併哪個節點,而是要和 0 做比較,應此將 compare 函式修改成如下。 ```c static int asc_cmp(void *priv, const struct list_head *l1, const struct list_head *l2) { char *s1 = list_entry(l1, element_t, list)->value; char *s2 = list_entry(l2, element_t, list)->value; return strcmp(s1, s2); } static int dec_cmp(void *priv, const struct list_head *l1, const struct list_head *l2) { return asc_cmp(priv, l2, l1); } ``` 手冊 [strcmp(3)](https://man7.org/linux/man-pages/man3/strcmp.3.html) 提到下方描述 >strcmp() returns an integer indicating the result of the comparison, as follows: • 0, if the s1 and s2 are equal; • a negative value if s1 is less than s2; • a positive value if s1 is greater than s2. 回傳值與兩個數值相減來判斷誰大誰小的概念一樣,所以 `strcmp(s1, s2)` 回傳 -5,那麼 s1 與 s2 調過來 `strcmp(s2, s1)` 就會回傳 5,可依此特性簡化升序與降序排列,合併函式就會依照正負數對調合併的節點。 合併函式的函式指標型態改成和 kernel 的 list_sort 的 cmp 一樣,不過傳入到比較函式會變成 next,如果拿原本的比較函式,除了結果不正確,用在 list_sort 還會造成 Segementation fault。 ```c static void merge_two_lists(struct list_head *l1, struct list_head *l2, struct list_head *sorted, bool descend) { struct list_head *next, **node; list_cmp_func_t cmp = sort_cmp[descend]; for (node = NULL; !list_empty(l1) && !list_empty(l2); *node = next) { node = cmp(NULL, l1->next, l2->next) <= 0 ? &l1->next : &l2->next; next = (*node)->next; list_move_tail(*node, sorted); } list_splice_tail_init(list_empty(l1) ? l2 : l1, sorted); } ``` ## `q_merge` 改寫自 [yanjiew1](https://hackmd.io/@yanjiew/linux2023q1-lab0) 的解法,利用雙向 linked list 的特性,每次迭代的時候,將頭跟尾串接到 sorted 串列上,如果頭跟尾是同一個,表示已經走完串列了,就把頭拼接到 sorted 上(選擇尾巴也可以),合併完之後在透過 `q_sort` 合併成一條 sorted list。最後把 sorted 的串列接到 head 上。 ```c int q_merge(struct list_head *head, bool descend) { if (!head) return 0; LIST_HEAD(sorted); queue_contex_t *cur = NULL; queue_contex_t *last = list_last_entry(head, queue_contex_t, chain); list_for_each_entry (cur, head, chain) { if (cur == last) { list_splice_init(cur->q, &sorted); break; } list_splice_init(cur->q, &sorted); list_splice_init(last->q, &sorted); last = list_entry(last->chain.prev, queue_contex_t, chain); } q_sort(&sorted, descend); int size = q_size(&sorted); list_splice_init(&sorted, list_first_entry(head, queue_contex_t, chain)->q); return size; } ``` --- `make test` 有時後會 95/100 有時候滿分,要開始讀論文才能把統計執行時間的函式修正確。 ``` +++ TESTING trace trace-17-complexity: # Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant ERROR: Probably not constant time or wrong implementation ERROR: Probably not constant time or wrong implementation Probably constant time Probably constant time --- trace-17-complexity 0/5 --- TOTAL 95/100 make: *** [Makefile:60: test] Error 1 ``` --- # 引入 list_sort.c 教材已寫清楚程式碼大部分的運作原理,搭配原始碼可以有效率的看懂程式碼,所以這裡改成做實驗還有驗證 paper <!-- TODO: study paper and experiment 和自己實做的 merge sort 做比較 --> ## 簡化程式碼,觀察輸出的指令 待整理 `merge` 原本的 merge 函式可以透過 indirect pointer 簡化原本 if-else 區塊相同操作的部份,其實就像在做 [merge two sorted lists](https://leetcode.com/problems/merge-two-sorted-lists/) 一樣 ```c __attribute__((nonnull(2, 3, 4))) static struct list_head * merge(void *priv, list_cmp_func_t cmp, struct list_head *a, struct list_head *b) { /* cppcheck-suppress unassignedVariable */ struct list_head *head, **tail = &head; struct list_head *node; for (;;) { /* if equal, take 'a' -- important for sort stability */ struct list_head **indir = cmp(priv, a, b) <= 0 ? &a : &b; node = *indir; *tail = node; tail = &node->next; *indir = node->next; if (!node->next) { break; } } *tail = (struct list_head *) ((uintptr_t) a | (uintptr_t) b); return head; } ``` `merge_final` ```c __attribute__((nonnull(2, 3, 4, 5))) static void merge_final( void *priv, list_cmp_func_t cmp, struct list_head *head, struct list_head *a, struct list_head *b) { struct list_head *tail = head; struct list_head *node; u8 count = 0; for (;;) { /* if equal, take 'a' -- important for sort stability */ struct list_head **indir = cmp(priv, a, b) <= 0 ? &a : &b; node = *indir; tail->next = node; node->prev = tail; tail = node; *indir = node->next; if (!node->next) break; } node = (struct list_head *) ((uintptr_t) a | (uintptr_t) b); /* Finish linking remainder of list b on to tail */ tail->next = node; do { /* * If the merge is highly unbalanced (e.g. the input is * already sorted), this loop may run many iterations. * Continue callbacks to the client even though no * element comparison is needed, so the client's cmp() * routine can invoke cond_resched() periodically. */ if (unlikely(!++count)) cmp(priv, node, node); node->prev = tail; tail = node; node = node->next; } while (node); /* And the final links to make a circular doubly-linked list */ tail->next = head; head->prev = tail; } ``` <!-- GCC 編譯兩個版本的輸出的指令一模一樣, --> ## 使用 GDB 觀察排序過程 [lab0 - :loudspeaker: Linux 核心的鏈結串列排序](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-e#%E4%BD%95%E6%99%82%E5%90%88%E4%BD%B5) 何時合併的表格整理變數 count 在各階段時 pending 上的節點數量,如果是自己做實驗該如何得知 pending 每個子串列的數量,還有其節點的內含值? 首先想到的就是 codegen 生成 graphviz script,但是這麼做要在 list_sort.c 加入很多程式碼又要額外處裡字串問題,寫完後除了驗證正確性外,還要用 `#ifdef` 覆蓋額外的程式碼,導致原本乾淨有品味的程式碼被我弄得一團亂。 要解決上述問題,可以使用強大的 GDB 搭配 [Python API](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-API.html) 在執行時期拿到 pending 串列,並用 Python 完成 codegen、存檔和畫圖,既避免弄髒程式碼又能夠練習 GDB。 靈感來自[手写红黑树, 再用gdb画给你看](https://www.bilibili.com/video/BV1Pw411S7Ea/?spm_id_from=333.337.search-card.all.click&vd_source=fd781c26b2abd26ec39acdc116130ae6) ### 新增 dump_sort 命令到 GDB dump_sort 用來做這次實驗的命令,格式如下 >dump_sort list [-o *png_path*] [-type *list_type*] `list`(必填) : 傳入要觀察的串列開頭,預設是當作都傳入 pending 串列,也就是 prev 指向下個子串列開頭,next 指向子串列的下個節點,另一種則是原本的雙向鍊結串列 `-type`(可忽略): pending 和 origin 兩種類型,如上所述 `-o`(可忽略): 輸出的檔案名稱,如果有包含資料夾的路徑在裡面,會自動建立資料夾。只會輸出 png 檔,所以無論傳入的副檔名是 jpg 還是 raw 都會被改為 png 完整程式碼請參考 [dump_sort.py](https://github.com/LambertWSJ/lab0-c/blob/master/scripts/dump_sort.py)。 要添加 gdb command 大致流程如下 - 建立繼承 gdb.Command 的 class - 物件初始化時傳給父類別 command 名稱,還有 command 類型,若沒有特別指定命令只能在什麼情況下用 (e.g. TUI),可以傳入 gdb.COMMAND_USER - 實做 gdb.Command 的 invoke 介面。GDB 使用到自定義的命令時會呼叫到這個函式。 >This method is called by GDB when this command is invoked. [CLI Commands In Python (Debugging with GDB)](https://sourceware.org/gdb/current/onlinedocs/gdb.html/CLI-Commands-In-Python.html) 這次 demo 只用到 gdb 函式庫的 `parse_and_eval` 和 `execute`,兩者都會讓 GDB 執行傳入的字串,但是前者會回傳結果,後者一般情況下不會有回傳值,除非 `to_string=True` 才會回傳結果。 考量到程式的可讀性,要取值時 `parse_and_eval`,其它都用 `execute`,具體使用方法如下程式碼的第 21 行走訪 pending 串列開始。 先看 26 行,從 pend 拿到子串列 list 的開頭後走訪子串列,節點的內含值使用包裝過 `container_of` 的 [node_val](https://github.com/LambertWSJ/lab0-c/blob/master/list_sort.c#L11) 取得,31 行執行完後回傳的是 `element_t` 的開頭,裡面的欄位用字典保存,因此要存取資料的話要傳入欄位名稱的字串,例 `val['value']`,回傳的結果放到 div_list,走訪完子串列,再將 div_list 加入到 sort_lists 以便後後續做 codegen 跟畫圖。 要換到下個子串列的話就要存取 `pending->prev`,也就是第 34 行的操作,如果沒有子串列就退出,有的話就重複上述過程,就能在 Python 的環境中使用整個 pending 串列。 ```python= import gdb class DumpDivide(gdb.Command): def reset(self): self.png_path = None self.list_type = "pending" def __init__(self) -> None: super().__init__("dump_sort", gdb.COMMAND_USER) self.reset() def invoke(self, args: str, from_tty: bool) -> None: self.reset() div_list = [] sort_lists = [] args = args.split(" ") self.parse_args(args) if self.list_type == "pending": gdb.execute(f"p $pend = {args[0]}") while True: if str(gdb.parse_and_eval("node_val($pend)")) == "0x0": break else: gdb.execute("p $list = $pend") while True: if str(gdb.parse_and_eval("node_val($list)")) == "0x0": break else: val = gdb.parse_and_eval("node_val($list)") div_list.append(val['value'].string()) gdb.execute("p $list = $list->next") gdb.execute("p $pend = $pend->prev") sort_lists.append(div_list) div_list = [] ``` ### 設置斷點 在 list_sort 的分割與合併階段下斷點,用來觀察兩階段執行完的 pending 串列。 分割與合併階段斷點的參考位置如下程式碼 highlight 的位置,使用這兩處的 pending 串列傳入到 `dump_sort` 可以印出這兩階段的結果。 - [ ] list_sort.c ````diff __attribute__((nonnull(2, 3))) void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp) { do { /* ... */ /* Move one element from input list to pending */ list->prev = pending; pending = list; list = list->next; pending->next = NULL; + count++; /* divide breakpoint */ } while (list); for (;;) { struct list_head *next = pending->prev; if (!next) break; list = merge(priv, cmp, pending, list); + pending = next; /* merge breakpoint */ } /* The final merge, rebuilding prev links */ merge_final(priv, cmp, head, pending, list); } ```` - [ ] qtest.c 原本想再 list_sort 的 merge_final 用 dump_sort 印出最終的排序好串列,但是這時候的 head 還沒有完全接好,要等到 merge_final 執行完,否則會存取到 `0xdeadbeaf`,所以改成執行完 list_sort 的下一行下斷點並傳入 `current->q` 印出結果。 ```diff bool do_sort(int argc, char *argv[]) { /* ... */ m_sort = sort_opt ? q_ksort : q_sort; set_noallocate_mode(true); if (current && exception_setup(true)) m_sort(current->q, descend); + exception_cancel(); set_noallocate_mode(false); } ``` ### GDB 測試 command 測資:建立 01~16 個節點,打亂後交由 list_sort 排序。篇幅關係不放完整 cmd 上來 - [ ] lab0/scripts/sort_step.cmd 為了避免每次執行到斷點就要停下來敲命令,這裡使用 gdb 的 command 命令規定執行到斷點時要執行的操作。檔名使用 `$cnt` 產生流水號後面加上階段,dump_sort 做完後更新流水號並繼續執行下去(c, continue)。 ```bash b list_sort.c:237 b list_sort.c:249 b qtest.c:634 p $cnt=1 # divide command 1 eval "set $png=\"./sort_step/%d_divide.png\"", $cnt dump_sort pending -o $png p $cnt += 1 c end # merge command 2 eval "set $png=\"./sort_step/%d_merge.png\"", $cnt dump_sort pending -o $png p $cnt += 1 c end # final command 3 eval "set $png=\"./sort_step/%d_list.png\"", $cnt dump_sort current->q -type origin -o $png p $cnt += 1 c end ``` ``` $ gdb -q -x ./scripts/dump_sort.py --args ./qtest -f ./traces/demo.cmd (gdb) source ./scripts/sort_step.cmd Breakpoint 4 at 0x55555555fda0: file list_sort.c, line 237. Breakpoint 5 at 0x55555555fdc4: file list_sort.c, line 249. Breakpoint 6 at 0x555555556e10: file qtest.c, line 634. $730 = 1 (gdb) r Starting program: /home/lambert-wu/Repos/lab0-c/qtest -f ./traces/demo.cmd ``` 假設 shuffle 完的串列入下 ``` l = [11 02 12 01 05 07 03 04 14 15 16 06 10 08 13 09] ``` 執行完成後,圖片和 dot 檔會存入到 lab0/sort_step/,暫時還不知道怎麼用 convert 命令把一系列的圖片大小不一 png 檔轉成每個步驟階段的串列置中顯示的 gif 檔,這樣會更生動 ``` $ ls ./sort_step/ 10_divide.dot 12_divide.png 15_divide.dot 17_merge.png 1_divide.dot 2_divide.png 5_divide.dot 7_divide.png 10_divide.png 13_divide.dot 15_divide.png 18_merge.dot 1_divide.png 3_divide.dot 5_divide.png 8_divide.dot 11_divide.dot 13_divide.png 16_divide.dot 18_merge.png 20_list.dot 3_divide.png 6_divide.dot 8_divide.png 11_divide.png 14_divide.dot 16_divide.png 19_merge.dot 20_list.png 4_divide.dot 6_divide.png 9_divide.dot 12_divide.dot 14_divide.png 17_merge.dot 19_merge.png 2_divide.dot 4_divide.png 7_divide.dot 9_divide.png ``` 上述測資分割完的 pending list 如下圖所示,圖形呈現參考〈[Queue-Mergesort](https://www.sciencedirect.com/science/article/pii/002001909390088Q?via%3Dihub)〉。 Note: 藍色箭頭為 prev,紅色箭頭為 next。 pending 每個排序好的子串列的數量都是 2 的冪,充分利用雙向鍊結串列的特性不需要用額外的空間配置 Queue,也符合教材的表格最終結果。 ```graphviz digraph { rankdir="LR" null[shape=none]; node09[label="09", shape="box"] node13[label="13", shape="box"] node08[label="08", shape="box"] node10[label="10", shape="box"] node06[label="06", shape="box"] node14[label="14", shape="box"] node15[label="15", shape="box"] node16[label="16", shape="box"] node01[label="01", shape="box"] node02[label="02", shape="box"] node03[label="03", shape="box"] node04[label="04", shape="box"] node05[label="05", shape="box"] node07[label="07", shape="box"] node11[label="11", shape="box"] node12[label="12", shape="box"] node09->node13->node08->node06->node01->null[color=blue]; { rank="same"; node08 -> node10[color=red] } { rank="same"; node06 -> node14 -> node15 -> node16[color=red] } { rank="same"; node01 -> node02 -> node03 -> node04 -> node05 -> node07 -> node11 -> node12[color=red] } } ``` 最後一次合併前的 pending 串列如下, 兩個子串列長度一樣。 ```graphviz digraph { rankdir="LR" null[shape=none]; node06[label="06", shape="box"] node08[label="08", shape="box"] node09[label="09", shape="box"] node10[label="10", shape="box"] node13[label="13", shape="box"] node14[label="14", shape="box"] node15[label="15", shape="box"] node16[label="16", shape="box"] node01[label="01", shape="box"] node02[label="02", shape="box"] node03[label="03", shape="box"] node04[label="04", shape="box"] node05[label="05", shape="box"] node07[label="07", shape="box"] node11[label="11", shape="box"] node12[label="12", shape="box"] node06->node01->null[color=blue]; { rank="same"; node06 -> node08 -> node09 -> node10 -> node13 -> node14 -> node15 -> node16[color=red] } { rank="same"; node01 -> node02 -> node03 -> node04 -> node05 -> node07 -> node11 -> node12[color=red] } } ``` 遞增排序完的串列如下圖所。 ```graphviz digraph { rankdir="LR" head[shape=none]; node01[label="01", shape="box"] node02[label="02", shape="box"] node03[label="03", shape="box"] node04[label="04", shape="box"] node05[label="05", shape="box"] node06[label="06", shape="box"] node07[label="07", shape="box"] node08[label="08", shape="box"] node09[label="09", shape="box"] node10[label="10", shape="box"] node11[label="11", shape="box"] node12[label="12", shape="box"] node13[label="13", shape="box"] node14[label="14", shape="box"] node15[label="15", shape="box"] node16[label="16", shape="box"] head->node01->node02->node03->node04->node05->node06->node07->node08->node09->node10->node11->node12->node13->node14->node15->node16[color=blue]; node16->node15->node14->node13->node12->node11->node10->node09->node08->node07->node06->node05->node04->node03->node02->node01->head[color=red]; } ```

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully