# 2024q1 Homework2 (quiz1+2) contributed by < [`56han`](https://github.com/56han/lab0-c) > ## 第 1 週測驗題 [2024q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1) ### 測驗 `1` #### 1. 補完程式碼,使其得以符合預期運作 取得 `list` 尾端的 `node_t`,先確認是否 `left` 且 `left->next` 不為 `NULL`,若不為 `NULL` 則指標往下一個 `node_t` 指,即 `left->next`,最後跳出 while 迴圈時剛好會指到 tail。 ```diff node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) - left = &(AAAA); + left = &((*left)->next); return *left; } ``` 當 `left` 不為 `NULL` 時,持續往下一個 `node_t` 指,走完整個鏈結串列時,剛好可得鏈結串列長度。 ```diff int list_length(node_t **left) { int n = 0; while (*left) { ++n; - left = &(BBBB); + left = &((*left)->next); } return n; } ``` `while` 迴圈持續往下一個指,將原本的鏈結串列分成 `> pivot` 和 `< pivot`,因此 `CCCC` 可填入 `n->next` 或 `p->next`。 接著判斷是否比 `pivot` 值還大,若有加入 `right` 鏈結串列,反之加入 `left` 鏈結串列。 ```diff while (p) { node_t *n = p; - p = CCCC; + p = p->next; list_add(n->value > value ? &right : &left, n); } ``` 使用 `begin[i]` 和 `end[i]` 紀錄 `left` 串列的頭、尾; `begin[i+1]` 和 `end[i+1]` 紀錄 `pivot`; `begin[i+2]` 和 `end[i+2]` 紀錄 `right` 串列的頭、尾。 這裡可以使用 `list_tail` 子函式,取得**尾端的節點**。 ```diff 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); ``` #### 2. 運作原理 使用鏈結串列實作 `quick sort`,實際上的鏈結串列如下,引用 [YangYeh-PD](https://hackmd.io/-iW9qu9MQYuaky39rwBi3A?view) 的畫法。 ```c typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t; ``` ```graphviz digraph "queue" { rankdir = "LR" subgraph "cluster node1" { node1_1[shape = record, label = "next"]; node1_2[shape = record, label = "value"]; node1_3[shape = record, label = "{<left> left |<right> right}"]; } subgraph "cluster node2" { node2_1[shape = record, label = "next"]; node2_2[shape = record, label = "value"]; node2_3[shape = record, label = "{<left> left |<right> right}"]; } subgraph "cluster node3" { node3_1[shape = record, label = "next"]; node3_2[shape = record, label = "value"]; node3_3[shape = record, label = "{<left> left |<right> right}"]; } node1_1 -> node2_1; node2_1 -> node3_1; } ``` 觀察程式碼可知,並沒有使用到 `left` 和 `right` ,因此可設計鍵結串列如下。 ```graphviz digraph structs { node[shape=plaintext]; pivot [fontcolor="red"]; L [fontcolor="blue"]; R [fontcolor="blue"]; p [fontcolor="black"]; node[shape=box]; struct0 [label= "4"]; struct1 [label= "5"]; struct2 [label= "3"]; struct3 [label= "2"]; struct4 [label= "1"]; { rank="same"; struct0 -> struct1 struct1 -> struct2 struct2 -> struct3 struct3 -> struct4 } pivot -> struct0; L -> struct0; R -> struct4; p -> struct1; } ``` 在上面的例子可以發現,當 4 是 pivot 時, 大於 pivot : 5 小於 pivot : 3, 2, 1 ```c while (p) { node_t *n = p; p = p->next; list_add(n->value > value ? &right : &left, n); } ``` 經過以上程式碼後,就會變成下圖。 ```graphviz digraph structs{ node[shape=plaintext]; left [fontcolor="blue"]; pivot [fontcolor="red"]; right [fontcolor="blue"]; node[shape=box] struct0 [label= "4"]; struct1 [label= "5"]; struct2 [label= "3"]; struct3 [label= "2"]; struct4 [label= "1"]; pivot -> struct0; left -> struct2; struct2 -> struct3; struct3 -> struct4; right -> struct1; } ``` 觀察整個 `quick_sort()` 函式,發現: 1. 都是先檢查 `right` 串列是否還有需要排序(因為`i += 2;`),再去檢查 `left` 。 >以上面例子來說, `right` 串列只有一個節點。因此將結果放入 `result` 串列中, `i--` 再去檢查 `pivot` 和 `left` 串列。 2. `begin` 和 `end` 使用了 2 倍的 `list_length` 空間,去記錄 `left` 和 `right` 串列中的頭尾。 3. `pivot` 都是取鏈結串列的第一個節點。 #### 3. 改進方案 * `begin` 和 `end` 是否真的需要 2 倍的 `list_length` 空間? 實際執行 `count` = `10` ~ `100000` ,觀察 `begin` 和 `end` 需要使用多大的空間。 ```c int main(int argc, char **argv) { node_t *list = NULL; for (int i = 10; i < 1000000; i *= 10) { printf("size : %d", i); size_t count = i; int *test_arr = malloc(sizeof(int) * count); for (int i = 0; i < count; i++) test_arr[i] = i; shuffle(test_arr, count); while (count--) list = list_construct(list, test_arr[count]); quick_sort(&list); assert(list_is_ordered(list)); list_free(&list); free(test_arr); } return 0; } ``` 在 `begin[i+2]` 和 `end[i+2]` 後面加入 ```c if(i+2 > max_deep){ max_deep = i+2; } ``` 最後發現 `begin` 和 `end` 只需要 10x( 計算 size 是10的幾次方 ) 的空間,就足夠了。 ``` size : 10, deepest level : 4 size : 100, deepest level : 14 size : 1000, deepest level : 26 size : 10000, deepest level : 38 size : 100000, deepest level : 48 ``` * 每次都取第一個節點為 `pivot` ,當遇到已排序成 ascending /descending order 的鏈結串列時,就會成為 worst case,如何以別的方式挑選 `pivot` 以避免 worst case 的發生? 1. `rand()` 取一數,並和 `head` 做交換,取新的 `head` 當作 `pivot` 2. Median-of-three #### 使用 Linux 核心風格的 List API 改寫上述程式碼,並針對鏈結串列 >commit [9fdec7b](https://github.com/56han/Optimized_QuickSort/commit/9fdec7b8c333957883e06861eab276593f371d9b) :::warning TODO:提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。 ::: ### 測驗 `2` #### 1. 補完程式碼,使其得以符合預期運作 ```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); } ``` #### 2. 運作原理 `timsort` 函式: * 一開始會先將雙向鏈結串列轉為**單向串列**。接著進入主要的迴圈,不斷呼叫 `find_run()` 找出下一個 run,並根據 run 的長度和目前的堆疊狀態,呼叫 `merge_collapse()` 觸發必要的合併。 * 當輸入串列掃描完畢後,函式會呼叫 `merge_force_collapse()` 強制合併所有剩餘的 run。 * 最後,根據堆疊的大小決定要呼叫 `build_prev_link()` 還是 `merge_final()` 來建立最終排序完成的雙向鏈結串列。 `merge` 函式:**合併兩個已經排序好的鏈結串列 `a` 和 `b`。** >它會比較兩個串列的元素,較小的元素會被放在合併後的串列前面。如果兩個元素相等,為了保持穩定性(stability),會優先選擇串列 `a` 的元素。 `build_prev_link` 函式:**重建雙向鏈結串列的 `prev` 指標。** >從 `tail` 開始,調整每個節點的 `prev`,讓它指向前一個節點。最後再將頭尾兩端的 `prev` 和 `next` 指標連接起來,構成一個環狀的雙向鏈結串列。 `merge_final` 函式:**執行最後一次的合併,並建立完整的雙向鏈結串列。** >函式會不斷比較 `a` 和 `b` 的元素,較小的會被連接到 `tail` 之後,調整對應的 `prev` 和 `next` 指標。一旦 `a` 或 `b` 的元素取完, 迴圈就會結束。最後,函式會呼叫 `build_prev_link()`,將 `b` 剩餘的元素全部接到 `tail` 之後,完成雙向鏈結串列的建立。 `find_run` 函式:**在未排序的鏈結串列中找出一段遞增或遞減的子串列(run)。** >它會從 `list` 開始,比較相鄰元素的大小,決定目前的 run 是遞增還是遞減。對於遞減的 run, 函式會同時進行反轉,讓 run 變成遞增。函式會持續比較元素,直到不再滿足遞增/遞減的條件為止。 >最後回傳一個 `pair`,其中 `head` 指向 run 的起點, `next` 指向 run 結束後的下一個節點。函式還會在 run 的第二個節點的 `prev` 欄位中暫存 run 的長度。 `merge_at` 函式:**合併指定位置 at 後面的兩個鏈結串列。** >首先計算要合併的兩個段的長度總和,然後呼叫 `merge()` 將它們合併。最後,它更新合併後的鏈結串列的長度訊息,將前一個節點的 next 指針指向合併後的鏈結串列,並減少堆疊的大小。 `merge_force_collapse` 函式:**在堆疊的 size >= 3 時,持續合併鏈結串列。** >它比較 tp 前面兩個鏈結串列的長度,合併較小的那一個。這個過程會一直持續,直到堆疊的 size < 3。 `merge_collapse` 函式:**盡可能地將相鄰的較短鏈結串列合併,從而減少鏈結串列中節點的數量,提高逐一走訪的效率。** 確保堆疊上的 run 長度保持平衡。該函式主要檢查堆疊頂端的 3 個 run 是否滿足以下原則: 1. A 的長度 > B 的長度 + C 的長度。 2. B 的長度 > C 的長度。 >主要是根據堆疊中當前的鏈結串列的長度情況,來決定如何進行合併操作。它會根據以下條件來判斷: >**條件一:** >stack 的 size >= 3 >且 `tp->prev->prev` 的長度 <= `tp->prev` 的長度 + `tp` 的長度。 >**條件二:** >stack 的 size >= 4 >且 `tp->prev->prev->prev` 的長度 <= `tp->prev->prev` 的長度 + `tp->prev` 的長度。 >**如果滿足條件一或條件二:** 如果 `tp->prev->prev` 較短,則呼叫 `merge_at(priv, cmp, tp->prev)` 合併 `tp->prev` 和 `tp->prev->prev`。 如果 `tp` 較短,則呼叫 `merge_at(priv, cmp, tp)` 合併 `tp` 和 `tp->prev`。 **如果上述兩個條件都不滿足:** 如果 `tp->prev` 較短,則呼叫 `merge_at(priv, cmp, tp)` 合併 `tp` 和 `tp->prev`。 如果 `tp` 較短,終止合併過程,返回 `tp`。 #### 3. 改進方法 * 原程式碼並無實作 Galloping mode :::warning TODO:實作 Galloping mode ::: :::warning TODO:將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。 ::: ## 第 2 週測驗題 [2024q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2) ### 測驗 `1` #### 1. 補完程式碼,使其得以符合預期運作 將新節點插入到雙向鏈結串列的 head 時,新節點的 next 指針應該指向的位置。因為我們是將新節點插入到 head,所以它的 next 應該指向原來的 head 節點。 ```diff 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->next = h->first; n->pprev = &h->first; h->first = n; } ``` `hlist_for_each` 用於逐一走訪 hash table 中特定鏈結串列的 head 指標。`heads` 是一個 hash table 的 heads array,每個元素對應一個鏈結串列,而 `hash` 是根據給定值計算出的 hash values,用於確定逐一走訪哪個鏈結串列。 ```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) { - struct order_node *on = CCCC(p, struct order_node, node); + hlist_for_each (p, &heads[hash]) { + struct order_node *on = list_entry(p, struct order_node, node); if (num == on->val) return on->idx; } return -1; } ``` 獲取 hash table 中特定鏈結串列的 head 指標,用於將新節點插入到對應鏈結串列的 head。 ```diff 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); + hlist_add_head(&on->node, &heads[hash]); } ``` #### 2. 運作原理 此程式碼的用意是透過構建一個 hash table,有效地儲存和查詢 **inorder** 序列中的節點,從而加速以 preorder 和 inorder 序列重建二元樹的過程。 ```c struct hlist_node { struct hlist_node *next, **pprev; }; ``` `hlist_node` 以圖像表示。 ```graphviz digraph G{ rankdir = LR; splines = false; node[shape = "record"] node_1[label = "<m>hlist_node | {<p>pprev | <n>next}", group = list]; } ``` 很多個 `hlist_node` 連接,則會形成下圖。 ```graphviz digraph G { rankdir = LR; splines = false; node[shape = "record"] list_head[label = "<m>list_head | <n>first"] node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list]; node_2[label = "<m>hlist_node_2 | {<p>pprev | <n>next}", group = list]; node_3[label = "<m>hlist_node_3 | {<p>pprev | <n>next}", group = list]; NULL[shape = plaintext, label = "NULL", group = list] {rank = "min" list_head} list_head -> node_1 -> node_2 -> node_3[ weight = 10, style = invis ] list_head:n -> node_1:m; node_1:p -> list_head:n; node_1:n -> node_2:m; node_2:p -> node_1:n; node_2:n -> node_3:m; node_3:p -> node_2:n; node_3:n -> NULL; } ``` ```c struct hlist_head { struct hlist_node *first; }; ``` `hlist_head` 以圖像表示。 ```graphviz digraph G{ rankdir = LR; splines = false; node[shape = "record"] node_1[label = "<m>hlist_head | <f>first", group = list]; } ``` 透過 `node_add` 函式:將 node 一個一個加入 hash table 中。 示意圖如下:引用 [ShchangAnderson](https://hackmd.io/OkL9VMRxS5-1dRUgiwiGgQ?view) 同學的圖。 ```graphviz digraph so { rankdir=TB ; Array [ shape = record, label = "{ 0 | 1 | 2 | 3 |4 }"] ; in_heads [shape= "none"] in_heads -> Array [style=invis] shape = "none" inorder [label="Inorder:";shape="none"] {rank=same inorder 9 3 15 20 7} 9 [shape = "none"] 3 [shape = "none"] 15 [shape = "none"] 20 [shape = "none"] 7 [shape = "none"] inorder -> 9 -> 3 -> 15 -> 20 -> 7 [style=invis] color = white; } ``` ----------------- ```graphviz digraph so { rankdir=LR ; {rank=same inh4 inh3 inh2 inh1 inh } inh [label="in_heads[0]"; shape="none"] inh1 [label="in_heads[1]"; shape="none"] inh2 [label="in_heads[2]"; shape="none"] inh3 [label="in_heads[3]"; shape="none"] inh4 [label="in_heads[4]"; shape="none"] 15 [label="{val : 15 | Idx : 2}", shape="record"]; 20 [label="{val : 20 | Idx : 3}", shape="record"]; 7 [label="{val : 7 | Idx : 4}", shape="record"]; 9 [label="{val : 9 | Idx : 0}", shape="record"]; 3 [label="{val : 3 | Idx : 1}", shape="record"]; inh -> 20 -> 15 inh2 -> 7 inh3 -> 3 inh4 -> 9 } ``` 建立完成 hash table 之後,呼叫 `dfs` 函式以深度優先搜尋往下尋找 root ,利用 `find` 函式找到 root 並且回傳 `idx` 值,即為 root 的位置。以 root 又可以切成左、右子樹,遞迴呼叫 `dfs` 函式。 引用 [ShchangAnderson](https://hackmd.io/OkL9VMRxS5-1dRUgiwiGgQ?view) 同學的圖。 ```graphviz digraph so { rankdir=TB ; shape = "none" inorder [label="Inorder:";shape="none"] {rank=same inorder 9 3 15 20 7} 9 [shape = "none"] 3 [color=blue;shape = ""] 15 [shape = "none"] 20 [shape = "none"] 7 [shape = "none"] inorder -> 9 -> 3 -> 15 -> 20 -> 7 [style=invis] 91 [label="9";shape = "none"] 31 [color=red;label="3";shape = "";] 151 [label="15";shape = "none"] 201 [label="20";shape = "none"] 71 [label="7";shape = "none"] preorder [label="Preorder:";shape="none"] {rank=same preorder 31 91 201 151 71} preorder -> 31 -> 91 -> 201 -> 151 -> 71[style=invis] 31 -> 9 [style=invis] } ``` ----------------- ```graphviz digraph so { rankdir=TB ; shape = "none" inorder [label="Inorder:";shape="none"] {rank=same inorder 9 3 } 9 [color=red;shape = ""] 3 [color=red;label="15 20 7"shape = ""] inorder 31 [color=blue;label="3";shape = "";] 31 -> 9 31 -> 3 } ``` ------ :::warning TODO: 1. 指出其中可改進之處並實作 2. 在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討 ::: ### 測驗 `2` #### 1. 補完程式碼,使其得以符合預期運作 目的是在從 hash table 中刪除一個節點時,更新下一個節點的 pprev 指標,使其指向上一個節點的地址。 閱讀 [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable#Linux-%E6%A0%B8%E5%BF%83%E7%9A%84-hash-table-%E5%AF%A6%E4%BD%9C) 學到為何是 `*next` 和 `**pprev`。 ```diff void hlist_del(struct hlist_node *n) { struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) - EEEE = pprev; + next->pprev = pprev; } ``` 釋放 LRU 快取實例所占用的記憶體。 ```diff 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); + LRUNode *cache = list_entry(pos, LRUNode, link); + list_del(&cache->link); free(cache); } free(obj); } ``` 從快取中獲取給定 key 對應的值,如果存在則返回該值,並將該節點移動到鏈結串列的頭部 (表示最近使用);如果不存在則返回 -1。 ```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->link, &obj->dhead); return cache->value; } } return -1; } ``` 將給定的 **鍵-值對** (key–value pair) 插入快取中。 Case 1:如果 **key 已存在**,則更新對應的值 Case 2:如果 key 不存在且**容量已滿**,則刪除鏈結串列尾部的節點(最久未使用),然後插入新的鍵-值對。 Case 3:如果 key 不存在且**容量未滿**,則直接插入新的鍵-值對在鏈結串列的頭部。 **不管 key 是新的還是已存在,對應的節點都會被移動到鏈結串列的頭部。** ```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; } ``` :::info 不理解第 20、21 行,實際上的意思是刪除第一個節點和加入新節點,但為甚麼 `hlist_del` 和 `hlist_add_head` 輸入參數都是 `&cache->node` ? ::: #### 2. 運作原理 利用 `lRUCachePut` 及 `lRUCacheGet` 等函式實作 LRU (Least Recently Used) 快取的資料結構。LRU 快取是一種常用的快取演算法,它會根據資料的最近使用情況,自動釋放最久未使用的資料,以確保快取空間的有效利用。 **LRUCache 結構體** 引用 [vax-r](https://hackmd.io/VNAV9GIkRx6uFQVulRVe_w?view) 同學的圖 * `capacity` :紀錄此 cache 最多能記錄幾筆資料 * `count` :cache 當前紀錄的資料筆數 * `dhead` :用來串接所有資料的雙向鍵結串列 * `hhead` :處理碰撞使用的 `hlist` 結構體陣列 ```c typedef struct { int capacity; int count; struct list_head dhead; struct hlist_head hhead[]; } LRUCache; ``` ```graphviz digraph structs{ node [shape="record"]; rankdir = "LR" subgraph cluster_a { label = "LRUCache"; struct1 [label="capacity"]; struct2 [label="count"]; struct3 [label="dhead"]; struct4 [label="<head>hhead[0]|hhead[1]|...|hhead[capacity - 1]"]; struct3 -> struct3 [label="next"]; struct3 -> struct3 [label="prev"]; } } ``` ```c struct hlist_node { struct hlist_node *next, **pprev; }; ``` `hlist_node` 以圖像表示。 ```graphviz digraph G{ rankdir = LR; splines = false; node[shape = "record"] node_1[label = "<m>hlist_node | {<p>prev | <n>next}", group = list]; } ``` 很多個 `hlist_node` 連接,則會形成下圖。 ```graphviz digraph G { rankdir = LR; splines = false; node[shape = "record"] list_head[label = "<m>list_head | <n>first"] node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list]; node_2[label = "<m>hlist_node_2 | {<p>pprev | <n>next}", group = list]; node_3[label = "<m>hlist_node_3 | {<p>pprev | <n>next}", group = list]; NULL[shape = plaintext, label = "NULL", group = list] {rank = "min" list_head} list_head -> node_1 -> node_2 -> node_3[ weight = 10, style = invis ] list_head:n -> node_1:m; node_1:p -> list_head:n; node_1:n -> node_2:m; node_2:p -> node_1:n; node_2:n -> node_3:m; node_3:p -> node_2:n; node_3:n -> NULL; } ``` #### 3.在 Linux 核心找出 LRU 相關程式碼 在 [Linus Torvalds 的 GitHub](https://github.com/torvalds/linux) 中可見,例如:`linux/mm/list_lru.c`。 以下程式碼主要是在處理 LRU list 時,將一個項目添加到特定節點的 LRU list中。 ```c bool list_lru_add(struct list_lru *lru, struct list_head *item, int nid, struct mem_cgroup *memcg) { struct list_lru_node *nlru = &lru->node[nid]; struct list_lru_one *l; spin_lock(&nlru->lock); if (list_empty(item)) { l = list_lru_from_memcg_idx(lru, nid, memcg_kmem_id(memcg)); list_add_tail(item, &l->list); /* Set shrinker bit if the first element was added */ if (!l->nr_items++) set_shrinker_bit(memcg, nid, lru_shrinker_id(lru)); nlru->nr_items++; spin_unlock(&nlru->lock); return true; } spin_unlock(&nlru->lock); return false; } EXPORT_SYMBOL_GPL(list_lru_add); ``` :::warning TODO:指出其中可改進之處並實作 ::: ### 測驗 `3` #### 1. 補完程式碼,使其得以符合預期運作 目的是找出 `word` 參數中最低位的1所在的位置(從0開始計算)。 ```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; } ``` 計算出 bitmask,然後找到該位所在的字,最後使用 `*p &= ~mask;` 來清除該位。 ```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; } ``` 用於尋找第 `n` 個 bit 為1的索引。 ```diff #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) \ + if (sz % BITS_PER_LONG) \ tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \ found: \ sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \ out: \ sz; \ }) ``` #### 2. 運作原理 此篇程式碼目的是在指定的記憶體空間找出第 N 個設定的位元。 ```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); } ``` :::info 不理解執行 find_nth_bit() 時,甚麼時候會需要 `return FIND_NTH_BIT(addr[idx], size, n);` ::: 用來檢查一個數字 `nbits` 是否 <= `BITS_PER_LONG` ,並且> 0 的常數。 >`__builtin_constant_p(nbits)` 是 GCC 提供的一個內建函數,用於在編譯時檢查 nbits 是否是一個已知的常數。 ```c #define small_const_nbits(nbits) \ (__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG && (nbits) > 0) ``` 用於生成一個 bitmask ,其中從第 `l` 位到第 `h` 位(含)的所有位都是1,其餘的位都是0。 >`(~0UL)`: 這表示一個 unsigned long 的值,其中所有位都是1。`~` 是反運算符,所以取反就是全部 bit 都為1。 >`(1UL << (l))`: 這將數值1(也是一個 unsigned long) 左移 `l` 位。左移操作會在右側補0,因此這個表達式生成了一個數,其中只有第 `l` 位是1,其餘位都是0。 ```c #define GENMASK(h, l) \ (((~0UL) - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h)))) ``` `fns` 函數的目的是找出 `word` 中第 `n` 個設定的位元。 >不斷呼叫 `__ffs` 來找到最低位元的1,如果那不是我們要找的位元,它就使用 `__clear_bit` 來清除這個位,然後繼續搜索下一個設置位。如果 `n` 等於0,意思是我們找到了想要的位元,所以它返回當前的 `bit`。 >如果 `word` 為0(即沒有更多設置位),則返回 `BITS_PER_LONG`,表示沒有找到。 ```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; } ``` 計算出 bitmask,然後找到該位所在的字,最後使用 `*p &= ~mask;` 來清除該位。 ```c static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr) { unsigned long mask = BIT_MASK(nr); //只有第 nr 位是1,其他位都是0 unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr); //計算某個位 nr 在一個 unsigned long 陣列中的哪個字 *p &= ~mask; } ``` #### 3. 在 Linux 核心原始程式碼找出 find_nth_bit 的應用案例 位於 `include/linux/cpumask.h` 的 [`cpumask_nth`](https://elixir.bootlin.com/linux/latest/source/include/linux/cpumask.h) 可以用來取得 cpumask 當中第 n 個 CPU。 >使用 `cpumask_bits(srcp)` 獲取 cpumask 以 bit 表示,然後使用 `small_cpumask_bits` 作為搜尋範圍的大小,最後通過 `cpumask_check(cpu)` 確保傳入的 CPU 編號是有效的。 ```c /** * cpumask_nth - get the Nth cpu in a cpumask * @srcp: the cpumask pointer * @cpu: the Nth cpu to find, starting from 0 * * Return: >= nr_cpu_ids if such cpu doesn't exist. */ static inline unsigned int cpumask_nth(unsigned int cpu, const struct cpumask *srcp) { return find_nth_bit(cpumask_bits(srcp), small_cpumask_bits, cpumask_check(cpu)); } ```