# 2024q1 Homework2 (quiz1+2) contributed by < [yuyuan0625](https://github.com/yuyuan0625) > ## 第一週測驗 ### 測驗一 **鏈結串列結構體 `node_t`:** ```c typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t; ``` ```graphviz digraph node_t { node [shape= "record"]; rankdir= "LR"; subgraph cluster_0 { label= "node_t"; node1 [label= "<head>value | next | {<left> left | <right> right}"]; } } ``` **補齊程式碼** ```c node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) left = &(AAAA); return *left; } ``` 此函式的目的是尋找鏈結串列的尾端,因此需要逐一走訪每個節點,`node_t **left` 為指標的指標,因此每一次迴圈都需要將 `left` 的值更新為指向下一個節點指標( `next` )的位址, 因此 `AAAA` 應為 `(*left)->next` 示意圖: ```graphviz digraph initial { graph [fontsize=12, compound=true]; node [shape="box"]; rankdir = "LR" "**left" [fontcolor=red, color=red, shape=plaintext] "*head" [shape=plaintext]; { rank=same; "**left"->"*head" [color=red] "*head"->node1 ; } { node1->node2 node2->node3 } } ``` ```c int list_length(node_t **left) { int n = 0; while (*left) { ++n; left = &(BBBB); } return n; } ``` 此函式和 `list_tail()` 相同,每一次迴圈都需要往下一個節點走訪。 因此 `BBBB` 同為 `(*left)->next)` ```c void quick_sort(node_t **list) { int n = list_length(list); int value; int i = 0; int max_level = 2 * n; node_t *begin[max_level], *end[max_level]; node_t *result = NULL, *left = NULL, *right = NULL; begin[0] = *list; end[0] = list_tail(list); while (i >= 0) { node_t *L = begin[i], *R = end[i]; if (L != R) { node_t *pivot = L; value = pivot->value; node_t *p = pivot->next; pivot->next = NULL; while (p) { node_t *n = p; p = CCCC; list_add(n->value > value ? &right : &left, n); } begin[i] = left; end[i] = DDDD; begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = EEEE; left = right = NULL; i += 2; } else { if (L) list_add(&result, L); i--; } } *list = result; } ``` 在此段程式碼中, **內部迴圈**會由 `begin[i]->next` 循序走訪至 `end[i]`,並和 `pivot` 比較,若大於 `pivot` 加入 `right`,反之則加入 `left`。 因此,和前兩題相同,都是要循序走訪鏈結串列,`p = p->next`。 **外部迴圈**:將鏈節串列分為三段: 1.小於等於 `pivot` 2.`pivot` 3.大於等於 `pivot` 並分別將其串列開頭分別存在 `begin[i]` , `begin[i+1]` , `begin[i+2]`,並使用 `end[i]` , `end[i+1]` , `end[i+2]` 儲存串列尾端。 因此 `DDDD` 就是 `left` 鏈結串列的尾端,使用 `&list_tail(left)` 來獲得其位址。同理,`EEEE` 即為 `&list_tail(right)` 。 #### 延伸問題 1. 解釋上述程式碼的運作原理,提出改進方案並予以實作。 以實際例子解釋: ```graphviz digraph initial { node [shape="box"]; rankdir = "LR"; begin0 [label= "begin[0]", shape= plaintext] end0 [label= "end[0]", shape= plaintext] node4 [label= "4"] node3 [label= "3"] node5 [label= "5"] node1 [label= "1"] NULL [shape="plaintext"] { node4->node3 node3->node5 node5->node1 node1->NULL } { rank= same; begin0->node4 } { rank= same; end0->node1 } } ``` 選第一個節點 `4` 作為 `pivot` ,將 `pivot` 從串列移除,並比較後面所有節點和 `pivot` 的大小關係,將小於 `pivot` 的節點利用 `list_add` 加入 `left` ,大於 `pivot` 的節點加入 `right` 。 ```graphviz digraph L1 { node [shape="plaintext"]; rankdir = "TB"; node4 [label= "4", shape="box"] node3 [label= "3", shape="box"] node5 [label= "5", shape="box"] node1 [label= "1", shape="box"] { "begin[1]"->node4 node4->"end[1]" [dir=back] } { left->node1 "begin[0]"->node1 node1->node3 node3->"end[0]" [dir=back] } { right->node5 "begin[2]"->node5 node5 -> "end[2]" [dir=back] } } ``` `i += 2`,此時 `i=2`,`begin[2]==end[2]`,因此利用 `add_list` 將 5 加入 `result`,`i--`。 ```graphviz digraph res { rankdir = "LR" node [shape="box"]; node5 [label= "5"] result [shape=plaintext]; result->node5 } ``` `i=1` ,`begin[1]==end[1]`,將 4 加入`result`,`i--`。 ```graphviz digraph res { rankdir = "LR" node [shape="box"]; node4 [label= "4"] node5 [label= "5"] result [shape=plaintext]; result->node4 node4->node5 } ``` `i=0` , 選 1 做 `pivot`,3 加入`right`, `i+=2` ```graphviz digraph L2 { node [shape="plaintext"]; rankdir = "TB"; node1 [label= "1", shape="box"] NULL node3 [label= "3", shape="box"] { "begin[1]"->node1 "end0[0]"->node1 } { left->NULL "begin[0]"->NULL "end[0]"->NULL } { right->node3 "begin[2]"->node3 "end[2]"->node3 } } ``` `i=2`,`begin[2]==end[2]`,將 3 加入 `result` ,`i--` `i=1`,`begin[1]==end[1]`,`i--` `i=0`,`begin[0]==end[0]`,將 1 加入 `result` ,`i--` ```graphviz digraph res { rankdir = "LR" node [shape="box"]; node1 [label= "1"] node3 [label= "3"] node4 [label= "4"] node5 [label= "5"] result [shape=plaintext]; result->node1 node4->node5 node3->node4 node1->node3 } ``` :::warning **問題與改進方案**: 此程式每次都只挑選第一個節點作為`pivot` ,若剛好鏈結串列為遞增時,每次 `left`皆為 NULL , `right` 皆為為 `pivot` 以外的其他節點,這樣就會需要走訪每一個節點才會結束,時間複雜度為 $O(n^2)$ 若要解決此問題,可以使用[median of three](https://stackoverflow.com/questions/7559608/median-of-three-values-strategy)或更進一步使用[median of median](https://en.wikipedia.org/wiki/Median_of_medians)來避免此問題。 **TODO** ::: ### 測驗二 `merge()` 會將 `a` , `b` 兩個已排序的鏈結串列合併成新的鏈結串列,一開始因為鏈結串列為空,要將 `**tail` 初始化為 `&head`。 接著利用迴圈比較 `a` , `b` 的第一個節點,將較小者加入新鏈結串列的尾部, 加入後要將 `tail` 指標向後移,下次加入節點時才能繼續加入鏈結串列尾端, 因此 `BBBB` 為 `&a->next`,`CCCC` 則為 `&b->next`。 **程式碼改進** 可將 for 迴圈替換成 while 迴圈,減少迴圈內 if 的判斷次數 ```diff - for (;;) { + while(a && b){ /* if equal, take 'a' -- important for sort stability */ if (cmp(priv, a, b) <= 0) { *tail = a; tail = BBBB; a = a->next; - if (!a) { - *tail = b; - break; - } } else { *tail = b; tail = CCCC; b = b->next; - if (!b) { - *tail = a; - break; - } } + tail = &(*tail)->next; } + *tail = (a) ? a : b; ``` `build_prev_link()` 會走訪每個節點,並且建立每個節點的 `prev` , 最後需要將頭尾相連。 ```diff - DDDD = head; + tail->next = head; - EEEE = tail; + head->prev = tail; ``` 在 `timsort()` 中,`stk_size` 表示 run 的個數,在合併的過程中 `stk_size` 會逐漸減少。因為是剩下最後一個 run 時才會需要用到 `build_prev_link()` 將鏈結串列的頭尾相接,所以可以推斷 `FFFF` 為 1。 :::warning **問題與改進方案**: 此程式沒有採用題幹介紹的Galloping mode方法, 後續可以加入Galloping mode進行實做,並且比較兩者的差異 **TODO** ::: --- ## 第二週測驗 ### 測驗一 ```c static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { if (h->first) h->first->pprev = &n->next; //藍箭頭 n->next = AAAA; //紅箭頭 n->pprev = &h->first; //橘箭頭 h->first = n; } ``` 此函式是要將新的節點加入串列的前端,所以 `AAAA` 應為 `h->first` ```graphviz digraph node_t { node [shape= "record"]; rankdir= "LR"; splines = false; list_head [label= "<h>list_head | <f>first"] node1 [label= "<self>node1 | {<p>pprev | <n>next}"] node2 [label= "<self>node2 | {<p>pprev | <n>next}"] NULL [shape= plaintext] {rank = "min" list_head} list_head -> node1 -> node2 [ weight = 10, style=invis ] list_head:f->node1:self node1:n->node2:self node1:p->list_head:f node2:n->NULL node2:p->node1:n } ``` ```graphviz digraph node_t { node [shape= "record"]; rankdir= "LR"; splines=false; list_head [label= "<h>list_head | <f>first"] node_n [label= "<self>node_n | {<p>pprev | <n>next}"] node1 [label= "<self>node1 | {<p>pprev | <n>next}"] node2 [label= "<self>node2 | {<p>pprev | <n>next}"] NULL [shape= plaintext] {rank = "min" list_head} list_head -> node_n-> node1 -> node2 [ weight = 10, style=invis ] list_head:f->node_n:self node_n:n->node1:self [color=red] node_n:p->list_head:f [color=orange] node1:n->node2:self node1:p->node_n:n [color=blue] node2:n->NULL node2:p->node1:n } ``` ```c static int find(int num, int size, const struct hlist_head *heads) { struct hlist_node *p; int hash = (num < 0 ? -num : num) % size; hlist_for_each (p, BBBB) { struct order_node *on = CCCC(p, struct order_node, node); if (num == on->val) return on->idx; } return -1; } ``` 此段程式是為了尋找 `num` 的位置,會先計算 hash 取得對應的 `hlist_head` ,接著逐一走訪該鏈結串列。由此可知 `BBBB` 為 `&head[hash]` , `CCCC` 為 `list_entry` 。 `dfs()` 本題的 input 為兩個 int 陣列 `preorder` 和 `inorder`。 在此函式中會使用 `*tn` 存取前序第一個節點的值,因為前序的第一個節點即為根節點, 並利用 `find()` 尋找此數值在中序的位置,左側為左子樹,右側為右子樹,再對左/右子樹繼續遞迴。 在下圖範例中, 會先查看 `preorder` 的第一個節點的值 3 ,在中序的位置 `idx` ,可知`idx = 1`。 `(idx - in_low)` 可以算出中序第一個節點和 `idx` 之間的間隔, 也就是左子樹的長度。而右子樹是從 `idx + 1` 到 `in_high` , 因此右子樹的長度為 `in_high - idx - 1` , 利用`pre_high - (in_high - idx - 1)` 即可獲得前序中左子樹的 head 位置。而中序內左子樹的範圍就是 `in_low` 到 `idx - 1` , 右子樹為 `idx + 1` 到 `in_high`。 ```graphviz digraph lists { node [shape= "none"] splines = false preorder[shape = record, label = " <l0> 3 | <l1> 9 | <l2> 20 | <l3> 15 | <l4> 7 ",height=0.5, width=3]; inorder[shape = record, label = " <l0> 9 | <l1> 3 | <l2> 15 | <l3> 20 | <l4> 7 ",height=0.5, width=3]; pre[label = "preorder"]; inr[label = "inorder"]; idx [fontcolor="orange"] p_left_h [label="pre_low + 1", fontcolor="red" ] p_left_t [label="pre_low + (idx - in_low)", fontcolor="red" ] p_right_h [label="pre_high - (in_high - idx - 1)", fontcolor="blue"] p_right_t [label="pre_high", fontcolor="blue"] i_left_h [label="in_low", fontcolor="red" ] i_left_t [label="idx - 1", fontcolor="red" ] i_right_h [label="idx + 1", fontcolor="blue"] i_right_t [label="in_high", fontcolor="blue"] pre -> preorder:l0; inr -> inorder:l0; idx -> inorder:l1; preorder:l1 -> p_left_h [dir=back] preorder:l1 -> p_left_t [dir=back] p_right_h -> preorder:l2 p_right_t -> preorder:l4 inorder:l0 -> i_left_h [dir=back] inorder:l0 -> i_left_t [dir=back] i_right_h -> inorder:l2 i_right_t -> inorder:l4 } ``` ```c static inline void node_add(int val, int idx, int size, struct hlist_head *heads) { struct order_node *on = malloc(sizeof(*on)); on->val = val; on->idx = idx; int hash = (val < 0 ? -val : val) % size; hlist_add_head(&on->node, DDDD); } ``` 此函式目的是將新節點加入對應 hash 的串列中, 因此 `DDDD` 應為 `&head[hash]` **在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討** 在 [/kernel/cgroup/cgroup.c](https://elixir.bootlin.com/linux/latest/source/kernel/cgroup/cgroup.c#L241) 中 ```c /* walk live descendants in pre order */ #define cgroup_for_each_live_descendant_pre(dsct, d_css, cgrp) \ css_for_each_descendant_pre((d_css), cgroup_css((cgrp), NULL)) \ if (({ lockdep_assert_held(&cgroup_mutex); \ (dsct) = (d_css)->cgroup; \ cgroup_is_dead(dsct); })) \ ; \ else ``` ### 測驗二 - `LRUCache` : `LRU` 緩存機制的主體結構。包含以下元素: - `capacity`: cache 的最大容量 - `count` : cache 當前紀錄的資料筆數 - `dhead` : 儲存 LRU 快取中的節點,並按最近使用的順序排列。 - `hhead[]` : 一個型態為 `hlist_head` 的陣列, 儲存每個 hash 對應的串列 head 節點 - `LRUNode` : 是緩存中每個項目的結構,包含以下資料: - `key` : 鍵值 - `value`: 數值 - `node` : 用於將此 cache 項目連接到 `hash` 的結構 - `link` : 用於將此 cache 項目連接到雙向鏈結的結構 ```graphviz digraph node_t { node [shape= "record"]; rankdir= "LR"; LRUCache [label= "{LRUCache | {capacity | count | <d>dhead | hhead[] }}"]; LRUNode [label= "{LRUNode | {key | value | node | link }}"]; } ``` ```c void hlist_del(struct hlist_node *n) { struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; //藍箭頭 if (next) EEEE = pprev; //紅箭頭 } ``` 此函式目的為刪除節點, 假設要刪除 `node2` ,要將 `*pprev` 也就是 `node1->next` 的值改為 `next (node3)`, 並且因為 `next` 存在,也要將 `next->pprev` 改為 `pprev (node1)`。 ```graphviz digraph node_t { node [shape= "record"]; rankdir= "LR"; splines = false list_head [label= "<h>list_head | <f>first"] node1 [label= "<self>node1 | {<p>pprev | <n>next}"] node2 [label= "<self>node2 | {<p>pprev | <n>next}"] node3 [label= "<self>node3 | {<p>pprev | <n>next}"] NULL [shape= plaintext] {rank = "min" list_head} list_head -> node1 -> node3->NULL[ weight = 100, style=invis ] list_head:f->node1:self node1:n->node3:self [color=blue] node1:p->list_head:f node3:n->NULL node3:p->node1:n [color=red] } ``` ```c void lRUCacheFree(LRUCache *obj) { struct list_head *pos, *n; list_for_each_safe (pos, n, &obj->dhead) { LRUNode *cache = list_entry(pos, LRUNode, FFFF); list_del(GGGG); free(cache); } free(obj); } ``` 此段程式碼目的為釋放 LRU 快取所占用的記憶體, `pos` 的型態為 `list_head`, 由此可知其為 `LRUNode` 中的 `link`, `FFFF = link`。因為要將 `list` 刪除, 故 `GGGG` 應為 `&cache->link`。 ```c int lRUCacheGet(LRUCache *obj, int key) { int hash = key % obj->capacity; struct hlist_node *pos; hlist_for_each (pos, &obj->hhead[hash]) { LRUNode *cache = list_entry(pos, LRUNode, HHHH); if (cache->key == key) { list_move(IIII, &obj->dhead); return cache->value; } } return -1; } ``` 此函式從快取中尋找給定 `key` 並獲取其對應的值 ,如果 `key` 存在於快取內則返回該值,並將該節點移動到鏈結串列的頭部 (表示最近使用);如果不存在則返回 -1。 `pos` 的型態為 `hlist_node`, 因此可推論其為`LRUNode` 中的 `node` , 故 `HHHH` 為 `node`。而 `IIII` 是移動至另一串列的前端, 因此為 `&cache->list`。 ```c void lRUCachePut(LRUCache *obj, int key, int value) { LRUNode *cache = NULL; int hash = key % obj->capacity; struct hlist_node *pos; hlist_for_each (pos, &obj->hhead[hash]) { LRUNode *c = list_entry(pos, LRUNode, JJJJ); if (c->key == key) { list_move(KKKK, &obj->dhead); cache = c; } } if (!cache) { if (obj->count == obj->capacity) { cache = list_last_entry(&obj->dhead, LRUNode, link); list_move(&cache->link, &obj->dhead); hlist_del(&cache->node); hlist_add_head(&cache->node, &obj->hhead[hash]); } else { cache = malloc(sizeof(LRUNode)); hlist_add_head(&cache->node, &obj->hhead[hash]); list_add(&cache->link, &obj->dhead); obj->count++; } cache->key = key; } cache->value = value; } ``` 此函式將給定的鍵-值對(key–value pair)加入快取中,有以下三種情況 - 如果 key 已存在,則更新對應的值 - key 不存在會判斷容量是否已滿 - 若已滿則刪除鏈結串列尾部的節點(最久未使用),然後將新的鍵-值對插入到鏈結串列的前端。 - 若未滿則配置新空間將節點加入串列前端以及加入 hash table 中,並將 count++ 。 因此不管 key 是新的還是已存在,對應的節點都會被移動到鏈結串列的前端。 `KKKK` 同樣為移動至另一串列的前端,故為 `&c->list` 。 :::warning TODO 在 Linux 核心找出 LRU 相關程式碼並探討 ::: ### 測驗三 `__ffs` ```c static inline unsigned long __ffs(unsigned long word) { int num = 0; #if BITS_PER_LONG == 64 if ((word & AAAA) == 0) { num += 32; word >>= 32; } #endif if ((word & 0xffff) == 0) { num += 16; word >>= 16; } if ((word & 0xff) == 0) { num += 8; word >>= 8; } if ((word & 0xf) == 0) { num += 4; word >>= 4; } if ((word & 0x3) == 0) { num += 2; word >>= 2; } if ((word & 0x1) == 0) num += 1; return num; } ``` `__ffs()` 是用來查詢一個 `word` 中最低位 1 的所在位置, 若 (word & **0xffffffff**) == 0 即可知道較低的32位元內沒有任一位元為1, 因此將 `word` 右移 32 位元再進行檢查。 `__clear_bit` ```c static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr) { unsigned long mask = BIT_MASK(nr); unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr); *p &= BBBB; } ``` 此段程式碼是透過 `BIT_MASK(nr)` 產生出只有第 `nr` 位為 1 ,其他位皆為 0 的遮罩,並利用此遮罩將該 `addr` 的第 `nr` 位清除。 故 `BBBB` 應為 `~mask` ,利用 ~ 運算將 mask 反轉成只有 `nr` 位為 0 ,即可達到清除該位元的效果。 `fns` ```c static inline unsigned long fns(unsigned long word, unsigned int n) { while (word) { unsigned int bit = __ffs(word); if (n-- == 0) return bit; __clear_bit(bit, &word); } return BITS_PER_LONG; } ``` 此段程式碼目的為找到第 n 個被設為 1 的位置,它會不斷地呼叫 `__ffs` 找到最低被設為 1 的位元,若還不是第 n 個就會使用 `__clear_bit` 將目前的位元清除,再繼續找下一個被設為 1 的位元。 `FIND_NTH_BIT` ```c static inline unsigned long find_nth_bit(const unsigned long *addr, unsigned long size, unsigned long n) { if (n >= size) return size; if (small_const_nbits(size)) { unsigned long val = *addr & GENMASK(size - 1, 0); return val ? fns(val, n) : size; } return FIND_NTH_BIT(addr[idx], size, n); } ``` 運作原理: 先利用 `small_const_nbits` 比較要找的位數是否超過限制的 `size` , 並利用 `GENMASK(h, l)` 將 h 到 l 間的位元變為 1和 `addr` 做 & 運算 ,若 `val` 有值代表 `addr` h 到 l 間有位元被設為1,因此再利用 `fns` 找到為 第 n 個被設為 1 的位元並回傳其位置。若不符合 `small_const_nbits` 就利用 `FIND_NTH_BIT` 來處理。 `FIND_NTH_BIT` ```c #define FIND_NTH_BIT(FETCH, size, num) \ ({ \ unsigned long sz = (size), nr = (num), idx, w, tmp; \ \ for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \ if (idx * BITS_PER_LONG + nr >= sz) \ goto out; \ \ tmp = (FETCH); \ w = hweight_long(tmp); \ if (w > nr) \ goto found; \ \ nr -= w; \ } \ \ if (sz CCCC BITS_PER_LONG) \ tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \ found: \ sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \ out: \ sz; \ }) ``` `FIND_NTH_BIT` 能夠搜尋超出單個 `BITS_PER_LONG` 長度的範圍。如果要查找的位元不在目前處理的字節中,能透過迴圈繼續在下一個 `BITS_PER_LONG` 長度的區塊中搜尋,直到找到該位為止。 因為 `size` 可能無法被 `BITS_PER_LONG` 整除,因此搜尋到最後一輪時應避免找到超出 `size` 範圍的字元,因此使用取餘數的方式找到最後一個字節所需要搜尋的字元數量,故 `CCCC` 應為 `%`。