# 2023q1 Homework1 (lab0)
contributed by < [`bonianlee`](https://github.com/bonianlee) >
## 開發環境
```bash
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 6
On-line CPU(s) list: 0-5
Thread(s) per core: 1
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 158
Model name: Intel(R) Core(TM) i5-9500 CPU @ 3.00GHz
Stepping: 10
CPU max MHz: 4400.0000
CPU min MHz: 800.0000
BogoMIPS: 6000.00
Virtualization: VT-x
L1d cache: 192 KiB
L1i cache: 192 KiB
L2 cache: 1.5 MiB
L3 cache: 9 MiB
NUMA node0 CPU(s): 0-5
```
## 開發過程
:::info
作業要求
- [x] q_new: 建立新的「空」佇列
- [x] q_free: 釋放佇列所佔用的記憶體
- [x] q_insert_head: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則)
- [x] q_insert_tail: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則)
- [x] q_remove_head: 在佇列開頭 (head) 移去 (remove) 給定的節點
- [x] q_remove_tail: 在佇列尾端 (tail) 移去 (remove) 給定的節點
- [x] q_size: 計算目前佇列的節點總量
- [x] q_delete_mid: 移走佇列中間的節點並釋放之
- [x] q_delete_dup: 在已經排序的狀況,移走佇列中具備重複內容的節點並釋放它們
- [x] q_swap: 交換佇列中鄰近的節點
- [x] q_reverse: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點
- [x] q_reverseK: 以K個節點為小組 (K-group) 進行分組,並以反向順序重新排列小組內的鏈結串列
- [x] q_sort: 以遞增順序來排序鏈結串列的節點
- [x] q_descend: 若佇列中任意節點的右端存在嚴格大於 (strictly greater) 的值 (value) ,則將該節點移去 (remove)
- [x] q_merge: 將所有佇列合併 (merge) 成一個以遞增順序排列好的鏈結串列
:::
### 三個重要的結構體
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
```c
/* Linked list element */
typedef struct {
char *value;
struct list_head list;
} element_t;
```
```c
/* The context managing a chain of queues */
typedef struct {
struct list_head *q;
struct list_head chain;
int size;
int id;
} queue_contex_t;
```
### q_new
建立新的「空」佇列,將其 linked list 的 ` next ` 與 ` prev ` 指向本身
- 實作流程
1. 使用 `malloc` 分配記憶體空間,並以 `head` 指向它
2. 如果空間分配失敗, `malloc` 會回傳 `NULL` 並被 `head` 指向,此時讓 `q_new` 回傳 `NULL`
3. 如果空間分配成功, `if (head)` 判斷式會執行,參考 `list.h` 中的函式 `INIT_LIST_HEAD` ,將其 linked list 的 ` next ` 與 ` prev ` 指向本身,做初始化的動作
- `list.h` 參考函式 `INIT_LIST_HEAD`
```c
static inline void INIT_LIST_HEAD(struct list_head *head)
{
head->next = head;
head->prev = head;
}
```
- 實作程式碼
```c
truct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (head) {
INIT_LIST_HEAD(head);
return head;
}
return NULL;
}
```
### q_free
釋放所有由佇列佔用的記憶體空間
- 實作流程
1. 如果 `list_head *l` 不存在 (`NULL`) ,則不執行記憶體釋放
2. 如果 `list_head *l` 存在,參考 `list.h` 中的函式 `list_empty` ,判斷 linked list 開頭 (head) 有沒有接著其他的 node ,若有則使用 `q_remove_head` 與 `q_release_element` 來連續清除記憶體空間,最後再將「空」佇列 `l` 佔用的記憶體空間清除
- 實作程式碼
```c
void q_free(struct list_head *l)
{
if (l) {
while (!list_empty(l))
q_release_element(q_remove_head(l, NULL, 0));
free(l);
}
return;
}
```
:::danger
列出對應的 GitHub commit 超連結即可。
:notes: jserv
:::
<s>
:::spoiler `q_remove_head` 與 `q_release_element` 程式碼
- `q_remove_head`
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *element = list_first_entry(head, element_t, list);
if (bufsize && sp) {
strncpy(sp, element->value, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
list_del_init(&element->list);
return element;
}
```
- `q_release_element`
```c
static inline void q_release_element(element_t *e)
{
test_free(e->value);
test_free(e);
}
```
:::
</s>
### q_insert_head
在佇列開頭 (head) 插入 (insert) 給定的新節點
- 實作流程
1. 使用 `malloc` 配置 `element_t` 的記憶體空間,若空間分配失敗,則回傳 `false`
2. 使用 `strlen` 計算引數 `s` 的字串長度 (包含空格) ,作為分配記憶體空間給 `element->value` 的依據,**如果 `value` 的記憶體配置失敗,記得要先將 `element` 的空間釋放掉,才可以回傳 `false`**
3. 使用 `strncpy` 將 `s` 複製給 `element->value` ,再參考 `list.h` 中的 `list_add` 將 `element` 插入 (insert) 到佇列開頭 (head)
- `list.h` 參考函式 `list_add`
```c
static inline void list_add(struct list_head *node, struct list_head *head)
{
struct list_head *next = head->next;
next->prev = node;
node->next = next;
node->prev = head;
head->next = node;
}
```
- 實作程式碼
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *element = malloc(sizeof(element_t));
if (!element)
return false;
int len = strlen(s);
element->value = malloc((len + 1) * sizeof(char));
if (!element->value) {
free(element);
return false;
}
strncpy(element->value, s, len);
*(element->value + len) = '\0';
list_add(&element->list, head);
return true;
}
```
### q_insert_tail
在佇列尾端 (tail) 插入 (insert) 給定的新節點
- 實作流程
重複 `q_insert_head` 實作流程的 1 , 2 步驟,惟步驟 3 改成使用 `list_add_tail` 將新的 `element` 插入 (insert) 到在佇列尾端 (tail)
- `list.h` 參考函式 `list_add_tail`
```c
static inline void list_add_tail(struct list_head *node, struct list_head *head)
{
struct list_head *prev = head->prev;
prev->next = node;
node->next = head;
node->prev = prev;
head->prev = node;
}
```
- 實作程式碼
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *element = malloc(sizeof(element_t));
if (!element)
return false;
int len = strlen(s);
element->value = malloc((len + 1) * sizeof(char));
if (!element->value) {
free(element);
return false;
}
strncpy(element->value, s, len);
*(element->value + len) = '\0';
list_add_tail(&element->list, head);
return true;
}
```
### q_remove_head
在佇列開頭 (head) 移去 (remove) 給定的節點
- 實作流程
1. 如果 `head` 不存在,或者為「空」佇列,則回傳 `NULL`
2. 使用 `list_first_entry` 取得開頭的 element ,並使用 `strncpy` 將 `element->value` 複製給 `sp`
3. 使用 `list_del_init` 將開頭 (head) 的 element 移去 (remove)
- 實作程式碼
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *element = list_first_entry(head, element_t, list);
if (bufsize && sp) {
strncpy(sp, element->value, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
list_del_init(&element->list);
return element;
}
```
:::spoiler `list_first_entry` 與 `list_del_init` 程式碼
- `lsit_first_entry`
```c
#define list_first_entry(head, type, member) \
list_entry((head)->next, type, member)
```
- `list_del_init`
```c
static inline void list_del_init(struct list_head *node)
{
list_del(node);
INIT_LIST_HEAD(node);
}
```
:::
### q_remove_tail
在佇列尾端 (tail) 移去 (remove) 給定的節點
- 實作流程
重複 `q_remove_head` 實作流程的步驟,惟步驟 2 的 `list_first_entry` 改成使用 `list_last_entry` ,取得尾端 (tail) 的 element
- `list.h` 中的 `list_last_entry`
```c
#define list_last_entry(head, type, member) \
list_entry((head)->prev, type, member)
```
- 實作程式碼
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *element = list_last_entry(head, element_t, list);
if (bufsize && sp) {
strncpy(sp, element->value, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
list_del_init(&element->list);
return element;
}
```
### q_size
計算目前佇列的節點總量
- 實作流程
1. 若 `head` 不存在,則傳回 0
2. 使用 `list.h` 中的巨集 `list_for_each` 逐一走訪鏈結串列,每存取到 `temp` 則遞增 `count`,最後回傳 `count`
:::warning
踩雷紀錄:迭代所使用的指標 `temp` 不要指向另外 `malloc` 的記憶體空間,因為若沒有 `free` 掉 `temp` 所佔用的記憶體空間,當 `$ make test` 執行重複呼叫 `q_size` 的 Trace 測試檔時,會導致 `q_free` 後仍有記憶體空間被佔用而沒被釋放掉,將 `temp` 改成指向 `NULL` ,則不會佔用到記憶體空間
:::
- 實作程式碼
```c
int q_size(struct list_head *head)
{
if (!head)
return 0;
int count = 0;
struct list_head *temp = NULL;
list_for_each (temp, head)
count++;
return count;
}
```
### q_delete_mid
移走佇列中間的節點並釋放之
參考[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)
- 思考方向
1. 龜兔賽跑 (Tortoise and Hare Algorithm):
使用兩個指標 `fast` 和 `slow` 指向 `head->next` ,其中 `fast` 的前進速度較快,一次走兩格節點,而 `slow` 的速度較慢,一次走一格節點,如此一來,只要當 `if (fast == head || fast->next == head)` 判斷式成立,指標 `slow` 所指向的節點即為佇列中間的節點,將之移走 (remove) 並且釋放掉 (release) 佔用的記憶體空間即可
2. 頭尾逼近
因為 `struct list_head` 是雙向的 linked list ,所以除了使用龜兔賽跑那樣同向的指標移動之外,還可以從頭跟尾互相接近的方式來找出佇列中間的節點位置,這種策略能夠減少指標移動的次數
- 實作流程 (採用頭尾逼近)
1. `head` 至少要接一個節點,才可以執行 delete mid 的動作,因此當 `if(!head || list_empty(head))` 判斷式滿足,則傳回 `false`
2. 建立兩個指標 `forward` 與 `backward` , `forward` 指向 `head->next` ,而 `backward` 指向 `head->prev` ,我使用 `while` 迴圈來不斷移動指標,若滿足判斷式 `forward != backward && forward != backward->prev` ,則使 `forward = forward->next` 和 `backward = backward->prev` ,即互相靠近,並且每次只前進一個節點
3. 當發生以下兩種情形之一,代表已經找出佇列中間的節點了
- `forward == backward`
若佇列除了 `head` 外,連接了**奇數**個節點,則這種情形就會發生,此時 `forward` 和 `backward` 指向的節點即為佇列中間的節點
- `forward == backward->prev`
若佇列除了 `head` 外,連接了**偶數**個節點,則這種情形就會發生,此時 `backward` 指向的節點即為佇列中間的節點
4. 將步驟 3 的兩種情形用 `if` 判斷式做區別,即可找出佇列中間的節點,並做移去 (remove) 與釋放 (release) 記憶體的動作,這邊我使用 `list.h` 中的 `list_del` 與 `list_first_entry` ,再搭配 `q_release_element` 來完成
:::warning
踩雷紀錄: `list.h` 內的 `list_del` 做的事情只有將節點從 linked list 中孤立出來,並沒有做記憶體釋放的動作,因此我使用 `list.h` 中的 `list_first_entry` 找出佇列中間 element 的**前一個** element 的地址,再用 `q_release_element` 釋放掉佇列中間 element 佔用的記憶體空間
:::
- 實作程式碼
```diff
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *forward = head->next;
struct list_head *backward = head->prev;
if (!forward)
return false;
if (!backward)
return false;
while (forward != backward && forward != backward->prev) {
forward = forward->next;
backward = backward->prev;
}
if (forward == backward) {
+ element_t *element = list_first_entry(forward->prev, element_t, list);
list_del(forward);
+ q_release_element(element);
return true;
}
if (forward == backward->prev) {
+ element_t *element = list_first_entry(forward, element_t, list);
list_del(backward);
+ q_release_element(element);
return true;
}
return true;
}
```
### q_delete_dup
在已經排序的狀況,移走佇列中具備重複內容的節點並釋放它們
:::danger
注意[資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
:notes: jserv
:::
:::success
遍歷:著重在「完全」走訪每個節點
本項 `q_delete_dup` 的實作目的是要找出特定內容的節點並釋放之,使用「遍歷」可能會令焦點模糊不清,已修正不當的詞彙描述,謝謝教授!
:cat2: ccasdqwe
:::
- 實作流程
1. 除了 `head` 指向的「空」節點外,至少還要連接兩個以上的節點才有可能發生重複內容的狀況,因此先做例外排除 `if (!head || list_empty(head) || list_is_singular(head))` ,若滿足判斷式就傳回 `false`
2. 宣告兩個新指標 `curr` 和 `safe` ,用來作為 `list_for_each_safe` 的引數,另外還宣告了布林變數 `diff` ,用來在之後的迭代過程判斷是否要繼續執行刪除的動作,將它初始化為 `false`
3. 使用 `list.h` 中的巨集 `list_for_each_safe` 來做 `curr` 與 `safe` 的<s>遍歷</s> --->逐一走訪,另外宣告一個指向 `curr->next` 的指標 `next` ,並宣告 `curr_entry` 與 `next_entry` 分別指向它們所屬的 element
4. 判斷式中 `!strcmp(curr_entry->value, next_entry->value)` 用來比較 `curr_entry` 與 `next_entry` 的 `value` 是否相同,若相同 `strcmp()` 會回傳 0 ,加入邏輯運算子 `!` 就會得到 `true` 的結果,如此一來,當 if 判斷式滿足,即代表兩個相鄰 element 的 `value` 重複出現,此時將 `curr` 移去 (remove) 並且釋放掉 (release) 它占用的記憶體空間,最後令 `diff = true` ,代表下一個 element 也是待釋放的目標
5. 當 `if ((next != head) && (!strcmp(curr_entry->value, next_entry->value)))` 不再滿足,且 `diff` 仍為 `true` ,代表 `curr` 所指向的 element 為連續重複內容的最末端 element ,將它釋放掉後,令 `diff = false` 代表已刪除完其中一組重複內容的 element
- 實作程式碼
```c
bool q_delete_dup(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return false;
bool diff = false;
struct list_head *curr, *safe;
list_for_each_safe (curr, safe, head) {
struct list_head *next = curr->next;
element_t *curr_entry = list_entry(curr, element_t, list);
element_t *next_entry = list_entry(next, element_t, list);
if ((next != head) && (!strcmp(curr_entry->value, next_entry->value))) {
list_del(curr);
q_release_element(curr_entry);
diff = true;
} else if (diff) {
list_del(curr);
q_release_element(curr_entry);
diff = false;
}
}
return true;
}
```
### q_swap
交換佇列中鄰近的節點
- 實作流程
1. 例外處理
`q_swap` 是在進行節點兩兩對調的動作,因此佇列除了 `head` 指向的節點外,若沒有額外連接兩個以上的節點,就無法執行對調的動作,故先使用 `if` 判斷式做例外處理
2. 宣告指標
我宣告了兩個指標 `left` 與 `right` ,分別指向 `head->next` 跟 `head->next->next` ,方便之後進行節點對調的動作
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
rankdir=LR
e->1->2->3->4->5
5->4->3->2->1->e
5->e
e->5
h[shape=plaintext, label="head"];
l[shape=plaintext, label="left"];
r[shape=plaintext, label="right"];
h->e
l->1
r->2
}
```
3. `next` 與 `prev` 的重新指向
將 `head` 指向的節點改成與 2 號節點相鄰, 1 號節點接在 2 號節點的後面, 3 號節點再去與 1 號節點做 `next` 與 `prev` 的重新指向,最後成功對調 1 號與 2 號節點,此時 `right` 指向 2 號節點,而 `left` 則指向 1 號節點
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
rankdir=LR
e->2->1->3->4->5
5->4->3->1->2->e
5->e
e->5
h[shape=plaintext, label="head"];
l[shape=plaintext, label="left"];
r[shape=plaintext, label="right"];
h->e
l->1
r->2
}
```
4. 移動指標並重複步驟 3
使用判斷式判斷 `left->next` 跟 `left->next->next` 是否為 `head` 所指向的節點,若滿足其中一個,則跳出 `while` 迴圈,代表剩餘的節點數已經不足 2 個,無法進行對調的動作
如果 `left->next` 跟 `left->next->next` 都不是 `head` 所指向的節點,代表佇列尚未完全地完成兩兩對調,故移動指標並繼續進行步驟 3 的對調動作,直到跳出 `while` 迴圈的條件被滿足為止,最終結果如下圖
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
rankdir=LR
e->2->1->4->3->5
5->3->4->1->2->e
5->e
e->5
h[shape=plaintext, label="head"];
l[shape=plaintext, label="left"];
r[shape=plaintext, label="right"];
h->e
l->3
r->4
}
```
- 實作程式碼
```c
void q_swap(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *left = head->next;
struct list_head *right = head->next->next;
head->next = right;
right->next->prev = left;
left->next = right->next;
right->next = left;
right->prev = head;
left->prev = right;
while (left->next != head) {
if (left->next->next != head) {
right = left->next->next;
right->prev->next = right->next;
right->next->prev = right->prev;
right->next = right->prev;
right->prev->prev = right;
right->prev = left;
left->next = right;
left = right->next;
} else
break;
}
return;
}
```
### q_reverse
以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點
- 實作流程
1. 例外處理
`q_reverse` 的目的是要將佇列以反向順序重新排列 (除了 `head` 所指向的節點之外) ,因此至少要額外連接 2 個以上的節點,才可以執行此動作,故先排除掉可能發生的例外
2. 排列節點
`q_reverse` 可以使用 `list.h` 中的 `list_for_each_safe` 與 `list_move` 很簡單的達成,方法是透過 `list_for_each_safe` <s>遍歷</s> --->逐一走訪佇列,過程中用 `list_move` 不斷把節點移至開頭 (head) ,過程如下圖所示
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
rankdir=LR
e->1->2->3->4->5
5->4->3->2->1->e
5->e
e->5
h[shape=plaintext, label="head"];
h->e
{
rank=same
dummy[ shape = point, width = 0 ];
1
}
2:n->dummy:n[label="move to"]
}
```
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
rankdir=LR
e->2->1->3->4->5
5->4->3->1->2->e
5->e
e->5
h[shape=plaintext, label="head"];
h->e
{
rank=same
dummy[ shape = point, width = 0 ];
2
}
3:n->dummy:n[label="move to"]
}
```
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
rankdir=LR
e->3->2->1->4->5
5->4->1->2->3->e
5->e
e->5
h[shape=plaintext, label="head"];
h->e
{
rank=same
dummy[ shape = point, width = 0 ];
3
}
4:n->dummy:n[label="move to"]
}
```
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
rankdir=LR
e->4->3->2->1->5
5->1->2->3->4->e
5->e
e->5
h[shape=plaintext, label="head"];
h->e
{
rank=same
dummy[ shape = point, width = 0 ];
4
}
5:n->dummy:n[label="move to"]
}
```
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
rankdir=LR
e->5->4->3->2->1
1->2->3->4->5->e
1->e
e->1
h[shape=plaintext, label="head"];
h->e
}
```
- 實作程式碼
```c
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *node = NULL;
struct list_head *safe = NULL;
list_for_each_safe (node, safe, head)
list_move(node, head);
return;
}
```
### q_reverseK
以 k 個節點為小組 (k-group) 進行分組,並以反向順序重新排列小組內的鏈結串列
- 實作流程
1. 例外處理
`q_reverseK` 會進行反向順序的重新排列,因此除了 `head` 所指向的節點外,至少還要連接兩個以上的節點,才能執行排列的動作,故先做例外排除
同理, `k < 2` 無法執行排列的動作,因此也要做例外排除
2. `k == 2` 與 `k > 2` 的情形
`k == 2` 時 `q_reverseK` 的功能與 `q_swap` 相同,是在進行節點兩兩對調的動作,因此直接呼叫 `q_swap` 即可
若 `k > 2` ,我的作法是先數好總共要做幾次的反向順序重排,再由 `for` 迴圈執行相同的反向順序重排流程,其中變數 `count` 代表 `head` 以外的節點總數, `repeat_times` 則為反向順序重排流程所要執行的次數
3. k 個節點為小組進行反向順序重新排列
宣告指標 `sub_head` 與 `cut_pos` ,分別都指向 `head` ,它們之後會作為引數提供給 `list_cut_position` 跟 `list_splice_init` 使用
:::info
為了圖形美觀,環狀佇列頭尾相連的鏈結省略不畫
:::
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
d [label="Empty"]
rankdir=LR
e->1->2->3->4->5
5->4->3->2->1->e
h[shape=plaintext, label="head"];
s[shape=plaintext, label="sub_head"];
c[shape=plaintext, label="cut_pos"];
t[shape=plaintext, label="head_temp"];
h->e
s->e
c->e
t->d
}
```
假設 k = 3 ,則移動指標 `cut_pos` 至節點 3 的位置,並以 `list_cut_position` 將節點們移至另一個佇列
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
d [label="Empty"]
rankdir=LR
e->4->5
5->4->e
d->1->2->3
3->2->1->d
h[shape=plaintext, label="head"];
s[shape=plaintext, label="sub_head"];
c[shape=plaintext, label="cut_pos"];
t[shape=plaintext, label="head_temp"];
h->e
s->e
c->3
t->d
}
```
使用 `q_reverse` 將 `head_temp` 為頭端 (head) 的鏈結串列進行反向順序重新排列
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
d [label="Empty"]
rankdir=LR
e->4->5
5->4->e
d->3->2->1
1->2->3->d
h[shape=plaintext, label="head"];
s[shape=plaintext, label="sub_head"];
c[shape=plaintext, label="cut_pos"];
t[shape=plaintext, label="head_temp"];
h->e
s->e
c->3
t->d
}
```
使用 `list_splice_init` ,將 `head_temp` 為頭端 (head) 的鏈結串列除了 `head_temp` 指向的節點以外,其他的節點移回原佇列
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
d [label="Empty"]
rankdir=LR
e->3->2->1->4->5
5->4->1->2->3->e
h[shape=plaintext, label="head"];
s[shape=plaintext, label="sub_head"];
c[shape=plaintext, label="cut_pos"];
t[shape=plaintext, label="head_temp"];
h->e
s->e
c->3
t->d
}
```
最後移動指標 `sub_head` 與 `cut_pos` ,若 `( count / k ) > 1` ,則重複上述的流程
```graphviz
digraph{
node[shape=box];
e [label="Empty"]
d [label="Empty"]
rankdir=LR
e->3->2->1->4->5
5->4->1->2->3->e
h[shape=plaintext, label="head"];
s[shape=plaintext, label="sub_head"];
c[shape=plaintext, label="cut_pos"];
t[shape=plaintext, label="head_temp"];
h->e
s->1
c->1
t->d
}
```
- 實作程式碼
```c
void q_reverseK(struct list_head *head, int k)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
if (k < 2)
return;
else if (k == 2)
q_swap(head);
else {
int count = 0;
struct list_head *node = NULL;
list_for_each (node, head)
count++;
if (count < k)
return;
else {
int repeat_times = count / k;
LIST_HEAD(dummy);
struct list_head *head_temp = &dummy;
struct list_head *sub_head = head;
struct list_head *cut_pos = head;
for (int i = 0; i < repeat_times; i++) {
for (int j = 0; j < k; j++)
cut_pos = cut_pos->next;
list_cut_position(head_temp, sub_head, cut_pos);
q_reverse(head_temp);
list_splice_init(head_temp, sub_head);
for (int j = 0; j < k; j++)
sub_head = sub_head->next;
cut_pos = sub_head;
}
}
}
return;
}
```
### q_sort
以遞增順序來排序鏈結串列的節點
參考 < [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list) > 與 < [Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/list-merge-sort#Merge-Sort-%E8%88%87%E5%AE%83%E7%9A%84%E8%AE%8A%E5%8C%96) >
- 實作流程
1. 例外處理
除了 `head` 所指向的節點外,至少要連接 2 個以上的節點才能夠進行排序,因此做例外處理
2. Merge sort
參考 < [Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/list-merge-sort#Merge-Sort-%E8%88%87%E5%AE%83%E7%9A%84%E8%AE%8A%E5%8C%96) > ,先將鏈結串列尾端 (tail) 的 `next` 指向 `NULL` ,即斷開 `next` 方向的環狀結構,再使用撰寫好的 `merge_sort` 進行遞增順序的排序
3. 重新連接環狀串列
將 `merge_sort` 過程中切斷的 linked list 指標重新指向
- 實作程式碼
```c
void q_sort(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
head->prev->next = NULL;
head->next = merge_sort(head->next);
struct list_head *curr = head, *next = curr->next;
while (next) {
next->prev = curr;
curr = next;
next = next->next;
}
curr->next = head;
head->prev = curr;
}
```
### q_descend
若佇列中任意節點的右端存在嚴格大於 (strictly greater) 的值 (value) ,則將該節點移去 (remove)
參考 < [KHLee529](https://hackmd.io/@KHLee529/linux2023-lab0#q_descend) > 的實作
- 實作流程
1. 參考 < [KHLee529](https://hackmd.io/@KHLee529/linux2023-lab0#q_descend) > 同學的實作流程,他的作法是從佇列尾端 (tail) 往回比較 `element->value` 的大小,過程中不斷更新 (或者說紀錄) 擁有最大值的字串,並由 `max` 指向它,當碰到比 `max` 大小還要小的 `value` ,則將之移除 (remove and release)
2. 在迭代過程中使用整數變數 `total` 與 `n_del` 分別紀錄 element 總數和移除的 element 總數,最後傳回兩者之差,即為剩下的 element 總數
- 實作程式碼
```c
int q_descend(struct list_head *head)
{
char *max = NULL;
element_t *entry = NULL, *safe = NULL;
int total = 0, n_del = 0;
for (entry = list_entry(head->prev, element_t, list),
safe = list_entry(entry->list.prev, element_t, list);
&entry->list != head;
entry = safe, safe = list_entry(safe->list.prev, element_t, list)) {
total += 1;
if (!max || strcmp(entry->value, max) > 0) {
max = entry->value;
} else {
list_del(&entry->list);
q_release_element(entry);
n_del += 1;
}
}
return total - n_del;
}
```
### q_merge
將所有佇列合併 (merge) 成一個以遞增順序排列好的鏈結串列
參考 < [chiangkd](https://hackmd.io/@chiangkd/2023spring-lab0-c#q_merge) > 的實作
- 實作流程
1. 使用 `list_for_each_entry_safe` 逐一走訪完每個節點,每當存取到 `c_cont` 就斷開部分連結,並以 `mergeTwoLists` 將 `sorted` 與 `c_cont->q->next` 合併
2. 迭代完成後須復原環狀 doubly linked list 結構,因為過程中各個 linked list 的 `prev` 被 `mergeTwoLists` 打亂了,只有 `next` 的指向是正確的
3. 使用 `list_splice` 將處理完的鏈結串列交還給 `head` 所屬的 element
- 實作程式碼
```c
int q_merge(struct list_head *head)
{
// reference to @chiangkd
if (!head)
return 0;
queue_contex_t *c_cont;
queue_contex_t *n_cont;
struct list_head *sorted = NULL;
list_for_each_entry_safe (c_cont, n_cont, head, chain) { // iterate context
c_cont->q->prev->next = NULL;
c_cont->q->prev = NULL;
sorted = mergeTwoLists(sorted, c_cont->q->next);
INIT_LIST_HEAD(c_cont->q); // reconnect the lists which are moved and
// merged to "sorted" list;
}
LIST_HEAD(tmp);
struct list_head *t = &tmp;
t->next = sorted;
struct list_head *c = t;
while (sorted) {
sorted->prev = c;
c = sorted;
sorted = sorted->next;
}
c->next = t;
t->prev = c;
int size = q_size(t); // store size before splice to main queue
list_splice(t, list_first_entry(head, queue_contex_t, chain)->q);
return size;
}
```
### 額外撰寫的函式
#### merge_sort
將佇列從中間切斷,產生的兩個子佇列再個別切斷中間的鏈結,以此種方式迭代,直到切成單個節點為止,再以 `mergeTwoLists` 進行遞增順序的合併
```c
struct list_head *merge_sort(struct list_head *head)
{
if (!head || !head->next)
return head;
struct list_head *slow = head;
for (struct list_head *fast = head->next; fast && fast->next;
fast = fast->next->next) {
slow = slow->next;
}
struct list_head *mid = slow->next;
slow->next = NULL;
struct list_head *left = merge_sort(head), *right = merge_sort(mid);
return mergeTwoLists(left, right);
}
```
#### mergeTwoLists
將兩個串列以遞增的順序合併在一起
```c
struct list_head *mergeTwoLists(struct list_head *L1, struct list_head *L2)
{
struct list_head *head = NULL, **ptr = &head, **node = NULL;
while (L1 && L2) {
element_t *L1_entry = list_entry(L1, element_t, list);
element_t *L2_entry = list_entry(L2, element_t, list);
node = strcmp(L1_entry->value, L2_entry->value) < 0 ? &L1 : &L2;
*ptr = *node;
ptr = &(*ptr)->next;
*node = (*node)->next;
}
*ptr = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2);
return head;
}
```
---
### 研讀 Linux 核心原始程式碼 < [`list_sort.c`](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) >
:::info
作業要求
- [x] 研讀 Linux 核心原始程式碼的 lib/list_sort.c
- [x] 嘗試引入 Linux 核心原始程式碼到 lab0-c 專案
- [ ] 比較自己實作的 merge sort 和 Linux 核心程式碼之間效能落差
:::
#### 研讀 Linux 核心原始程式碼的 lib/list_sort.c
參考 < [Risheng1128](https://hackmd.io/@Risheng/linux2022-lab0#%E7%A0%94%E8%AE%80-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC-list_sortc) > 的 < [研讀 Linux 核心原始程式碼 list_sort.c](https://hackmd.io/@Risheng/list_sort#%E7%A0%94%E8%AE%80-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC-list_sortc) >
研讀原始程式碼 `list_sort.c` 發現主要是由三個函式 `merge()` 、 `merge_final()` 及 `list_sort()` 組成,接著要個別分析它們的作用
在分析之前,先探討函式屬性 `__attribute__((nonnull()))` 的作用,因為三個函式在宣告函式原型時都擁有這個屬性,參考 < [` __attribute__((nonnull)) ` function attribute](https://developer.arm.com/documentation/dui0375/g/Compiler-specific-Features/--attribute----nonnull---function-attribute) > 可以得知 `nonnull` 的作用是當特定函式的引數為 `NULL` 時,編譯器會發出警告訊息
>This function attribute specifies function parameters that are not supposed to be null pointers. This enables the compiler to generate a warning on encountering such a parameter.
##### `merge()`
:::spoiler `merge()` 原始碼
```lua=
__attribute__((nonnull(2,3,4)))
static struct list_head *merge(void *priv, list_cmp_func_t cmp,
struct list_head *a, struct list_head *b)
{
struct list_head *head, **tail = &head;
for (;;) {
/* if equal, take 'a' -- important for sort stability */
if (cmp(priv, a, b) <= 0) {
*tail = a;
tail = &a->next;
a = a->next;
if (!a) {
*tail = b;
break;
}
} else {
*tail = b;
tail = &b->next;
b = b->next;
if (!b) {
*tail = a;
break;
}
}
}
return head;
}
```
:::
- 引數 `cmp` 分析
資料型態為 `list_cmp_func_t` ,根據 < [ linux/include/linux/list_sort.h ](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h) > ,得知 `cmp` 是一個函式指標
```c
typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *,const struct list_head *, const struct list_head *);
```
`cmp` 指向回傳型態為 `int` 的函式,該函式的引數為 `void *` 和 2 個 `const struct list_head *`
- 宣告指標 ( line 5 )
宣告兩個指標,一個是指向 `struct list_head` 資料型態的指標 `head` ,另一個則是指向指標的指標 `tail` ,並且將它初始化指向 `head` 的地址
- for 迴圈 ( line 7 )
沒有設定判斷式,為無限迭代的迴圈,使用 `cmp` 來判斷下一個節點是要從 `a` 或者 `b` 來取得,假如 linked list `a` 已經沒有節點時,則接上 linked list `b` ,反之,假如 linked list `b` 已經沒有節點時,則接上 linked list `a`
##### `merge_final()`
將所有節點的 `prev` 接回前一個節點
```c
/* Finish linking remainder of list b on to tail */
tail->next = b;
do {
/*
* If the merge is highly unbalanced (e.g. the input is
* already sorted), this loop may run many iterations.
* Continue callbacks to the client even though no
* element comparison is needed, so the client's cmp()
* routine can invoke cond_resched() periodically.
*/
if (unlikely(!++count))
cmp(priv, b, b);
b->prev = tail;
tail = b;
b = b->next;
} while (b);
/* And the final links to make a circular doubly-linked list */
tail->next = head;
head->prev = tail;
```
##### `list_sort()`
首先閱讀此函式的註解
```c
* This mergesort is as eager as possible while always performing at least
* 2:1 balanced merges. Given two pending sublists of size 2^k, they are
* merged to a size-2^(k+1) list as soon as we have 2^k following elements.
*
* Thus, it will avoid cache thrashing as long as 3*2^k elements can
* fit into the cache. Not quite as good as a fully-eager bottom-up
* mergesort, but it does use 0.2*n fewer comparisons, so is faster in
* the common case that everything fits into L1.
```
由此可知,此 merge sort 總是等到 linked list 的長度比例為 2 : 1 時才會開始進行合併的動作,舉例來說,有兩個大小為 $2^k$ 的 linked list 時, merge sort 不會直接進行合併,而是會等到有第三個 $2^k$ 時,才開始合併,並且是以 $2^k+1$ 和 $2^k$ 兩個 linked list 進行合併,滿足前述的 2 : 1
若以這種方式進行 merge sort ,假如被合併的 linked list 都可以被放進 cache 裡,則可以避免 [Cache thrashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science)) 的問題
#### 引入 Linux 核心原始程式碼到 lab0-c 專案
參考 < [komark06](https://hackmd.io/@komark06/SyCUIEYpj#%E5%BC%95%E5%85%A5-liblist_sortc) > 的方法引入原始程式碼,並且加入新的 `qtest` 命令 `list_sort`
:::info
實作流程
1. 將檔案 lib/list_sort.c 加進 lab0-c
2. 將位於其他檔案的巨集函式加進 `list_sort.h`
3. 在 `qtest.c` 新增函式 `cmp()`
4. 修改 `qtest.c` 的 `do_sort()` 函式
:::
首先要在 lab0-c 專案裡面建立 `lsit_sort.c` 及 `lsit_sort.h` ,前者包含 Linux 核心原始程式碼,而後者則是包含 lab0-c 所缺少的必要定義
以下為 `lsit_sort.h` 的內容
```c
#ifndef _LIST_SORT_H
#define _LIST_SORT_H
#include <stdint.h>
#include "list.h"
#define USE_LINUX_SORT 1
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *);
void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp);
#endif /* End of _LIST_SORT_H_ */
```
接著在 Makefile 中的 `OBJS` 新增 `list_sort.o`
```c
OBJS := qtest.o report.o console.o harness.o queue.o list_sort.o\
random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \
shannon_entropy.o \
linenoise.o web.o
```
在 `qtest.c` 上建立函式 `cmp()`
```c
__attribute__((nonnull(2,3)))
int cmp(void *priv, const struct list_head *list1, const struct list_head *list2)
{
element_t *list1_entry = list_entry(list1, element_t, list);
element_t *list2_entry = list_entry(list2, element_t, list);
return strcmp(list1_entry->value, list2_entry->value) < 0 ? 0 : 1;
}
```
---
## 運用 Valgrind 排除 qtest 實作時的記憶體錯誤
:::info
作業要求
- [x] 運用 Valgrind 排除 qtest 實作時的記憶體錯誤,並且使用 Massif 進行視覺化記憶體使用情況的分析
:::
當 `queue.c` 的實作基本上完成後,執行 `$ make valgrind` 進行動態分析,發現 `trace-13-malloc.cmd` 沒有通過測試,因此針對這個檔案進行分析,使用命令 `valgrind -q --leak-check=full ./qtest -f traces/trace-13-malloc.cmd` 對檔案 `trace-13-malloc.cmd` 進行測試
:::spoiler 測試結果
```c
# Test of malloc failure on new
WARNING: Malloc returning NULL
l = NULL
l = []
WARNING: Malloc returning NULL
l = NULL
l = []
l = []
l = []
==76976== 32 bytes in 1 blocks are still reachable in loss record 1 of 3
==76976== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==76976== by 0x10CBCE: do_new (qtest.c:145)
==76976== by 0x10DDFA: interpret_cmda (console.c:181)
==76976== by 0x10E3AF: interpret_cmd (console.c:201)
==76976== by 0x10E7B0: cmd_select (console.c:610)
==76976== by 0x10F09C: run_console (console.c:705)
==76976== by 0x10D1EC: main (qtest.c:1223)
==76976==
==76976== 160 bytes in 5 blocks are possibly lost in loss record 2 of 3
==76976== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==76976== by 0x10CBCE: do_new (qtest.c:145)
==76976== by 0x10DDFA: interpret_cmda (console.c:181)
==76976== by 0x10E3AF: interpret_cmd (console.c:201)
==76976== by 0x10E7B0: cmd_select (console.c:610)
==76976== by 0x10F09C: run_console (console.c:705)
==76976== by 0x10D1EC: main (qtest.c:1223)
==76976==
==76976== 224 bytes in 4 blocks are still reachable in loss record 3 of 3
==76976== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==76976== by 0x10F110: test_malloc (harness.c:133)
==76976== by 0x10F515: q_new (queue.c:23)
==76976== by 0x10CC07: do_new (qtest.c:149)
==76976== by 0x10DDFA: interpret_cmda (console.c:181)
==76976== by 0x10E3AF: interpret_cmd (console.c:201)
==76976== by 0x10E7B0: cmd_select (console.c:610)
==76976== by 0x10F09C: run_console (console.c:705)
==76976== by 0x10D1EC: main (qtest.c:1223)
==76976==
```
:::
從錯誤訊息中發現有 still reachable 類型的記憶體遺失情形發生,且不是在 `queue.c` 實作中造成的,因此根據錯誤訊息的追隨路徑,開始檢查 `qtest.c` 中有無不合理的操作,但找了許久還是沒有頭緒,參考了 [KHLee529](https://hackmd.io/@KHLee529/linux2023-lab0#Valgrind) 同學的筆記後,才發現 `qtest.c` 中的 `q_quit` 敘述第一行為 `return true;` ,也就是說這個函式第一行以後的敘述毫無作用,很明顯語意上有誤
> still reachable: 程式結束時有尚未釋放的記憶體,且還被指標所指向,通常會發生在全域變數
:::spoiler `q_quit`
```diff
static bool q_quit(int argc, char *argv[])
{
- return true;
report(3, "Freeing queue");
if (current && current->size > BIG_LIST_SIZE)
set_cautious_mode(false);
if (exception_setup(true)) {
struct list_head *cur = chain.head.next;
while (chain.size > 0) {
queue_contex_t *qctx, *tmp;
tmp = qctx = list_entry(cur, queue_contex_t, chain);
cur = cur->next;
q_free(qctx->q);
free(tmp);
chain.size--;
}
}
exception_cancel();
set_cautious_mode(true);
size_t bcnt = allocation_check();
if (bcnt > 0) {
report(1, "ERROR: Freed queue, but %lu blocks are still allocated",
bcnt);
return false;
}
return true;
}
```
:::
當我將錯誤的語意清除後重新執行 `valgrind -q --leak-check=full ./qtest -f traces/trace-13-malloc.cmd` ,先前 still reachable 類型的記憶體遺失情形也隨之解決了
:::spoiler 測試結果
```c
# Test of malloc failure on new
l = []
l = []
WARNING: Malloc returning NULL
l = NULL
WARNING: Malloc returning NULL
l = NULL
WARNING: Malloc returning NULL
l = NULL
l = []
Freeing queue
```
:::
使用圖形化工具 < [Massif](https://valgrind.org/docs/manual/ms-manual.html) > 來查看執行 `trace-13-malloc.cmd` 時記憶體的分配狀況,輸入命令 `valgrind -v --tool=massif ./qtest -f traces/trace-13-malloc.cmd` ,產生了檔案 `massif.out.85066` ,接著使用 massif-visualizer 打開檔案,輸入命令 `massif-visualizer ./massif.out.85066` ,最終結果為下圖
![](https://i.imgur.com/ma1dLAR.png)
從動態記憶體配置的圖形發現,佔用的記憶體最終都<s>歸零</s> --->歸還了,可見 `qtest` 在 `trace-13-malloc.cmd` 執行結束後,過程中佔用的記憶體都已成功被釋放
:::warning
注意用語,請查詢辭典,理解「歸零」的意涵是否可對應到本例 malloc/free 操作。
:notes: jserv
:::
:::success
歸零:將儀表或計量裝置的指示值調至零點或起點
本例的操作是對記憶體進行分配與釋放 (歸還) ,記憶體總量沒有減少,因此不適合用「歸零」來描述,已修正錯誤的詞彙使用,謝謝教授!
:cat2: ccasdqwe
:::
---