# linux2025-homework2 contributed by <[``Ian-Yen``](https://github.com/Ian-Yen/lab0-c)> {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 2025q1 第 1 週測驗題 ### 測驗一 這邊先把答案填上: `AAAA : &l->head` `BBBB : before` `CCCC : &(*p)->next` `DDDD : (*p)->next` :::success 解釋上方程式碼運作原理 ::: ```c static inline void list_insert_before(list_t *l, list_item_t *before, list_item_t *item) { list_item_t **p; for (p = &l->head; *p != before; p = &(*p)->next) ; *p = item; (*p)->next = before; } ``` 根據題目敘述,這個函式希望能夠把 `item` 放在 `l` 這個 `list` 的 `before` 之前。 而這個list最前面的節點就是 `head` 。 ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] before [label = "<name>before"] node1 [label = "item"] node2 [label = "..."] node3 [label = "<name>..."] null [label = "NULL"] p->head head->node3:name node3-> before before->node2 node2->null } ``` 先把 `p` 指向 `head` ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] before [label = "<name>before"] node1 [label = "item"] node2 [label = "..."] node3 [label = "<name>..."] null [label = "NULL"] head->node3:name p->node3 node3-> before before->node2 node2->null } ``` 往後面一個一個找 ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] before [label = "<name>before"] node1 [label = "item"] node2 [label = "..."] node3 [label = "<name>..."] null [label = "NULL"] head->node3:name node3-> before p->before before->node2 node2->null } ``` 找到 `before` 這時跳出迴圈。 ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] before [label = "<name>before"] node1 [label = "item"] node2 [label = "..."] node3 [label = "<name>..."] null [label = "NULL"] head->node3:name node3-> node1 p->node1 before->node2 node2->null } ``` 把 `item` 塞進去。 ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] before [label = "<name>before"] node1 [label = "item"] node2 [label = "..."] node3 [label = "<name>..."] null [label = "NULL"] head->node3:name node3-> node1 p->node1 node1->before before->node2 node2->null } ``` 把 `before` 接回去。 下面是如果 `before` 是 `head` 。 ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] item p->head } ``` `p` 指向 `head` 然後跳出去。 ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] p->item->head } ``` `item` 變到前面了。 下面是如果 `before` 是 `NULL`。 ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] node1 [label = "..."] node2 [label = "NULL(before)"] item head->node1->node2 p->node2 } ``` `p` 找到最後一個的 `next` 也就是 `NULL`。 ```graphviz digraph linked_list { node [shape=record]; rankdir=LR; p [label = "p"] head [label = "head"] node1 [label = "..."] node2 [label = "NULL(before)"] item head->node1->item p->item->node2 } ``` `item` 跑到最後一個了。 其他行為都是為定義的行為。 :::success 在現有的程式碼基礎上,撰寫合併排序操作 ::: ```c static int list_size(list_t *l) { if(!l || !l->head) return 0; int ret = 1; for(struct list_item *h = l->head;h->next;h = h->next, ret++) ; return ret; } ``` 先補上裡面沒有的 `list_size`。 #### `merge`。 ```c static inline list_item_t *merge(list_item_t *l, list_item_t *r) { list_item_t *h = NULL; list_item_t **node = &h; for(;l && r;){ if(l->value <= r->value) { *node = l; node = &l->next; l = l->next; } else { *node = r; node = &r->next; r = r->next; } } *node = l ? l : r; return h; } ``` 先判斷要先放 `l` 或是 `r` 的下一個值,然後使用到指標的指標來把他放進去,並先找到下一個值應該要被放在哪裡,一直循環直到某一邊空了,就直接把另一邊放進去然後回傳。 ##### 例外處理 如果 `l` 與 `r` 只有一個不是 `NULL` 或是兩個都是 `NULL` ,就會直接跳過迴圈,然後先找有沒有不是 `NULL` 的那個回傳,都是的話就隨便傳一個,我這邊用 `l` 判斷所以都是 `NULL` 就會回傳 `r` 。 #### `sort` 這邊原本要用 `linux` 核心風格的 `list_sort` ,但是單向鍊結串列沒有雙向鍊結串列的優點,少一邊不能串在一起,所以在這邊使用陣列來做,擷取他的想法,用 `count` 來判斷要合併幾個,雖然少了保證合併時大小的比值至多為 `2` 的優勢 ,但這樣可以不用在合併的時候多耗費 $O(log_2n)$ 的時間來把所有的東西往前移一格,且也能盡量作到合併時大小為 `1:1` 的優勢。 ```c static inline void sort(list_t *l) { if(!l || !l->head) return; list_item_t *stack[12] = {NULL}; int count = 0, pos = 0; for (list_item_t *p = l->head, *safe;p; p = safe) { safe = p-> next; p->next = NULL; stack[pos++] = p; for (int tmp = count; tmp & 1; tmp >>= 1, pos--) stack[pos - 2] = merge(stack[pos - 1], stack[pos - 2]); count++; } for (;pos > 1 ;pos--) stack[pos - 2] = merge(stack[pos - 1], stack[pos - 2]); l->head = stack[0]; } ``` ### 測驗二 這邊先把答案填上 `EEEE : (*pred_ptr)->r` `FFFF : &(*pred_ptr)->r` :::success 解釋上方程式碼運作的原理,並嘗試補完程式碼,使其可獨立執行,應提供相關的測試程式碼 ::: 這邊的 `remove_free_tree` 是希望找到一個擁有大於等於 `target` 的 `size` 的所有位置裡面的最小值,然後把它從樹裡面移除,如果這棵數裡面所有的節點都比 `target` 的 `size` 小,就回傳NULL。 而因為這棵數是一棵 `binary search tree` 所以要找到目標位置,可以先看這個節點的 `size` 有沒有大於 `target` ,如果沒有的話,就往右邊的子節點去找,如果有的話,就先看看往左邊的子節點的 `size` 有沒有大於等於 `target` 要的 `size` ,有救往左邊的子節點找,沒有的話就回傳現在的節點。 :::success 解釋上方程式碼運作的原理,並嘗試補完程式碼,使其可獨立執行,應提供相關的測試程式碼 ::: 要讓這邊的程式碼能動,只要補足 `find_free_tree` 即可。 ```c block_t **find_free_tree(block_t **root, block_t *target) { if(!(*root)) return root; if((*root)->l && (*root)->l->size >= target->size) return find_free_tree(&(*root)->l, target); else if((*root)->size >= target->size) return root; else return find_free_tree(&(*root)->r, target); } ``` :::success 參閱 memory-allocators,針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現 ::: 待辦 ### 測驗三 這邊先把答案填上: `GGGG : head->prev = prev` `HHHH : list_entry(pivot, node_t, list)->value` `IIII : list_entry(n, node_t, list)->value` `JJJJ : pivot` `KKKK : right` :::success 解釋上述程式碼運作原理 ::: #### 分析quick_sort實做細節 首先把第一個節點當作 `pivot` ,並用他的值來當作 `value` 從左邊往右邊找,當一個節點的值小於等於 `value` 就把它丟進 `left` 其他丟進 `right`。 然後把 `left` , `pivot` , `right` 分別照順序丟進 `begin` , `begin` 裡面紀錄的值的值域可以很明顯的發現是排序後的 `(begin[i]的最大值 < begin[i + 1]的最小值)` 然後往右找,先把右邊的做好了,再做左邊。 :::success 研讀〈[A comparative study of linked list sorting algorithms](https://dl.acm.org/doi/pdf/10.1145/225890.225893)〉,分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作 ::: 從這篇裡面可以看到,他利用雙向 `link_list` 做了一個 `tree_sort` 作法是將 `prev` 與 `next` 拿去當成 `l` 與 `r` 然後用遞迴的方式再把它變回一個雙向 `link_list` 的形式。 ```c static struct void traverse(struct list_head *root, struct list_head *head) { struct list_head *work; if(!root){ traverse(root->prev); work = root->next; list_add(root, head); traverse(work); } } static void tree_sort(struct list_head *head) { if(!head) return; struct list_head *result = NULL, **p = &result, *h = NULL, *t = NULL; element_t *pos, *safe; list_for_each_entry_safe (pos, safe, head, list) { p = &result; while(*p) { if(strcmp(pos->value, list_entry(*p, element_t, list)->value) >= 0) p = &(*p)->next; else p = &(*p)->prev; } *p = &pos->list; (*p)->next = NULL; (*p)->prev = NULL; } head->next = head; head->prev = head; traverse(result, head); } ``` ## 2025q1 第 2 週測驗題 ### 測驗一 這邊先把答案填上: `AAAA : list_first_entry` `BBBB : list_del` `CCCC : list_move_tail` `DDDD : list_add` `EEEE : list_splice` `FFFF : list_splice_tail` :::success 解釋上方程式碼運作原理,改進 random_shuffle_array 也探討 list_for_each_entry_safe 建構的迴圈中,若將 list_move_tail 換為 list_move,為何會無法滿足 stable sorting ::: 他的運作原理跟上面的測驗3很像,只是把迴圈的部份改成遞迴,這邊就不多贅述了。 把 `list_move_tail` 改成 `list_move` 不能滿足 `stable sorting` 的原因是因為這樣順序會顛倒,如果有兩個相同的 `value` 他們的順序會跟初始的不一樣。 #### 改進 random_shuffle_array 用 `hw1` 提到過的 `Fisher-Yates Shuffle` 改進 `random_shuffle_array` ```c static inline void random_shuffle_array(uint16_t *operations, uint16_t len) { for (uint16_t i = 0; i < len; i++) { uint16_t j = i + get_unsigned16() % (len - i); uint16_t temp = operations[i]; operations[i] = operations[j]; operations[j] = temp; } } ``` :::success 將程式碼整合進 lab0 提及的 qtest,並探討環狀雙向鏈結串列的排序效能,並善用 perf 一類的效能分析工具,探討包含 Linux 核心 list_sort 在內排序實作的效能表現並充分解釋 ::: ![cdf_plot](https://hackmd.io/_uploads/SkqCLbLhkl.png) ### 測驗二