# 2024q1 Homework2 (quiz1+2) contributed by <[yulin0625](https://github.com/yulin0625)> ## 第一週測驗題 [2024q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1#%E6%B8%AC%E9%A9%97-1) ### 測驗 1 #### 補完程式碼、程式碼解析 `list_tail` ```diff node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) - left = &(BBBB); + left = &(left->next); return *left; } ``` 解釋: 這個函式的功能是找到鏈結串列的最後一個節點,並回傳該鏈結串列的最後一個節點的指標。 圖示說明: **Initial:** ```graphviz digraph { node [shape=box] rankdir = LR; left[shape=plaintext,fontcolor=red]; N1[label=A] Addr1[label="&A", shape=plaintext]; left_next[label="(*left)->next",shape=plaintext,fontcolor=blue]; N2[label=B]; Addr2[label="&B", shape=plaintext]; N3[label=C]; Addr3[label="&C", shape=plaintext]; NULL1[label="NULL", shape=plaintext]; {rank=same; left->Addr1->N1} {rank=same; left_next->Addr2->N2} {rank=same; Addr3->N3} N1->N2->N3->NULL1; } ``` **Step1:** ```graphviz digraph { node [shape=box] rankdir = LR; left[shape=plaintext,fontcolor=red]; N1[label=A] Addr1[label="&A", shape=plaintext]; N2[label=B]; Addr2[label="&B", shape=plaintext]; left_next[label="(*left)->next",shape=plaintext,fontcolor=blue]; N3[label=C]; Addr3[label="&C", shape=plaintext]; NULL1[label="NULL", shape=plaintext]; {rank=same; Addr1->N1} {rank=same; left->Addr2->N2} {rank=same; left_next->Addr3->N3} N1->N2->N3->NULL1; } ``` **Step2:** ```graphviz digraph { node [shape=box] rankdir = LR; left[shape=plaintext,fontcolor=red]; N1[label=A] Addr1[label="&A", shape=plaintext]; N2[label=B]; Addr2[label="&B", shape=plaintext]; N3[label=C]; Addr3[label="&C", shape=plaintext]; NULL1[label="NULL", shape=plaintext]; {rank=same; Addr1->N1} {rank=same; Addr2->N2} {rank=same; left->Addr3->N3} N1->N2->N3->NULL1; } ``` `list_length` ```diff int list_length(node_t **left) { int n = 0; while (*left) { ++n; - left = &(BBBB); + left = &((*left)->next); } return n; } ``` 解釋: 原理與 `list_tail` 相同,當 `left` 每間接指到一個節點,計數器 `n` 就會增加一,函釋回傳值為該鏈結串列的長度。 `quick_sort` ```diff 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; + p = p->next; list_add(n->value > value ? &right : &left, n); begin[i] = left; - end[i] = DDDD; + end[i] = &list_tail(left); begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; - end[i + 2] = EEEE; + end[i + 2] = &list_tail(right); left = right = NULL; i += 2; } else { if (L) list_add(&result, L); i--; } } *list = result; } ``` 解釋: 此函式是運用 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/) 所實作非遞迴 (non-recursive; iterative) 的快速排序法程式碼。 排序過程: 1. 迴圈 while (i >= 0) 用於走訪和排序鏈結串列的不同部分 2. 在迴圈內,選擇串列的第一個節點作為 pivot 3. 走訪 pivot 後的所有節點,並與 pivot 比較值的大小,若較小則將該節點加入 left 串列,否則加入 right 串列 4. 更新 begin 和 end 堆疊 5. 若達到一個只有一個節點的子列表,則將該節點添加到 result 串列中 圖示說明: - 選擇第一個節點(節點4)作為 pivot ```graphviz digraph { node [shape=box] rankdir = LR; pivot[shape=plaintext,fontcolor=red]; N1[label=4]; N2[label=1]; N3[label=3]; N4[label=5]; N5[label=2]; N6[label=7]; NULL1[label="NULL", shape=plaintext]; {rank=same; pivot->N1} N1->N2->N3->N4->N5->N6->NULL1; } ``` - 走訪pivot 後的所有節點,根據它們的值相對於 pivot 的大小,將它們分到 left 或 right 子串列 此時的 left, pivot, right: ```graphviz digraph { rankdir = LR; node[shape=plaintext]; left[fontcolor=blue]; pivot[fontcolor=red]; right[fontcolor=blue]; node [shape=box] L1[label="2"]; L2[label="3"]; L3[label="1"]; P1[label="4"]; R1[label="7"]; R2[label="5"]; left->L1->L2->L3 pivot->P1 right->R1->R2 } ``` - 更新 begin、end 堆疊,並將 `i = i + 2` - 將begin[i] 設為 left - 將begin[i + 1] 設為 pivot - 將begin[i + 2] 設為 right ```graphviz digraph { rankdir = TB; node[shape=plaintext]; begin0[label="begin[0]"]; begin1[label="begin[1]"]; begin2[label="begin[2]"]; end0[label="end[0]"]; end1[label="end[1]"]; end2[label="end[2]"]; left[fontcolor=blue]; pivot[fontcolor=red]; right[fontcolor=blue]; node [shape=box]; L1[label="2"]; L2[label="3"]; L3[label="1"]; P1[label="4"]; R1[label="7"]; R2[label="5"]; begin0->L1 {rank=same;left->L1->L2->L3} end0->L3 begin1->P1 {rank=same;pivot->P1} end1->P1 begin2->R1 {rank=same;right->R1->R2} end2->R2 } ``` - 此時 i=2,演算法會對 begin[2] 堆疊所存的 right 串列進行走訪,並選擇節點7,作為 pivot ```graphviz digraph { rankdir = LR; node[shape=plaintext] pivot[fontcolor=red]; node [shape=box] N1[label=7]; N2[label=5]; pivot->N1 N1->N2; } ``` - 此時的 begin、end 堆疊 ```graphviz digraph { rankdir = TB; node[shape=plaintext]; begin0[label="begin[0]"]; begin1[label="begin[1]"]; begin2[label="begin[2]"]; end0[label="end[0]"]; end1[label="end[1]"]; end2[label="end[2]"]; left[fontcolor=blue]; pivot[fontcolor=red]; right[fontcolor=blue]; node [shape=box]; L1[label="5"]; P1[label="7"]; R1[label="NULL"]; begin0->L1 {rank=same;left->L1} end0->L1 begin1->P1 {rank=same;pivot->P1} end1->P1 begin2->R1 {rank=same;right->R1} end2->R1 } ``` - 當 begin 與 end 相等(left == right 時),將節點加入 result 串列,並將 `i--` ```graphviz digraph { rankdir = LR; node[shape=plaintext] result; NULL; node [shape=box] N1[label=5]; N2[label=7]; result->N1->N2->NULL; } ``` - 重複直到所有節點排序完成 **可改進之處:** 目前演算法每次都取第一個節點為 pivot ,若遇到已完成排序的鏈結串列時,就會成為 worst case,時間複雜度為O(n²) 解決方法: 1. Middle-of-Three: 令 middle = (left + right) /2,比較 left、middle 與 right 這三筆資料,排出中間值,再將中間值與 left 做交換 2. Random Pivot Selection: 用亂數選取的方式,隨機挑一個值作為 pivot,降低發生 Worst Case 的機率 3. [Median-of-Medians](https://www.wikiwand.com/en/Median_of_medians) ### 測驗 2 ```diff static struct list_head *merge(void *priv, list_cmp_func_t cmp, struct list_head *a, struct list_head *b) { struct list_head *head; - struct list_head **tail = AAAA; + struct list_head **tail = &head; for (;;) { /* if equal, take 'a' -- important for sort stability */ if (cmp(priv, a, b) <= 0) { *tail = a; - tail = BBBB; + tail = &a->next; a = a->next; if (!a) { *tail = b; break; } } else { *tail = b; - tail = CCCC; + tail = &b->next; b = b->next; if (!b) { *tail = a; break; } } } return head; } ``` ```diff static void build_prev_link(struct list_head *head, struct list_head *tail, struct list_head *list) { tail->next = list; do { list->prev = tail; tail = list; list = list->next; } while (list); /* The final links to make a circular doubly-linked list */ - DDDD = head; + tail->next = head; - EEEE = tail; + head->prev = tail; } ``` ```diff void timsort(void *priv, struct list_head *head, list_cmp_func_t cmp) { stk_size = 0; struct list_head *list = head->next, *tp = NULL; if (head == head->prev) return; /* Convert to a null-terminated singly-linked list. */ head->prev->next = NULL; do { /* Find next run */ struct pair result = find_run(priv, list, cmp); result.head->prev = tp; tp = result.head; list = result.next; stk_size++; tp = merge_collapse(priv, cmp, tp); } while (list); /* End of input; merge together all the runs. */ tp = merge_force_collapse(priv, cmp, tp); /* The final merge; rebuild prev links */ struct list_head *stk0 = tp, *stk1 = stk0->prev; while (stk1 && stk1->prev) stk0 = stk0->prev, stk1 = stk1->prev; - if (stk_size <= FFFF) { + if (stk_size <= 1) { build_prev_link(head, head, stk0); return; } merge_final(priv, cmp, head, stk1, stk0); } ``` 運作原理: ## 第二週測驗題 [2024q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2) ### 測驗 1 `hlist_add_head`: 此函式的功能為將新的節點加入串列的前端。新節點的 next 應指向原本的第一個節點,所以 `AAAA` 為 `h->first` ```diff static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { if (h->first) h->first->pprev = &n->next; // 1 - n->next = AAAA; + n->next = h->first; // 2 n->pprev = &h->first; // 3 h->first = n; // 4 } ``` ```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 [label=4] node_n:n->node1:self [label=2] node_n:p->list_head:f [label=3] node1:n->node2:self node1:p->node_n:n [label=1] node2:n->NULL node2:p->node1:n } ``` `find` ```diff 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) { + hlist_for_each (p, &head[hash]) { - struct order_node *on = CCCC(p, struct order_node, node); + 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` 概念: 由前序可知哪個節點在上面 (越前面的越上面)、由中序可知哪個節點靠左(越前面的越左邊),於是可得知前序的第一個元素一定是 root 的值,且該元素對應到中序後,左邊的值就在左邊,右邊的值就在右邊。 解釋: 此函式會先取前序的第一個節點,並利用 `find` 尋找該節點的值出現在中序的哪個位置,並將左側的節點當成左子樹的 root 、 左側的節點當成右子樹的 root,繼續遞迴再尋找,直到 inorder 中找不到對應元素。 圖示說明: - 若前序為: [3, 9, 20, 15, 7],中序為: [9, 3, 15, 20, 7] - 先查看前序的第一個節點的值 3 ,在中序的位置 `idx`,此時 `idx = 1` - 左子樹的長度為中序第一個節點和 `idx` 之間的間隔,也就是 `(idx - in_low)` ,因此左子樹的範圍為 `pre_low + 1` 至 `pre_low + (idx - in_low)` - 而右子樹的長度為 `idx` 到中序最後一個節點的間隔,也就是 `in_high - (idx + 1)`,因此右子樹在前序的範圍為 `pre_high - (in_high - idx - 1)` 至 `pre_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", fontsize="20"]; inr[label = "inorder", fontsize="20"]; 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 } ``` ### 測驗 2 LRU(Least Recently Used Cache) 是一種快取的實做方式,概念是儲存最近用過的內容,如果越常被使用,內容會被擺在 List 越前方的位置。如果快取滿了,則會從 List 最末端元素開始移除。 - `LRUCache` : `LRU` 緩存機制的主體結構。包含以下元素: - `capacity`: cache 的最大容量 - `count` : cache 當前紀錄的資料筆數 - `dhead` : 儲存 LRU 快取中的節點,並按最近使用的順序排列。 - `hhead[]` : 儲存每個 hash 對應串列的 head 節點 - `LRUNode` : 是緩存中每個項目的結構,包含以下元素: - `key` : 鍵值 - `value`: 數值 - `node` : 用於將此 cache 項目連接到 `hash` 的結構 - `link` : 用於將此 cache 項目連接到雙向鏈結串列的結構 `lRUCacheGet`: 讀取快取 ```diff 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); + LRUNode *cache = list_entry(pos, LRUNode, node); if (cache->key == key) { - list_move(IIII, &obj->dhead); + list_move(&cache->list, &obj->dhead); return cache->value; } } return -1; } ``` 先將 `key` 丟入 hash function 進行計算得到對應的 hash 值。接著從快取中尋找 `key` 。如果 `key` 存在於快取內則返回該值,並將該節點移動到鏈結串列的頭部 (表示最近使用);如果不存在則返回 -1。 `lRUCachePut`: 寫入快取 ```diff 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); + LRUNode *c = list_entry(pos, LRUNode, node); if (c->key == key) { - list_move(KKKK, &obj->dhead); + list_move(&c->link, &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++` ### 測驗 3 目的: 找出 word 參數中最低位的1所在的位置(從0開始計算) `__ffs` ```diff static inline unsigned long __ffs(unsigned long word) { int num = 0; #if BITS_PER_LONG == 64 - if ((word & AAAA) == 0) { + if ((word & 0xffffffff) == 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` ```diff 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; + *p &= ~mask; } ``` 此段程式碼是透過 `BIT_MASK(nr)` 產生出只有第 `nr` 位為 1 ,其他位皆為 0 的遮罩,並利用 ~ 運算將 mask 反轉成只有 `nr` 位為 0,最後利用反轉後的遮罩與 `addr` 做 bitwise and,將該 `addr` 的第 `nr` 位清除。 `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` 應為 `%`。