# linux2024-homework2
## Week1
### 測驗 `1`
- 排序原理
使用非遞迴的方式來實現快速排序法,運作的原理是先選定 `pivot` 節點,再將串列以此為基準,分為 `left` 和 `right` 兩個串列,使用 `begin` 和 `end` 兩個堆疊來儲存兩個串列的開頭與結尾以及分類的中心 `pivot`。
下面以圖形話來重現程式的流程:
開始有串列 [2, 0, 4, 1, 5, 3],`left` 以及 `right` 皆為 NULL,儲存比較數值的 `L` 和 `R` 先指在頭跟尾,`piovt` 為串列第一節點的數值。
```graphviz
digraph structs {
node[shape=plaintext];
pivot [fontcolor="red"];
L [fontcolor="blue"];
R [fontcolor="blue"];
node[shape=box];
struct0 [label= "2"];
struct1 [label= "0"];
struct2 [label= "4"];
struct3 [label= "1"];
struct4 [label= "5"];
struct5 [label= "3"];
{
rank="same";
struct0 -> struct1
struct1 -> struct2
struct2 -> struct3
struct3 -> struct4
struct4 -> struct5
}
pivot -> struct0;
L -> struct0;
R -> struct5;
}
```
隨著指標 `p` 從頭遍歷到尾部,將串列分成三個部分,比 `pivot` 數值小的會被依序放入 `left` 串列,反之放入 `right` 串列,過程中呼叫 `list_add` 函式執行加入的動作,該函式將以建立好的節點放入串列的最前端,下面是三個部份的示意圖。
```graphviz
digraph structs{
node[shape=plaintext];
left [fontcolor="blue"];
pivot [fontcolor="red"];
right [fontcolor="blue"];
node[shape=box]
struct0 [label= "2"];
struct1 [label= "0"];
struct2 [label= "4"];
struct3 [label= "1"];
struct4 [label= "5"];
struct5 [label= "3"];
pivot -> struct0;
left -> struct3;
struct3 -> struct1;
right -> struct5;
struct5 -> struct4;
struct4 -> struct2;
}
```
完成分開左右串列後,開始建立 `begin` 和 `end` 堆疊,首先加入 `left` 串列的首跟尾,再來是 `pivot`,最後是 `right`,完成堆疊建立後索引 `i` 會被加二,指在兩個堆疊最末端。
```graphviz
digraph structs{
node[shape=plaintext];
begin [fontcolor="blue"];
end [fontcolor="blue"];
i [fontcolor="red"];
node[shape=box]
struct0 [label= "2"];
struct1 [label= "0"];
struct2 [label= "4"];
struct3 [label= "1"];
struct4 [label= "2"];
struct5 [label= "3"];
begin -> struct3;
struct3 -> struct0;
struct0 -> struct5;
end -> struct1;
struct1 -> struct4;
struct4 -> struct2;
{
rank="same";
i -> struct2;
}
}
```
當索引 `i` 大於零時,`while` 迴圈條件成立,將 `begin[i]` 和 `end[i]` 分別指派給 `L` 和 `R`, `pivot` 初始化為 `L` (數值為 3 的節點),再次進行分開左右串列以及進行堆疊的動作。
```graphviz
digraph structs{
node[shape=plaintext];
left [fontcolor="blue"];
pivot [fontcolor="red"];
right [fontcolor="blue"];
node[shape=box]
struct2 [label= "4"];
struct3 [label= "NULL"]
struct4 [label= "5"];
struct5 [label= "3"];
pivot -> struct5;
left -> struct3;
right -> struct2;
struct2 -> struct4;
}
```
`begin` 和 `end` 堆疊在索引 `i` 的位置會被新進數值覆蓋(以紅字表示),索引 `i` 再次加二:
```graphviz
digraph structs{
node[shape=plaintext];
begin [fontcolor="blue"];
end [fontcolor="blue"];
i [fontcolor="red"];
node[shape=box]
struct0 [label= "2"];
struct1 [label= "0"];
struct2 [label= "NULL"][fontcolor="red"];
struct3 [label= "1"];
struct4 [label= "2"];
struct5 [label= "NULL"][fontcolor="red"];
struct6 [label= "3"];
struct7 [label= "3"];
struct8 [label= "4"];
struct9 [label= "5"];
begin -> struct3;
struct3 -> struct0;
struct0 -> struct5;
struct5 -> struct6;
struct6 -> struct8;
end -> struct1;
struct1 -> struct4;
struct4 -> struct2;
struct2 -> struct7;
struct7 -> struct9;
{
rank="same";
i -> struct9;
}
}
```
因為現在索引 i 所指的兩個數值(`begin[i]`=4) 和 (`end[i]`=5),仍然不相等,所以還要進行下一輪比較,重複上面分割後為堆疊插入新值兩步驟,當下一輪比較結束時`begin` 和 `end` 的模樣為下面的樣式:
```graphviz
digraph structs{
node[shape=plaintext];
begin [fontcolor="blue"];
end [fontcolor="blue"];
i [fontcolor="red"];
node[shape=box]
struct0 [label= "2"];
struct1 [label= "0"];
struct2 [label= "NULL"];
struct3 [label= "1"];
struct4 [label= "2"];
struct5 [label= "NULL"];
struct6 [label= "3"];
struct7 [label= "3"];
struct8 [label= "NULL"][fontcolor="red"];
struct9 [label= "NULL"][fontcolor="red"];
struct10 [label= "4"];
struct11 [label= "4"];
struct12 [label= "5"];
struct13 [label= "5"];
begin -> struct3;
struct3 -> struct0;
struct0 -> struct5;
struct5 -> struct6;
struct6 -> struct8;
struct8 -> struct10;
struct10 -> struct12;
end -> struct1;
struct1 -> struct4;
struct4 -> struct2;
struct2 -> struct7;
struct7 -> struct9;
struct9 -> struct11;
struct11 -> struct13;
{
rank="same";
i -> struct13;
}
}
```
此時索引 `i` 所指的兩個數值(`begin[i]`=5) 和 (`end[i]`=5)相等,不滿足 `if` 條件,所以進行 `else` 的動作,如果 `L` 不為空的時候,將 `L` 的值,也就`begin[i]` 加入串列 `reslut`,並將索引 `i` 減一,重複動作值到碰到不相等的時候(`begin[i] != end[i]`)。
當索引 `i` 扣到小於 0 時,`while` 條件不成立跳離,將 `result` 指派給 `list`,因為是使用間接指標,所以可以直接影響原本的值。
```graphviz
digraph structs {
node[shape=plaintext];
List [fontcolor="blue"];
node[shape=box];
struct0 [label= "0"];
struct1 [label= "1"];
struct2 [label= "2"];
struct3 [label= "3"];
struct4 [label= "4"];
struct5 [label= "5"];
{
rank="same";
struct0 -> struct1
struct1 -> struct2
struct2 -> struct3
struct3 -> struct4
struct4 -> struct5
}
}
```
- 效能評估及改進
使用 `clock()` 方法來計算中央處理器時脈時間,除以 `CLOCKS_PER_SEC` 得到程式執行花費時間,原本的程式執行效能結果為:
```
Size: 10, Execution time: 0.000005 seconds, Cpu clock time: 5
Size: 100, Execution time: 0.000038 seconds, Cpu clock time: 38
Size: 1000, Execution time: 0.000561 seconds, Cpu clock time: 561
Size: 10000, Execution time: 0.044383 seconds, Cpu clock time: 44383
Size: 100000, Execution time: 5.653967 seconds, Cpu clock time: 5653967
```
改進的部分認為可以避免每次都給 `pivot` 從最左邊開始選擇,可以避免整個串列都要走一遍的最糟狀況。
### 測驗 `2`
總共有四個檔案:
- `sort_impl.h` 包裝 Timsort 在內的排序操作介面
- `main.c` 用來測試 Timsort 的鏈結串列程式碼
- `timsort.c` 實作 Timsort,刻意不額外配置記憶體空間
- `list.h` 引入 Linux 核心原始程式碼的鏈結串列實作
在檔案 `sort_impl.h` 中定義一個名為 `list_cmp_func_t` 的函式指標類型,該函式指標類型可以指向一個具有特定格式的函式,這個函式接受三個參數,參數代表了比較列表元素所需的資料以及兩個要比較的列表元素。函式該返回一個整數值,表示比較結果,程式段落如下:
```c
typedef int (*list_cmp_func_t)(void *,
const struct list_head *,
const struct list_head *);
```
在 `main.c` 程式中使用 `create_sample` 函式為以配置空間的 `samples` 創造 `nums` 個節點的資料,並將其複製兩份 `testdata_head` 和 `warmdata_head` 分別對其使用 `timsort` 函式排序,使用 `check_list` 函式確認是否有按升序且是預期的順序排序。
檔案 `timsort.c` 定義 Timsort 的實作,首先使用 find_run 函式將鏈結串列切開,接著呼叫 merge_collapse 函式,將 run 合併直到輸入資料完畢,最後使用 `merge_force_collapse` 合併到 `skt_size` 小於 3,如果只剩下一個 run 就呼叫 `build_prev_link` 函式建立往前的連結,剩下兩個 run 就使用 `merge_final` 函式將其合併。
---
下面以圖形話來重現程式的流程,同時再次作答測驗:
開始有串列 [0, 2, 5, 4, 1, 3],這個串列同時進行 run 的切割與合併的動作:
```graphviz
digraph structs {
node[shape=plaintext];
head [fontcolor="blue"];
tail [fontcolor="blue"];
node[shape=box];
struct0 [label= "0"];
struct1 [label= "2"];
struct2 [label= "5"];
struct3 [label= "4"];
struct4 [label= "1"];
struct5 [label= "3"];
{
rank="same";
head -> struct0;
struct0 -> struct1
struct1 -> struct2
struct2 -> struct3
struct3 -> struct4
struct4 -> struct5
struct5 -> tail;
}
}
```
首先進入 `find_run` 切割的動作,參數 list_cmp_func_t 是 compare 函式,此函式會回傳兩者數值相減後的值,判斷式會呼叫 compare 判斷兩節點的大小,若是下個節點 next 小於或等於當前節點 list,則紀錄升序的片段並切割,若 next 大於當前節點 list,則要將其反轉後切割,所以再來得到的樣子是:
```graphviz
digraph structs {
node[shape=plaintext];
"result.head" [fontcolor="blue"];
"result.next" [fontcolor="blue"];
"" [fontcolor="blue"];
NULL [fontcolor="red"]
node[shape=box];
struct0 [label= "0"];
struct1 [label= "2"];
struct2 [label= "4"];
struct3 [label= "5"];
struct4 [label= "1"];
struct5 [label= "3"];
{
rank="same";
"result.head" -> struct0;
struct0 -> struct1;
struct1 -> struct3;
struct3 -> "NULL";
"result.next" -> struct2;
struct2 -> struct4;
struct4 -> struct5;
}
}
```
執行完 `find_run` 回到 `timesort` 函式,將回傳的 `result.head`(0, 2, 5) 值指派給 `tp` 和 `result.next`(4, 1, 3) 值指派給 `list` 後的模樣是:
```graphviz
digraph structs {
node[shape=plaintext];
list [fontcolor="blue"];
"" [fontcolor="blue"];
NULL [fontcolor="red"];
node[shape=box];
struct0 [label= "0"];
struct1 [label= "2"];
struct2 [label= "4"];
struct3 [label= "5"];
struct4 [label= "1"];
struct5 [label= "3"];
{
rank="same";
NULL -> struct0;
struct0 -> struct1;
struct1 -> struct3;
struct3 -> "";
}
{
rank="same";
list -> struct2;
struct2 -> struct4;
struct4 -> struct5;
}
}
```
接下來,因為 stk_size = 1,所以將 `tp` 傳入函式 `merge_collapse` 後沒有做任何動作就回傳,`while` 的條件式因為 `list(4, 1, 3)` 不為空,所以會繼續執行,再次執行 `find_run` ,因為當前節點數值 4 大於下一個節點數值 1,所以要反轉,結果如下:
```graphviz
digraph structs {
node[shape=plaintext];
"result.head" [fontcolor="blue"];
"result.next" [fontcolor="blue"];
"" [fontcolor="blue"];
NULL [fontcolor="red"]
node[shape=box];
struct2 [label= "4"];
struct4 [label= "1"];
struct5 [label= "3"];
{
rank="same";
"result.head" -> struct4;
struct4 -> struct2;
struct2 -> "NULL";
"result.next" -> struct5;
}
}
```
執行完 `find_run` 回到 `timesort` 函式,之前的 `tp`(0, 2, 5) 會放到 result.head 前面,而現在`result.head`(1, 4) 值指派給 `tp` 和 `result.next`(3) 值指派給 `list`,`tp` 的樣子如下:
```graphviz
digraph structs {
node[shape=plaintext];
NULL [fontcolor="red"];
node[shape=box];
struct0 [label= "0, 2, 5"];
struct1 [label= "1,4"];
{
rank="same";
NULL -> struct0;
struct0 -> struct1;
}
}
```
stk_size 加一現在值是 2,再將 `tp` 傳入函式 `merge_collapse`,但是因為 run_size(tp->prev)=3 小於 run_size(tp)=2,所以直接跳離函式,回到 timsort 函式,list(3) 再進入 find_run,結果如下:
```graphviz
digraph structs {
node[shape=plaintext];
"result.head" [fontcolor="blue"];
"result.next" [fontcolor="blue"];
"" [fontcolor="blue"];
NULL [fontcolor="red"]
node[shape=box];
struct5 [label= "3"];
{
rank="same";
"result.head" -> struct5;
"result.next" -> "NULL";
}
}
```
回到 timsort 函式,list 指派為空,stk_size 加一值為 3,將之前的 tp 接到 result.head 前面, tp 現在節點指派為現在的 result.head(3),現在 tp 總共有三個節點((0, 2, 5), (1, 4), (3))。
將 `tp` 傳入函式 `merge_collapse`,滿足 `(n >= 3 && run_size(tp->prev->prev) <= run_size(tp->prev) + run_size(tp))`,呼叫 `merge_at`,其中他會呼叫 `merge` 兩兩比較,將小的點先放入鏈表當中,跳離後對於 stk_size 減一值為2。
函式 `merge` 中 AAAA 填入 &head, BBBB 填入 &a->next,而 CCCC 填入 &b->next,函式,最後回傳結果會是 (1, 3, 4)。
```c
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 = &head; //AAAA
for (;;) {
/* if equal, take 'a' -- important for sort stability */
if (cmp(priv, a, b) <= 0) {
// 回傳 a->val - b->val,如果小於等於零,表示節點 b 數值較大
*tail = a;
tail = &a->next; // BBBB
a = a->next;
if (!a) {
*tail = b;
break;
}
} else {
*tail = b;
tail = &b->next; // CCCC
b = b->next;
if (!b) {
*tail = a;
break;
}
}
}
return head;
}
```
完成這次的合併後, tp 剩下兩個節點 ((0, 2, 5),(1, 3, 4)),因為如果剩下的節點大於 1 就需要做最後的合併 `merge_final`,所以這裡的 `FFFF` 答案填入 1。
```c
if (stk_size <= 1) {
build_prev_link(head, head, stk0);
return;
}
merge_final(priv, cmp, head, stk1, stk0);
```
現在的stk_size = 2,所以是要進入 `merge_final`,一樣兩兩相比後加入鏈表,最後呼叫 `build_prev_link` 將單向鏈表完善成雙向的,其中 DDDD 填入 `tail->next`,而 EEEE 填入 `head->prev`。
```c
static void build_prev_link(struct list_head *head,
struct list_head *tail,
struct list_head *list)
{
tail->next = list; //tail-list
do {
list->prev = tail;
tail = list;
list = list->next;
} while (list);
/* The final links to make a circular doubly-linked list */
tail->next = head; // DDDD = head;
head->prev = tail; // EEEE = tail;
}
```
---
- 改進方案
1. 加入 minrun 的使用
- 合併序列時,若 run 的數量等於或者略小於 2 的冪(ex:2, 4, 8, 16, 32...),效率會最高;若略大於 2 的冪,效率就會特別低。minrun 的設計目的是讓剩餘資料在分割為 minrun 時,能夠盡可能接近 2 的冪,所以選擇 minrun 的策略是 Timsort 的關鍵。
2. 使用 Galloping mode 來合併不合規則的串列,但要判斷使用時機。
- 整合進 lab0-c
>將改進過的 Timsort 實作整合進 lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。
建立 timsort.c 和 timsort.h 兩個檔案進入 lab0-c 的目錄底下,但這是初步的完成 timsort指令,在比較後發現跟 list_sort.c 檔案的函式有所重疊,未來應該要把先前拿掉 list_cmp_fun_t 參數加回來,拿掉重疊的部分:
commit message:
>Implementation of a novel version merge sort
因為 lab0-c 中結構體 `element_t` 的 `attribute` 是 `char` 資料型態的,所以在比較的 `compare` 函式需要做更動, `strcmp` 大於 0,表示參數一較大,可以直接回傳結果作為 `compare` 函式的回傳值。
```diff
// 比較兩個串列節點的值
int tim_compare(const struct list_head *a, const struct list_head *b)
{
if (!a || !b || a == b)
return 0;
- int res = list_entry(a, element_t, list)->val -
- list_entry(b, element_t, list)->val;
+ element_t *a_ele = container_of(a, element_t, list);
+ element_t *b_ele = container_of(b, element_t, list);
+ int result = 0;
+ if (a_ele && b_ele)
+ result = strcmp(a_ele->value, b_ele->value);
- if (priv)
- *((int *) priv) += 1;
return result;
}
```
在 `qtest.c` 檔案中增加排序指令,並且增加 `do_timsort` 函式。
```diff
+ bool do_timsort(int argc, char *argv[]){...}
+ ADD_COMMAND(timsort, "New version of merge sort", "")
```
- 測試
要在 traces 目錄下增加新的 .cmd 檔案,並且更改 scripts 目錄下的 drive.py 檔案,來執行新的命令檔案。
```
# Test of new version of sorting "tim sort"
option fail 0
option malloc 0
new
ih RAND 50000
time
timsort
time
free
```
make test 執行結果如下:
```csharp
scripts/driver.py -c
--- Trace Points
+++ TESTING trace timsort:
# Test of new version of sorting list_sort
Elapsed time = 0.115, Delta time = 0.115
Elapsed time = 0.154, Delta time = 0.038
--- timsort 5/5
--- TOTAL 5/5
```
完成 timsort 指令想要 commit 的時候出現以下錯誤:
```
timsort.c:16:24: error: Null pointer dereference: (struct element_t*)0 [nullPointer]
element_t *a_ele = container_of(a, element_t, list);
```
嘗試在前面先判斷 a 是否為空指令,排除指到空指令的情況,但還是無法,最後是在程式碼後面加上 `cppcheck-suppress nullPointer` 才能 push 成功,程式碼紀錄在 [commit cb5eddd](https://github.com/Lisa304/lab0-c/commit/cb5edddcdad1c4183d2a28d2a1f018c77c33c103)。
因為最近 github 要求要使用手機兩段式認證,導致原先的 https 連結無法使用,最後依照[Generating a new SSH key and adding it to the ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)將連結改成 SSH Key 才恢復 push 的功能。
## week2
### 測驗 `1`
preorder 是"中->左->右", inorder 是 "左->中->右",指要在三種排序中知道兩種,就能夠建構出獨一無二的二元樹。
- preorder 的開頭就是 root value
- 接著去 inorder 找對應的左右元素,不斷將左右當成 root,反覆做到直到再也找不到對應元素。
本程式使用 Linux 核心的 hash table 實作,當所有的 key 經過雜湊函數轉換後,會得到 index,如果都不一樣,稱此雜湊函數為 perfect function。
敘述程式運作原理,以及重新作答:
在 hlist_add_head 函式,AAAA 的答案是 h->first:
```c
/*
hlist_add_head: 往前加節點
@n: 要加入的節點
@h: 現在的 head node
*/
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
if (h->first)
h->first->pprev = &n->next;
n->next = h->first; //AAAA
n->pprev = &h->first;
h->first = n;
}
```
在 `find` 函式當中,BBBB 的答案是 &heads[hash] 和 CCCC 的答案是 list_entry:
```c
/*
find: 使用 node 的 num 屬性找 node 的 idx 屬性
@num: 用來尋找的數值
@size: 用來跟 num 計算 hash
@heads: 現在的 head node
找不到有這個數值的 num 屬性的節點時,就會回傳 -1。
*/
static int find(int num, int size, const struct hlist_head *heads)
{
struct hlist_node *p;
// int hash = (num < 0 ? -num : num) % size; // 轉成正整數的 num 對於 size 取餘數
hlist_for_each (p, &heads[hash]) { // BBBB
struct order_node *on = list_entry(p, struct order_node, node); // CCCC
if (num == on->val)
return on->idx;
}
return -1;
}
```
在 `node_add` 函式當中,DDDD 的答案是 &heads[hash]:
```c
/*
node_add: 新增節點到鏈表最前端
@val: 新節點的數值
@idx: 新節點的在排序中的位置,感覺未來會想要用 hash 來替代
@size: 用來跟 val 計算 hash
@heads: 現在的 head node
建立一個新的節點,並且呼叫 hlist_add_head 把新節點放到鏈表最前端
*/
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, &heads[hash]); //DDDD
}
```
---
改進程式:
1. 使用 hash 值,現在的函式當中 idx 值表示的是該數值在陣列中的位置,或許應該要換成 hash 值來運作。
2. 增加顯示 postortder 排序的函式 `tra_postorder` ,印出 postorder 排序的樣式,方便查看程式的運作。
```c
void tra_postorder(struct TreeNode* node){
while(node->right){
tra_postorder(node->right);
break;
}
printf("%d ", node->val);
while(node->left){
tra_postorder(node->left);
break;
}
}
```
---
### 測驗 `2`
實作遵循最近最少使用 (LRU) 快取約束的資料結構。實作四個功能: `lRUCacheCreate`、`lRUCacheFree` 、`lRUCacheGet` 和 `lRUCachePut`。
- `lRUCacheCreate` 以參數 @capacity 大小,來初始化 LRU 快取的容量。
- `lRUCacheFree` 為 LRU 快取刪除全部節點,並釋放快取的記憶體空間。
- `lRUCacheGet` 取出參數 @key 所對應的數值,如果找不到就會回傳 -1。
- `lRUCachePut` 更新或新增key,沒位置就將LRU的key刪除。
敘述程式運作原理,以及重新作答 EEEE ~ KKKK:
首先程式中會使用到兩種結構體 LRUCache 和 LRUNode,他們會使用 list_head 來串連彼此,而儲存數值的是雜湊表格,LRUCache 儲存多個 hlist_head,LRUNode 會使用 key 屬性找到自己位於哪個 hlist_head 鏈表當中。
首先呼叫 lRUCacheCreate 函式宣告一個容量為 3 的快取:
```graphviz
digraph {
rankdir=LR;
node[shape=record];
subgraph cluster_4 {
dhlist_head [label="dhead | {<prev>prev | <next>next}"];
subgraph cluster_5{
hash0 [label=" hhead[0]", color=lightgrey];
hash1 [label=" hhead[1]", color=lightgrey];
hash2 [label=" hhead[2]", color=lightgrey];
}
label="LRUCache"
}
}
```
使用 `lRUCachePut(obj, 1, 1)` 將節點 (key=1, value=1) 放入快取當中,JJJJ 答案是 node,KKKK 答案是 &c->link:
```c
/*
lRUCachePut: 更新或新增key,沒位置就LRU的key刪除。
@obj: 指向快取的指標
@key: 要放入的 key
@value: 要放入的 key 的對應值
如果該 @key 存在,則更新該 @key 的值。使用 list_move
將節點移到最前面,表示最近才使用過。
否則,將鍵值對新增至快取。如果 @key 的數量超過此快取的
容量(@obj->capacity),則逐出最近最少使用的鍵。
*/
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, node); // JJJJ
if (c->key == key) {
list_move(&c->link, &obj->dhead); // KKKK 把節點移到最前面,表示最新被使用的
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 節點的值
hlist_add_head(&cache->node, &obj->hhead[hash]); // hlist 加到頭
} else {
// 沒有滿,所以就直接加入節點,加在 hash 表上的第 key % obj->capacity 的 hlist
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 = 1,所以 hash 為 1%3 = 1,這個新的 LRUNode 將會放到 hhead[1] 鏈表當中:
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
subgraph cluster_0 {
dhlist_head [label="dhead | {<prev>prev | <next>next}"];
subgraph cluster_3{
hash0 [label=" hhead[0]", color=lightgrey];
hash1 [label=" hhead[1]", color=lightgrey];
hash2 [label=" hhead[2]", color=lightgrey];
}
label="LRUCache"
}
subgraph cluster_1 {
node [shape=record];
link1 [label="link | {<prev>prev | <next>next} "];
node1 [label="node | {<prev>pprev | <next>next} "];
label="LRUNode 1"
}
null [label=NULL, shape="plaintext", color = red]
hash1 -> node1
link1:next -> null [color = red]
node1: prev -> hash1
dhlist_head:next -> link1:prev [color = red]
}
```
我們在執行一次 `lRUCachePut(obj, 2, 2)` 方便下面的功能實作展示,因為 key = 2,所以 hash 為 2%3 = 2,這個新的 LRUNode 將會放到 hhead[2] 鏈表當中:
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
subgraph cluster_0 {
dhlist_head [label="dhead | {<prev>prev | <next>next}"];
subgraph cluster_3{
hash0 [label=" hhead[0]", color=lightgrey];
hash1 [label=" hhead[1]", color=lightgrey];
hash2 [label=" hhead[2]", color=lightgrey];
}
label="LRUCache"
}
subgraph cluster_1 {
node [shape=record];
hlink1 [label="link | {<prev>prev | <next>next} "];
node1 [label="node | {<prev>pprev | <next>next} "];
label="LRUNode 1"
}
subgraph cluster_2 {
node [shape=record];
hlink2 [label="link | {<prev>prev | <next>next} "];
node2 [label="node | {<prev>pprev | <next>next} "];
label="LRUNode 2"
}
null [label=NULL, shape="plaintext"]
hash1 -> node1
hlink1: next -> hlink2:prev[color = red]
node1: prev -> hash1
dhlist_head:next -> hlink1:prev [color = red]
hash2 -> node2
hlink2:next -> null [color = red]
node2: prev -> hash2
}
```
此時如果想要取用 key 值為 2 的資料的時候,首先會去快取查詢是否有這 key 值,我們會呼叫函式 `lRUCacheGet`,HHHH 答案是 node,IIII 答案是 &cache->link:
```c
/*
lRUCacheGet: 查詢 key 的數值
@obj: 指向快取的指標
@key: 想要查詢數值的 key
如果鍵存在則傳回鍵的值,否則回傳-1。
*/
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, node); // HHHH
if (cache->key == key) {
list_move(&cache->link, &obj->dhead); // IIII
return cache->value;
}
}
return -1;
}
```
想要查詢值要去雜湊表查詢,所以在 `list_entry` 要傳入參數是資料型別為 hlist_node 的 `node`,而因為被使用者查詢過,代表最近有使用過,所以要將此節點往前移動,呼叫 `list_move` 函式執行,而儲存節點關連的是 list_head 資料型別,所以傳入參數應該要是 `link`,而此函式要求的參數型別為指標,所以要使用地址運算子 &cache->link,下方是執行 `lRUCacheGet(obj, 1)`後的樣子,可以看到點的鏈結順序已經產生變化 (dhead->LRUNode2->LRUCode1):
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
subgraph cluster_0 {
dhlist_head [label="dhead | {<prev>prev | <next>next}"];
subgraph cluster_3{
hash0 [label=" hhead[0]", color=lightgrey];
hash1 [label=" hhead[1]", color=lightgrey];
hash2 [label=" hhead[2]", color=lightgrey];
}
label="LRUCache"
}
subgraph cluster_1 {
hlink1 [label="link | {<prev>prev | <next>next} "];
node1 [label="node | {<prev>pprev | <next>next} "];
label="LRUNode 1"
}
subgraph cluster_2 {
hlink2 [label="link | {<prev>prev | <next>next} "];
node2 [label="node | {<prev>pprev | <next>next} "];
label="LRUNode 2"
}
null [label=NULL,shape="plaintext"]
dhlist_head:next -> hlink2:prev [color = red]
hlink2: next -> hlink1:prev[color = red]
hlink1: next -> null [color = red]
hash1 -> node1
node1: prev -> hash1
hash2 -> node2
node2: prev -> hash2
}
```
如果呼叫 `lRUCachePut` 函式時,發現快取已經被放滿,那會將鏈表最末端的節點刪除,進行刪除時會呼叫 `hlist_del` 函式,這裡 EEEE 作答為 `next->pprev`,而`pprev` 間接指標,所存放的是前一個節點的 `next` 指標,間接指標的使用解決了刪除 head 節點時的特例。
```c
/*
hlist_del: 刪除節點
@n: 用於指向目標刪除節點的指標
刪除指標 @n 所指向的節點
*/
void hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next, **pprev = n->pprev;
*pprev = next;
if (next)
next->pprev = pprev; //EEEE
}
```
當實驗完成後,要釋放記憶體空間,呼叫函式 `lRUCacheFree`,這裡 GGGG 的答案是 &cache->link,因為要刪除節點,所以是要取出 list_head 屬性的變數 link,而 `list_del` 函式要求的參數型態為指標,所以要加上地址運算子(&):
```c
/*
lRUCacheFree: 為 LRU 快取刪除全部節點,並釋放快取的記憶體空間
@obj: 指向快取的指標
使用 list_for_each_safe 允許刪除,在遍歷時的當下節點指標名稱為 pos,
使用 pos 傳入 list_entry 找到 LRUNode 節點,刪除其屬性 link 後,釋放其記憶體。
*/
void lRUCacheFree(LRUCache *obj)
{
struct list_head *pos, *nex;
list_for_each_safe (pos, nex, &obj->dhead) {
LRUNode *cache = list_entry(pos, LRUNode, link); // FFFF
list_del(&cache->link); // GGGG
free(cache);
}
free(obj);
}
```
### 測驗 `3`
>此程式實作 64 bit 架構的記憶體是如何找尋位址中存放的數值,第 n 個標記為 1 的位元位置。
重新作答以及敘述程式運作原理,從最外層的程式往內部運作說明:
**find_nth_bit**
首先,這是模擬 64 位元的 Linux kernel,將 BITS_PER_LONG 定義為 64,想要尋找第 n 個標記為 1 的位元位置,會呼叫函式 `find_nth_bit`,傳入的三個參數分別是開始進行搜尋的地址 @addr、要搜尋的範圍 @size 以及想要找到為 1 位元的第 @n 個。
當 @size 小於 @n 時,就像如果設定 @n = 5 和 @size = 1,那麼搜尋範圍只有一個位元,不管該位元是否為 1,在 @size 的範圍內也一定找不到第 5 個為 1 的位元,函式會值接回傳 @size。
如果 @size 大於 @n 時,函式呼叫 `small_const_nbits(@size)` 巨集,巨集會檢查三個條件,第一個檢查參數是否為編譯時常數,寫法很特殊,使用到 `__builtin_constant_p` 一個內建的 GCC 函數,如果不能確定是編譯時常數回傳 0,否則回傳 1。第二個檢查是確認 @size 是在 64 以下,最後確認 @size 是否為正數。如果以上三個條件都通過,表示想要檢查的範圍是在一個 word 以內,函式會使用巨集 `GENMASK` 清空 @addr 從 @size-1 到 0 的以外的部分,如果清空後不為 0,則呼叫 `fns`,清空後為零則回傳 @size 表示找不到。
```c
/*
GENMASK(h, l): 產生一個位元掩碼,其中位元 @h 到位 @l 之間的位元被設定為 1,其他位元被清除。
@h: 開始保留為 1 的位置
@l: 結束保留為 1 的位置
@l = 2,((~0) - (1 << (2)) + 1) = -4 = ...1111 1111 1111 1100
@h = 4,(~0UL >> (BITS_PER_LONG - 1 - (4))) = -1 >> 59 = 0000... 0001 1111
*/
#define GENMASK(h, l) \
(((~0UL) - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h))))
```
如果函式呼叫 `small_const_nbits(@size)` 巨集結果為 false 的時候,`find_nth_bit` 函式回傳 `FIND_NTH_BIT(addr[idx], size, n)`。
**fns**
先討論第二種情況,也就是 `small_const_nbits(@size)` 巨集結果為 ture,並且清空後的地址不為零,進入 `fns` 的狀況。
`fns` 函式會傳入兩個參數,第一個參數 @word 代表要進行尋找的 word,也是剛才被清空後的地址,而第二個參數跟前面,是想要找到為 1 位元的第 @n 個。
當 @word 地址不為零的時候,呼叫 `__ffs(@word)` 巨集,這裡的 AAAA 作答為 0xffffffff,如果 @word 是 0000 1000,函式回傳 3 代表最低的有 1 位元是第四位。
```c
/*
__ffs: find first bit in word. 用於查找給定的 64 位無符號整數中的最低位元。
@word: The word to search
Undefined if no bit exists, so code should check against 0 first.
一個 f 代表 4 個 1,所以 16 三個 f,32 該要有 8 個 f。
*/
static inline unsigned long __ffs(unsigned long word)
{
int num = 0;
#if BITS_PER_LONG == 64
if ((word & 0xffffffff) == 0) { // AAAA
// 檢查低 32 位是否全為 0,是的話就要將 word 右移 32 位
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` 完成後,`fns` 函式會檢查 n 是否為零,如果為零代表已經找到第 @n 個 1 值接回傳,不是的話就就 @n 減一並且呼叫 `__clear_bit` 巨集,將剛才找到的 1 給清除,這裡 BBBB 的答案是 ~mask,要使用 ~ 運算子將呼叫 `BIT_MASK` 巨集產生的遮罩做反轉,達到清空特定位元的效果。
```c
static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr)
{
/*
BIT_MASK: 用於生成位元 nr 所對應的遮罩
@nr: 生成只有在第 @nr 個位置會為 1 的數值
會變成 2 的 @nr % 64 次方,如果 @nr = 2 => 0000 0100
生成位元遮罩的意義在於方便地進行位元操作
*/
unsigned long mask = BIT_MASK(nr);
// BIT_WORD 用於計算 @nr 在位圖中對應的字(word)索引
// p 是把地址在加上偏差值
unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr);
*p &= ~mask; // BBBB
}
```
在最後如果 `fns` 找不到 1 位元的話就會回傳 `BITS_PER_LONG` 表示找不到。
**FIND_NTH_BIT**
第三種情況,也就是 `small_const_nbits(@size)` 巨集結果為 false,`find_nth_bit` 函式回傳 `FIND_NTH_BIT(addr[idx], size, n)`的狀況,這裡作答 CCCC = %。
```c
/*
FIND_NTH_BIT: 找到從左到右數第 n 個為 1 的位置
@FETCH: 一個將被替換的表達式
@size: 要被搜尋的最大數量位元數
@num: 從0開始計數,指定要找第 n 個為 1
*/
#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 % BITS_PER_LONG)
tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \
found: \
sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \
out: \
sz; \
})
```
函式中呼叫 `hweight_long(tmp)` 來位 64 位元的參數計算當中含有的 1 位元個數,最底層的原理是使用下面的 `__const_hweight8` 巨集來達成的:
```c
/*
__const_hweight8: 用來計算一個 8 位數字中包含的位元 1 的個數
@w: pointer to the previous node in the list
(w) & (1ULL << n): 這個表達式將 w 中的第 n 位與 1 做按位與運算,以檢查該位是否為 1
(1ULL << 0): << 是左移位運算符,這部分代表將 1 左移 0 位,也就是 1
如果表達式的值為 0,則 !! 將其轉換為 0;如果表達式的值為非零,則 !! 將其轉換為 1
假設現在 w = 5,其二進為表示是 0000 0101,經過此巨集結果為 2
*/
#define __const_hweight8(w) \
((unsigned int) ((!!((w) & (1ULL << 0))) + (!!((w) & (1ULL << 1))) + \
(!!((w) & (1ULL << 2))) + (!!((w) & (1ULL << 3))) + \
(!!((w) & (1ULL << 4))) + (!!((w) & (1ULL << 5))) + \
(!!((w) & (1ULL << 6))) + (!!((w) & (1ULL << 7)))))
```