# 2022q1 Homework1 (quiz1) contributed by < [`tinyynoob`](https://github.com/tinyynoob) > > [作業要求](https://hackmd.io/@sysprog/B166rc3Jq) ## 測驗 1 本題主要是在闡述 Linux kernel 當中 hash table 的實作,觀察 ```c struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; ``` 可知 table 結構大致如下 ```graphviz digraph { rankdir = LR; node [shape=record]; heads [label="heads[0]|heads[1]|heads[2]|heads[3]"]; label = "struct hlist_head heads[4]"; } ``` 每格都連接著一條 linked list (或 NULL ),以 `heads[2]` 為例 ```graphviz digraph { rankdir = LR; node [shape=record]; subgraph cluster { first; label = "heads[2]"; } a [label="<body>hlist_node a|{<p>pprev|<n>next}"]; b [label="<body>hlist_node b|{<p>pprev|<n>next}"]; c [label="<body>hlist_node c|{<p>pprev|<n>next}"]; node [shape=none]; none1 [label=""]; edge [weight=3 style="invis"]; first -> a -> b -> c; edge [weight=2 style=""]; first -> a:body; a:n -> b:body; b:n -> c:body; c:n -> none1 [arrowhead=dot]; edge [color="blue"]; a:p -> first; b:p -> a:n; c:p -> b:n; } ``` 每個 hlist_node 上的 entry 結構定義為: ```c struct hash_key { int key; void *data; struct hlist_node node; }; ``` 其中不同節點的 key 應不同,即 key 的唯一性需要被保證,否則後續在 **find_key()** 可能會出問題。 ```c typedef struct { int bits; struct hlist_head *ht; } map_t; #define MAP_HASH_SIZE(bits) (1 << bits) ``` table size (圖中的數字 4 ) 是由 `MAP_HASH_SIZE()` macro 來取得,其值為輸入乘以 2 ,並會紀錄於 `map_t` 的 `bits` 成員。 :::warning 為何 `bits` 不使用 `size_t` 型別? 此外,為何會選用 `bits` 做為變數名稱?是否有什麼特殊意涵? > 取名 `bits` 主要是搭配 capacity 的計算,無論是 `size_t` 或 `ssize_t` 都不影響理解。 > :notes: jserv ::: ### map_init() ```c map_t *map_init(int bits) { map_t *map = malloc(sizeof(map_t)); if (!map) return NULL; map->bits = bits; map->ht = malloc(sizeof(struct hlist_head) * MAP_HASH_SIZE(map->bits)); if (map->ht) { for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) (map->ht)[i].first = NULL; } else { free(map); map = NULL; } return map; } ``` 該函式的功能為動態配置出 table 結構,並依序將每個 hlist_head 的 `first` 成員都初始化為 NULL 。 ### find_key() ```c= static struct hash_key *find_key(map_t *map, int key) { struct hlist_head *head = &(map->ht)[hash(key, map->bits)]; for (struct hlist_node *p = head->first; p; p = p->next) { struct hash_key *kn = container_of(p, struct hash_key, node); if (kn->key == key) return kn; } return NULL; } ``` 函式的第 2 行使用 hash function 快速前往資料所在的 hlist ,接著 3 到 7 行走訪該 hlist ,一個一個比對是否為我們要尋找的節點,若找不到則回傳 NULL 。 以上功能正確運作的一個必要條件是 key 的唯一性,否則可能會找到 key 值相同的另一個節點,使得後續取出的 data 不如預期。 這裡頭所使用的 hash function 定義為 ```c #define GOLDEN_RATIO_32 0x61C88647 static inline unsigned int hash(unsigned int val, unsigned int bits) { /* High bits are more random, so use them. */ return (val * GOLDEN_RATIO_32) >> (32 - bits); } ``` 目前不太清楚為何選用 golden ratio 為乘數。 --- 這裡使用了 [Fibonacci hashing](https://en.wikipedia.org/wiki/Hash_function#Fibonacci_hashing) ,根據 wikipedia ,這樣的 hash 方法可以使結果均勻分配在 table space 中。 :::warning 或許可以用程式測看看它的分佈 ::: 至於前面的 macro define 怎麼來呢?首先, ==golden ratio== 在數學上為 $\varphi=(\sqrt{5}+1)/2\approx 1.618033989$ ,我們可以計算出其倒數 $\varphi^{-1}= (\sqrt{5}-1)/2\approx 0.618033989$ 恰好是原本的小數部份。 [**linux/hash.h**](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 的註解中引註了一篇文章 [Linux Kernel Hash Table Behavior: Analysis and Improvements](http://www.citi.umich.edu/techreports/reports/citi-tr-00-1.pdf) ,在其 5.1 節給出 > $(\sqrt{5} - 1 ) / 2 = 0.618033989$ $2654435761 / 4294967296 = 0.618033987$ 這個 4294967296 顯然是 32 位無號數的最大值加 1 ,也就是 $2^{32}$ 。 這代表我們將輸入的 `val` 乘上 $\varphi^{-1}$ 幾乎等同於乘上 2654435761 並右移 32 ,然後我們需要左移 `bits` 位以充分利用 hashtable 的 size 。 然而 **hash.h** 裡定義的 golden_ratio_32 是 0x61C88647 也就是 1640531527 ,並不是文章中的 2654435761 ,由於 0x61C88647 的二進制最高位是 0 ,所以我以為這個差異不是來自有無號轉換。 查詢了[網路上的討論](https://stackoverflow.com/questions/38994306/what-is-the-meaning-of-0x61c88647-constant-in-threadlocal-java),得到答案說這其實還是跟有號數、無號數有關,我整理如下 |signed|unsigned|binary|hex| |:-:|:-:|:-:|:-:| |1640531527|1640531527|01100001110010001000011001000111|0x61C88647| |-1640531527|2654435769|10011110001101110111100110111001|0x9E3779B9| 所以兩種相乘的答案雖然會以截然不同的面貌存在記憶體中,但就二補數的電腦數值系統而言,它們所表達的數「值」是相同,只差了 sign ,也因此並不影響 hash 的 distribution 。 另外雖然 2654435769 並不是文章中給出的 2654435761 ,但是它更接近於我們期望的 0.618033989 : ```shell (gdb) p (double)2654435769/4294967296 $2 = 0.6180339886341244 (gdb) p (double)2654435761/4294967296 $3 = 0.61803398677147925 ``` ### map_get() ```c void *map_get(map_t *map, int key) { struct hash_key *kn = find_key(map, key); return kn ? kn->data : NULL; } ``` 該函式去 `map` 中尋找是否有符合 `key` 值的 entry ,若有便將該 entry 的 `data` 讀出來。 ### map_add() ```c= void map_add(map_t *map, int key, void *data) { struct hash_key *kn = find_key(map, key); if (kn) return; kn = malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; struct hlist_head *h = &map->ht[hash(key, map->bits)]; struct hlist_node *n = &kn->node, *first = h->first; AAA; if (first) first->pprev = &n->next; h->first = n; BBB; } ``` 首先 3 至 7 行直接到 `map` 當中查找,若已存在該 key 則不增添新節點,反之進入新增節點的程序。 ```graphviz digraph { check [label="於 map 中尋找 key"]; add [label="新增節點"]; node [shape=none]; return; check -> add [dir=forward, xlabel="not found"]; check -> return [label="已經有了"]; } ``` 以下舉例說明新增節點的過程: Suppose $\mathrm{hash}(\mathrm{key}) = 1$ ```graphviz digraph { rankdir = LR; node [shape=record]; heads [label="<first>[0]|<second>[1]|[2]|[3]"]; node [color=red]; kn [label="<body>|{<p>pprev|<n>next}"]; node [shape=none]; ht; h; knptr [label="kn"]; ht -> heads:first; h -> heads:second; knptr -> kn; } ``` 假設 `[1]` 原為 ```graphviz digraph { rankdir = LR; node [shape=record]; subgraph cluster { first; label = "[1]"; } a [label="<body>a|{<p>pprev|<n>next}"]; node [shape=none]; none1 [label=""]; edge [weight=3 style="invis"]; first -> a; edge [weight=2 style=""]; first -> a:body; a:p -> first; a:n -> none1 [arrowhead=dot]; } ``` 現在我們希望將 `kn` 所指到的節點放進這條 list ,最便利的方法是直接塞在最前方 ```graphviz digraph { rankdir = LR; node [shape=record]; subgraph cluster { first; label = "[1]"; } a [label="<body>a|{<p>pprev|<n>next}"]; node [color=red]; kn [label="<body>|{<p>pprev|<n>next}"]; node [shape=none]; none1 [label=""]; edge [weight=3 style="invis"]; first -> kn -> a edge [weight=2 style=""]; first -> kn:body; a:p -> kn:n; a:n -> none1 [arrowhead=dot]; edge [color="blue"]; kn:p -> first; kn:n -> a:body } ``` 圖中黑色的連線已由程式碼第 14 及 15 行完成,因此另外兩條藍色的連線即為我們在 `AAA` 和 `BBB` 需要達成的功能。 ### map_deinit() ```c= void map_deinit(map_t *map) { if (!map) return; for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) { struct hlist_head *head = &map->ht[i]; for (struct hlist_node *p = head->first; p;) { struct hash_key *kn = container_of(p, struct hash_key, node); struct hlist_node *n = p; p = p->next; if (!n->pprev) /* unhashed */ goto bail; struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) next->pprev = pprev; n->next = NULL, n->pprev = NULL; bail: free(kn->data); free(kn); } } free(map); } ``` `map_deinit()` 的功能為刪除掉整個 hash table ,其運作流程大致如下圖 ```flow st=>start: 進入 map->ht 陣列 op1=>operation: 前往陣列的下一項 op2=>operation: 刪除首節點 cond1=>condition: list 裡有無內容 cond2=>condition: 已走訪完陣列 e=>end: free(map) st->op1->cond1->op2->cond1->cond2 cond1(yes)->op2 cond1(no)->cond2 cond2(yes)->e cond2(no)->op1 ``` 接著圖解內層迴圈的行為,假設程式在某個時候執行到第 8 行 ```graphviz digraph { rankdir = LR; node [shape=record]; heads [label="<first>[0]|[1]|<now>[2]|[3]"]; a [label="<body>a|{<p>pprev|<n>next}"]; b [label="<body>b|{<p>pprev|<n>next}"]; c [label="<body>c|{<p>pprev|<n>next}"]; node [shape=none]; ht; none1 [label=""]; node [fontcolor=red]; p; h [label="head"]; edge [weight=3 style="invis"]; heads:now -> a -> b ->c; edge [weight=2 style=""]; ht -> heads:first; h -> heads:now; p -> a; heads:now -> a:body; a:n -> b:body; b:n -> c:body; c:n -> none1 [arrowhead=dot]; edge [color=blue]; a:p -> heads:now; b:p -> a:n; c:p -> b:n; } ``` 在程式執行到第 21 行時會變成 ```graphviz digraph { compound = true; subgraph cluster { rankdir = LR; node [shape=record]; a [label="<body>a|{<p>pprev|<n>next}"]; node [shape=none label="" width=.1]; none2; none3; edge [weight=1 arrowhead=dot style=""]; a:p -> none2; a:n -> none3; } rankdir = LR; node [shape=record]; heads [label="<first>[0]|[1]|<now>[2]|[3]"]; b [label="<body>b|{<p>pprev|<n>next}"]; c [label="<body>c|{<p>pprev|<n>next}"]; node [shape=none]; ht; none1 [label=""]; node [fontcolor=red]; p; kn; h [label="head"]; edge [weight=3 style="invis"]; heads:now -> b ->c; edge [weight=2 style=""]; ht -> heads:first; h -> heads:now; p -> b; heads:now -> b:body; b:n -> c:body; c:n -> none1 [arrowhead=dot]; kn -> a [lhead=cluster]; edge [color=blue]; b:p -> heads:now; c:p -> b:n; } ``` :::warning 百思不解什麼情況下會出現第 13 行的 condition ,或許與多執行緒有關? > 看 Linux 核心原始程式碼 `list.h` 對於 `hlist_unhashed` 的註解: > ```cpp > /** > * hlist_unhashed - Has node been removed from list and reinitialized? > * @h: Node to be checked > * > * Not that not all removal functions will leave a node in unhashed > * state. For example, hlist_nulls_del_init_rcu() does leave the > * node in unhashed state, but hlist_nulls_del() does not. > */ > ``` > 然後再翻閱核心文件關於 RCU 對於 reader 的行為 > :notes: jserv ::: ### twoSum() ```c= int *twoSum(int *nums, int numsSize, int target, int *returnSize) { map_t *map = map_init(10); *returnSize = 0; int *ret = malloc(sizeof(int) * 2); if (!ret) goto bail; for (int i = 0; i < numsSize; i++) { int *p = map_get(map, target - nums[i]); if (p) { /* found */ ret[0] = i, ret[1] = *p; *returnSize = 2; break; } p = malloc(sizeof(int)); *p = i; map_add(map, nums[i], p); } bail: map_deinit(map); return ret; } ``` 程式運作邏輯可大致理解如下: ```flow st=>start: 建立 hash table op1=>operation: 前往陣列的下一項 nums[i] cond=>condition: 檢查 hash table 中是否 存在 target-nums[i] op2=>operation: 將 nums[i] 加入 hash table e=>end: 回傳 index st->op1->cond->op2->op1 cond(yes)->e cond(no)->op2 ``` 成功配對出 `target` 值則達成目標,否則將現值加入 hash table 再繼續往下尋找。 此處使用 hashtable 帶來的主要好處是加快搜尋舊資料的速度,避免一一確認帶來的 $O(n)$ 低效。 ### 探討 kernel 中的 hashtable 實作 本題範例與 Linux kernel 中所使用的 hash 結構相同,最大的區別在於 kernel 中有為了++多使用者並行讀寫++的情境設計了另一組 API ,同時在此種情境下的 hashtable 被稱為 *rcu enabled hashtable* 。 ==RCU== 的主要概念是將 **更新** 動作拆分成 **移除** 與 **再生** ,原因在於在並行的情境下,若更新在 reader 使用資料時發生,則可能會導致 reader 讀到更新一半的資料,發生非預期的後果; RCU 機制使用 read-side critical section , reader 只可能讀到完全舊的資料抑或是完全新的資料,如此便不致發生讀取錯誤。 > [What is RCU? – “Read, Copy, Update”](https://www.kernel.org/doc/html/latest/RCU/whatisRCU.html#whatisrcu) ## 測驗 2 ```c= struct ListNode *deleteDuplicates(struct ListNode *head) { if (!head) return NULL; if (COND1) { /* Remove all duplicate numbers */ while (COND2) head = head->next; return deleteDuplicates(head->next); } head->next = deleteDuplicates(head->next); return head; } ``` 在遞迴的實作中,程式先確認 `head` 與 `head->next` 是否 duplicate ,若有則進入 while 迴圈刪除這組 duplicate ,此外就繼續遞迴下去。 例如: ```graphviz digraph{ rankdir = LR; node [shape=record]; a1,a2,a3 [label="a"]; b; node [shape=none]; h [label="head"]; h -> a1 -> a2 -> a3 -> b; } ``` 經過 while 後會變成 ```graphviz digraph{ rankdir = LR; node [shape=record]; a; b; node [shape=none]; h [label="head"]; h -> a -> b; } ``` 由於第 10 行的遞迴參數是 `head->next` ,因此它會對 b 呼叫遞迴,如此一來便移除了所有重複的 a 。 COND1 及 COND2 都是 duplicate 的判斷式 ```c head->next && head->val == head->next->val ``` ### 撰寫非遞迴版本 ```c struct ListNode* deleteDuplicates(struct ListNode* head){ if (!head) return NULL; for (struct ListNode **it = &head; *it;) { if((*it)->next && (*it)->val == (*it)->next->val) { while((*it)->next && (*it)->val == (*it)->next->val) *it = (*it)->next; *it = (*it)->next; } else { it = &(*it)->next; } } return head; } ``` ## 測驗 3 ### LRUCache 這支程式主要是用 linked list 來實現 cache 的運作,並使用 mod 運算來映射對應的 cache 位置。 cache 的本體結構宣告為 ```c typedef struct { int capacity, count; struct list_head dhead, hheads[]; } LRUCache; ``` cache 的 creation 如下 ```c= LRUCache *lRUCacheCreate(int capacity) { LRUCache *obj = malloc(sizeof(*obj) + capacity * sizeof(struct list_head)); obj->count = 0; obj->capacity = capacity; INIT_LIST_HEAD(&obj->dhead); for (int i = 0; i < capacity; i++) INIT_LIST_HEAD(&obj->hheads[i]); return obj; } ``` 大致上就是配置空間和初始化,唯一需要留意的是第 3 行的語法可以依據 `capacity` 的值動態配置 `hheads[]` 的大小。 每個載有資料的節點有結構: ```c typedef struct { int key, value; struct list_head hlink, dlink; } LRUNode; ``` 比較特別的是節點有 `hlink` 與 `dlink` 兩個用於 linked list 的成員,稍後會解釋為何如此。 ### CacheGet() ```c= int lRUCacheGet(LRUCache *obj, int key) { LRUNode *lru; int hash = key % obj->capacity; MMM2 (lru, &obj->hheads[hash], hlink) { if (lru->key == key) { list_move(&lru->dlink, &obj->dhead); return lru->value; } } return -1; } ``` 第 4 行由輸入 `key` 值對應 cache 的位置。 第 5 行開始明顯是個用來尋找 node 的迴圈,觀察參數的數量與型別,可以推測 `MM2` 應為 `list_for_each_entry()` 。 程式在 `obj->hheads[hash]` list 中尋找,找到之後的行為是回傳 `value` 並將 `dlink` 移到 `obj->dhead` 前方,如果找不到則回傳 -1 。我們可以發現 `obj->hheads[hash]` 這條 list 都沒有被改變。 得知 `obj->hheads[]` 結構是 array of struct list_head ,每一個 element 拖著一條 list ,其中的 node 藉由 `hlink` 成員與對應的 list 互動。所有被放到 cache 的資料都會以節點形式存在 `obj->hheads[]` 的某處,且可以用近乎 $O(1)$ 的速度快速被找到。 ```graphviz digraph { rankdir = LR; node [shape=record]; hheads [label="hheads[0]|hheads[1]|hheads[2]|hheads[3]"]; label = "obj->hheads[]"; } ``` For some variable $\text{hash}$, ```graphviz digraph { rankdir = LR; node [shape=record]; h [label="<body>hhead[hash]|{<p>prev|<n>next}"]; a [label="<body>node|{<p>prev|<n>next}"]; node [shape=none]; edge [weight=3 style="invis"]; h -> a; edge [weight=2 style=""]; h:n -> a:body; a:n -> h:body; edge [color=blue]; a:p -> h:body; h:p -> a:body; } ``` 這裡用的都是 **list.h** 裡一般的 `list` ,所以 `prev` 全部是 direct pointer 。 除了 `hheads[]` 以外,我們的 cache 額外 maintain 了一條 list `dhead` 。由於 `obj->dhead` 這條 list 在每次節點被 access 時都會更動,我們可以猜想它就是用來指示 recently used 的標的。因為資料每次被 access 就會被移到最前方,所以節點位置越靠後表示越久沒被 access 。而每個節點是用 `dlink` 來與 `obj->dhead` 串列互動, ++`hlink` 與 `dlink` 各自獨立、互不干擾++。這就像是一筆資料有兩個分身,一個在 `hheads[]` 中,另一個在 `dhead` 中。 Example for obj->capacity=3 : ```graphviz graph { rankdir = LR; node [shape=record]; h [label="<0>hheads[0]|<1>hheads[1]|<2>hheads[2]"]; edge [weight=3 label="hlink"]; h:0 -- a -- c; h:2 -- b; edge [weight=1]; c -- h:0; b -- h:2; } ``` ```graphviz graph { rankdir = LR; node [shape=record]; edge [weight=3 label="dlink"]; dhead -- c -- b -- a; edge [weight=1]; a -- dhead; } ``` 此外值得注意的是第 7 行,藉由 `lru->dlink` 的方式快速找到目標節點在 `dhead` 中的分身,我們就不必再於 `dhead` 走訪一次。 ### CachePut() 以上圖為例,當又有一筆資料要放進 cache 的時候,節點 `a` 的兩個分身都會被踢出,因為它在 `dhead` 這條串列的尾巴,如此功能便是在 `CachePut()` 實現。 ```c= void lRUCachePut(LRUCache *obj, int key, int value) { LRUNode *lru; int hash = key % obj->capacity; MMM3 (lru, &obj->hheads[hash], hlink) { if (lru->key == key) { list_move(&lru->dlink, &obj->dhead); lru->value = value; return; } } if (obj->count == obj->capacity) { lru = MMM4(&obj->dhead, LRUNode, dlink); list_del(&lru->dlink); list_del(&lru->hlink); } else { lru = malloc(sizeof(LRUNode)); obj->count++; } lru->key = key; list_add(&lru->dlink, &obj->dhead); list_add(&lru->hlink, &obj->hheads[hash]); lru->value = value; } ``` 此函式概略運作邏輯如圖: ```graphviz digraph{ st [label="CachePut"]; search [label="查詢是否已在 cache 中"]; st -> search; exist [label="修改 lru->value"]; capcheck [label="cache 是否已滿"]; search -> exist [label="yes"]; search -> capcheck [label="no"]; full [label="踢掉最舊的節點 並換成新的"]; not [label="為 lru 配置空間 並新增到 cache 中"]; capcheck -> full [label="yes"]; capcheck -> not [label="no"]; } ``` 第 5 行依據 `key` 尋找節點是否已存在,因此 `MM3` 顯然也是 `list_for_each_entry()` 。 根據流程圖可以分成 3 個分支討論 - (yes) - (no, yes) - (no, no) #### (yes) 如果有找到的話就更新 `dlink` (recently used) ,並修改節點的 `value` 值。 #### (no, yes) 使用 `obj->count` 來計數是否已達 cache 容量上限 。 在空間已滿的情況下,我們的 policy 是移除 least recently used 的節點,也就是 `dhead` 的尾節點。 為了避免 `free()` 之後又馬上 `malloc()` ,我們可以回收利用舊節點空間。首先我們得要找到舊節點,取得尾節點可以使用 API `list_last_entry()` ,於是我們解出了 `MM4` 。 接著得將節點從對應的 `hhead[]` 和 `dhead` 中移出,填入新資料之後依據新的 `key` 重新串入 `hheads[hash]` 並移到 `dhead` 最前方。 #### (no, no) 這個分支也相對簡單,就是為資料建構新節點,並更新 `hheads[hash]`, `dhead` 及 `count` 。 ### CacheFree() ```c void lRUCacheFree(LRUCache *obj) { LRUNode *lru, *n; MMM1 (lru, n, &obj->dhead, dlink) { list_del(&lru->dlink); free(lru); } free(obj); } ``` 將 `obj` 清空的函式, `MM1` 為 `list_for_each_entry_safe()` ,此 API 有註解 ```c iterate over list entries and allow deletes ``` 注意到因為所有進到 cache 節點必定有在 `dhead` 的分身,所以我們不需要去走訪 `hheads[]` 陣列,只要清空 `dhead` 串列就可以釋放完所有的 `LRUNode` 。 且因 `hheads[]` 是隨著 `obj` 動態配置而來,所以我們不應該執行 ```c free(hheads); ``` 只要呼叫 ```c free(obj); ``` 便能一勞永逸。 ### 缺點探討 在 `hlink` 所屬的串列上,我們會進行的操作就是 - 走訪 - 新增 - 走訪後移除 並沒有特別需要 access 尾節點的需求,所以我認為我們沒有必要使用環狀串列,取而代之可以使用 kernel 中 `hlist` 風格 (如測驗 1 ) 的串列,它更便利於從 list 中移除節點。 而 `dlink` 所屬的串列則因頭尾操作的需要,維持 kernel `list` 風格。 ### LRU in Linux kernel 對應本題,我找到了 [**linux/lru_cache.h**](https://github.com/torvalds/linux/blob/master/include/linux/lru_cache.h) 。 其中定義了兩個資料結構 ```c struct lc_element { struct hlist_node colision; struct list_head list; /* LRU list or free list */ unsigned refcnt; /* back "pointer" into lc_cache->element[index], * for paranoia, and for "lc_element_to_index" */ unsigned lc_index; /* if we want to track a larger set of objects, * it needs to become an architecture independent u64 */ unsigned lc_number; /* special label when on free list */ #define LC_FREE (~0U) /* for pending changes */ unsigned lc_new_number; }; ``` 這裡的 `colision` 成員相當於我們的 `hlink` ,使用了 `hlist` 的結構,與我的想法相符; `list` 成員相當於 `dlink` 。 `lc` 應是 `lru_cache` 的縮寫。 :::warning 不確定 element 是怎麼存放資料的,是用內嵌結構或是用 unsigned 儲存指標訊息 ::: ```c struct lru_cache { /* the least recently used item is kept at lru->prev */ struct list_head lru; struct list_head free; struct list_head in_use; struct list_head to_be_changed; /* the pre-created kmem cache to allocate the objects from */ struct kmem_cache *lc_cache; /* size of tracked objects, used to memset(,0,) them in lc_reset */ size_t element_size; /* offset of struct lc_element member in the tracked object */ size_t element_off; /* number of elements (indices) */ unsigned int nr_elements; /* Arbitrary limit on maximum tracked objects. Practical limit is much * lower due to allocation failures, probably. For typical use cases, * nr_elements should be a few thousand at most. * This also limits the maximum value of lc_element.lc_index, allowing the * 8 high bits of .lc_index to be overloaded with flags in the future. */ #define LC_MAX_ACTIVE (1<<24) /* allow to accumulate a few (index:label) changes, * but no more than max_pending_changes */ unsigned int max_pending_changes; /* number of elements currently on to_be_changed list */ unsigned int pending_changes; /* statistics */ unsigned used; /* number of elements currently on in_use list */ unsigned long hits, misses, starving, locked, changed; /* see below: flag-bits for lru_cache */ unsigned long flags; void *lc_private; const char *name; /* nr_elements there */ struct hlist_head *lc_slot; struct lc_element **lc_element; }; ``` 在 **lru_cache.h** 中,節點通常不會真的被刪除,而是不斷被移到 `lru_cache` 裡的各個不同串列,見這段註解 ``` * .list is on one of three lists: * in_use: currently in use (refcnt > 0, lc_number != LC_FREE) * lru: unused but ready to be reused or recycled * (lc_refcnt == 0, lc_number != LC_FREE), * free: unused but ready to be recycled * (lc_refcnt == 0, lc_number == LC_FREE), * * an element is said to be "in the active set", * if either on "in_use" or "lru", i.e. lc_number != LC_FREE. ``` 藉此可以推測 `refcnt` 的功能是紀錄該 element 的 user 數量,原意可能是 reference counter 。若沒有 user 使用則會被移到 `lru` 或 `free` 。 為了了解這些函式,得再前往 [**lru_cache.c**](https://github.com/torvalds/linux/blob/master/lib/lru_cache.c) :::spoiler `lc_get` 的實作函式 ```c static struct lc_element *__lc_get(struct lru_cache *lc, unsigned int enr, unsigned int flags) { struct lc_element *e; PARANOIA_ENTRY(); if (lc->flags & LC_STARVING) { ++lc->starving; RETURN(NULL); } e = __lc_find(lc, enr, 1); /* if lc_new_number != lc_number, * this enr is currently being pulled in already, * and will be available once the pending transaction * has been committed. */ if (e) { if (e->lc_new_number != e->lc_number) { /* It has been found above, but on the "to_be_changed" * list, not yet committed. Don't pull it in twice, * wait for the transaction, then try again... */ if (!(flags & LC_GET_MAY_USE_UNCOMMITTED)) RETURN(NULL); /* ... unless the caller is aware of the implications, * probably preparing a cumulative transaction. */ ++e->refcnt; ++lc->hits; RETURN(e); } /* else: lc_new_number == lc_number; a real hit. */ ++lc->hits; if (e->refcnt++ == 0) lc->used++; list_move(&e->list, &lc->in_use); /* Not evictable... */ RETURN(e); } /* e == NULL */ ++lc->misses; if (!(flags & LC_GET_MAY_CHANGE)) RETURN(NULL); /* To avoid races with lc_try_lock(), first, mark us dirty * (using test_and_set_bit, as it implies memory barriers), ... */ test_and_set_bit(__LC_DIRTY, &lc->flags); /* ... only then check if it is locked anyways. If lc_unlock clears * the dirty bit again, that's not a problem, we will come here again. */ if (test_bit(__LC_LOCKED, &lc->flags)) { ++lc->locked; RETURN(NULL); } /* In case there is nothing available and we can not kick out * the LRU element, we have to wait ... */ if (!lc_unused_element_available(lc)) { __set_bit(__LC_STARVING, &lc->flags); RETURN(NULL); } /* It was not present in the active set. We are going to recycle an * unused (or even "free") element, but we won't accumulate more than * max_pending_changes changes. */ if (lc->pending_changes >= lc->max_pending_changes) RETURN(NULL); e = lc_prepare_for_change(lc, enr); BUG_ON(!e); clear_bit(__LC_STARVING, &lc->flags); BUG_ON(++e->refcnt != 1); lc->used++; lc->pending_changes++; RETURN(e); } ``` ::: ## 測驗 4