# 2024q1 Homework1 (lab0)
contributed by < [LambertWSJ](https://github.com/LambertWSJ) >
==[作業說明](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-a)==
# 實作 Queue 介面
## `q_insert_head` / `q_insert_tail`
將節點插入到 queue 的頭尾。可以利用 doubly linked list 的特性簡化成傳入 queue 頭尾的節點,之後再用 list_add 插入新節點。
`list_add(node, head)`: node 插入到 head 後面
```c
static inline bool q_insert_node(struct list_head *head, char *s)
{
element_t *el = malloc(sizeof(element_t));
if (!el)
return false;
el->value = strdup(s);
if (!el->value) {
free(el);
return false;
}
list_add(&el->list, head);
return true;
}
```
假設 queue 有 aa, bb, cc 三個節點,要新增的節點為 dd,如下所示
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
node1[label=aa]
node2[label=bb]
node3[label=cc]
node4[label=dd]
subgraph cluster1 {
rankdir=LR
color=none
head->node1
node1->node2
node2->node3
}
}
```
插入開頭 `list_add(dd, head)`,將 dd 插入到 head 後方,效果相當於 `list_add_tail(dd, aa)`
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
node1[label=aa]
node2[label=bb]
node3[label=cc]
node4[label=dd]
subgraph cluster1 {
rankdir=LR
color=none
head->node4
node4->node1
node1->node2
node2->node3
}
}
```
由於大部分的操作相同,所以只需要 `q_insert_node` 來處裡插入頭尾的函式即可。
```c
/* Insert an element at head of queue */
bool q_insert_head(struct list_head *head, char *s) {
return q_insert_node(head, s);
}
/* Insert an element at tail of queue */
bool q_insert_tail(struct list_head *head, char *s) {
return q_insert_node(head->prev, s);
}
```
## `q_remove_head` / `q_remove_tail`
從 queue 的頭尾移除節點也能夠簡化成只使用一個函式來處理,利用雙向串列的特性傳入頭跟尾的 list_head 即可,從串列拿掉 `node` 後,再把 `el->value` 複製到 sp 上用來比對答案。
```c
static inline element_t *q_remove_node(struct list_head *node,
char *sp,
size_t bufsize) {
if (list_empty(node))
return NULL;
element_t *el = list_entry(node, element_t, list);
list_del(node);
if (sp && el->value) {
size_t len = strlen(el->value) + 1;
len = len < bufsize ? len : bufsize;
strncpy(sp, el->value, len);
sp[len - 1] = '\0';
}
return el;
}
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) {
return q_remove_node(head->next, sp, bufsize);
}
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) {
return q_remove_node(head->prev, sp, bufsize);
}
```
## `q_reverse` / `q_reverseK`
- `q_reverse`
串列反轉的操作較為簡單,只需要將每個節點移動到 head 前面即可達成。這裡要使用 safe 版的 for 迴圈,確保節點在移動後, node 可以取得下一個節點,否則永遠會停留在第一個跟第二個節點,不斷的在循環。
```c
void q_reverse(struct list_head *head) {
struct list_head *node, *safe;
list_for_each_safe (node, safe, head) {
list_move(node, head);
}
}
```
如果在 safe 版的 for 迴圈中使用 list_move_tail 會發生什麼事? 那只會不斷的把當前的節點,往後面移動,那麼 node 將永遠都到不了 head,一樣會遇到無窮迴圈。
上述的迴圈可以輕鬆改寫成遞迴,不過反轉的節點太多會導致 stack overflow
```c
static void do_recur_reverse(struct list_head *node, struct list_head *head)
{
if (node == head)
return;
struct list_head *safe = node->next;
list_move(node, head);
do_recur_reverse(safe, head);
}
void q_reverse_recur(struct list_head *head)
{
do_recur_reverse(head->next, head);
}
```
除了單向的實做,亦可以利用雙向鍊結串列的特性,頭尾兩兩交換,概念類似一樣的技巧用在陣列反轉。
list_for_each_safe 由上到下的判斷式,依序考慮下面兩種 case
1. 節點數量為偶數個時,則必然會遇到 cur 的下一個節點就會是 last,表示其它節點已經兩兩交換完畢,剩下彼此還沒交換位置。
2. 節點數量為奇數個時,則 cur 與 last 必然會重疊,重疊時表示所有節點都已交換完畢,可以結束迴圈。
```c
void q_reverse_bidir(struct list_head *head)
{
struct list_head *cur, *safe;
struct list_head *last = head->prev, *last_safe;
list_for_each_safe (cur, safe, head) {
last_safe = last->prev;
if (cur->next == last) {
list_swap(cur, last);
break;
} else if (cur == last)
break;
list_swap(cur, last);
last = last_safe;
}
}
```
其中 `list_swap` 的實做如下,考慮到可能是前後節點交換,所以 e2 和 e1 交換完後,可以檢查 prev 是不是 e1,如果是的話表示 e1 與 e2 是鄰近節點,e2 搬動完後也就完成交換了,不必再做一次 `list_move`。
```c
static void list_swap(struct list_head *e1, struct list_head *e2)
{
struct list_head *prev = e2->prev;
list_move(e2, e1);
if (prev != e1)
list_move(e1, prev);
}
```
- [ ] console.c
三種不同的實做要拿來測試的話,可以在加入 option 變數來決定使用哪個實做來進行測試
```c
int rev_opt = 0;
void init_cmd()
{
/* ... */
add_param("rev_opt", &rev_opt,
"chose reverse implemntation"
", 0 for list_move method(default)"
", 1 for bi-direction reverse"
", 2 for recursive reverse(may stack overflow)",
NULL);
}
```
- [ ] qtest.c
有了 option 後,就能夠透過 rev_opt 來查表選擇實做,這裡使用 typedef 簡化函式指標,用來提昇程式碼的可讀性
```diff
typedef void (*queue_op)(struct list_head *head);
queue_op rev_tlb[] = {q_reverse, q_reverse_bidir, q_reverse_recur};
queue_op reverse_op = q_reverse;
static bool do_reverse(int argc, char *argv[])
{
/* .... */
+ if (rev_opt < 3)
+ reverse_op = rev_tlb[rev_opt];
+ else {
+ printf(
+ "unknown reverse option: %d\n"
+ "rev_opt only support follow option number\n"
+ "0 for list_move method(default)\n"
+ "1 for bi-direction reverse\n"
+ "2 for recursive reverse(may stack overflow)\n",
+ rev_opt);
+ trigger_exception("Please chose option carefully");
+ }
+ if (current && exception_setup(true))
- q_reverse(current->q);
+ reverse_op(current->q);
}
```
- `q_reverseK`
題目大意為每 k 個節點就將串列反轉。
實做時,卡在 reverse 後,怎麼指向下個 head? 去年參考 [komark06](https://hackmd.io/@komark06/SyCUIEYpj) 的寫法後,才知道指向 safe 的前一個節點當做 head。
直觀的做法就是 `anchor` 來記錄要反轉的子串列的頭,再用另一個變數去紀錄迴圈是不是走了 k 次,是的話就將走過的這一段串列反轉,反轉的作法是將走過 k 的一段串列切割出來給 `q_reverse` 反轉,完畢後再放回去。
`anchor` 接上反轉後的子串列,要將 `anchor` 更新到 `safe` 的前一個節點,當作下一輪反轉的子串列的 `list_head`。
有了不同的反轉實做,就能夠將 reverse_op 帶入到 q_reverseK。
```diff
void q_reverseK(struct list_head *head, int k) {
if (!head || k <= 1) {
return;
}
struct list_head *node, *safe, *anchor;
LIST_HEAD(rev_head);
anchor = head;
int i = 0;
list_for_each_safe (node, safe, head) {
i++;
if (i == k) {
list_cut_position(&rev_head, anchor, node);
- q_reverse(&rev_head);
+ reverse_op(&rev_head);
list_splice_init(&rev_head, anchor);
anchor = safe->prev;
i = 0;
}
}
}
```
## `q_ascend` / `q_descend`
題目大意為刪掉串列的解點,使串列的值從右手邊觀察是嚴格遞增。單向串列的方法是透過遞迴讓 head 走訪到最後一個節點再比大小,因為題目要嚴格遞增,從最後一個節點開始記錄最大值,如果節點的值比最大值還要小就刪掉,比最大值還大就紀錄節點的內涵值,遞迴結束後就可以得到題目要求的串列。
我們可以利用雙向串列的特點,反過來走訪串列,並重複上述比大小的過程,就不用寫成遞迴了,也避開了 stack overflow 的風險。
由於是從從右手邊觀察,又要刪掉節點,所以用 macro 把迴圈簡化成 `list_for_each_safe_reverse`
```c
#define list_for_each_safe_reverse(node, safe, head) \
for (node = head->prev, safe = node->prev; node != head; \
node = safe, safe = node->prev)
int q_descend(struct list_head *head) {
if (list_empty(head))
return 0;
char *maxv = "\0";
int sz = 0;
struct list_head *cur, *safe;
list_for_each_safe_reverse(cur, safe, head) {
sz++;
element_t *entry = list_entry(cur, element_t, list);
if (strcmp(entry->value, maxv) > 0) {
maxv = entry->value;
} else {
list_del(cur);
q_release_element(entry);
sz--;
}
}
return sz;
}
```
與 `q_descend` 解法一樣,差別在於改成從左手邊觀察,所以只要把迴圈換成 `list_for_each_safe` 即可。
```c
int q_ascend(struct list_head *head) {
if (list_empty(head))
return 0;
char *maxv = "\0";
int sz = 0;
struct list_head *cur, *safe;
list_for_each_safe (cur, safe, head) {
sz++;
element_t *entry = list_entry(cur, element_t, list);
if (strcmp(entry->value, maxv) > 0) {
maxv = entry->value;
} else {
list_del(cur);
q_release_element(entry);
sz--;
}
}
return sz;
}
```
## `q_swap`
將串列的節點兩兩交換,使用 for each 走訪串列,將下個節點移動到當前節點的尾,如果下個節點是 head 就停止,head 不是用來交換的而是用來找到串列的本體。
```c
void q_swap(struct list_head *head) {
struct list_head *node;
list_for_each (node, head) {
if (node->next == head)
break;
list_move_tail(node->next, node);
}
}
```
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
node1
node2
node3
node4
node5
cur_node[shape=oval,label="node"]
cur_node->node1
subgraph cluster1 {
rankdir=LR
color=none
head->node1
node1->node2
node2->node3
node3->node4
node4->node5
}
}
```
呼叫 `list_move(node->next, node)`,node2 跟 node1 交換
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
node1
node2
node3
node4
node5
cur_node[shape=oval,label="node"]
cur_node->node1
subgraph cluster1 {
rankdir=LR
color=none
head->node2
node2->node1
node1->node3
node3->node4
node4->node5
}
}
```
下一輪迴圈 node 指向 node3,依此類推將 node3 跟 node4 交換,直到 node 的下個節點是 head 就停下。
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
node1
node2
node3
node4
node5
cur_node[shape=oval,label="node"]
cur_node->node3
subgraph cluster1 {
rankdir=LR
color=none
head->node2
node2->node1
node1->node3
node3->node4
node4->node5
}
}
```
實做 q_reverse 的過程中,也順手實做了 `list_swap`,可以用來替代 `list_move_tail`,暫時沒想到好的函式名稱,先命名為 `q_swap2`
```c
void q_swap2(struct list_head *head)
{
struct list_head *node;
list_for_each (node, head) {
if (node->next == head)
break;
list_swap(node->next, node);
}
}
```
## `q_delete_mid`
刪除 queue 中間的節點,可以使用快慢指標的技巧來尋找中間的節點再刪除即可。
```c
bool q_delete_mid(struct list_head *head) {
struct list_head *fast = head->next, *node;
list_for_each (node, head) {
if (fast->next == head || fast->next->next == head)
break;
fast = fast->next->next;
}
list_del(node);
q_release_element(list_entry(node, element_t, list));
return true;
}
```
## `q_delete_dup`
從單向串列的解法改寫過來,如果遇到是重複的就刪除掉節點,並將 dup 設為 true,如果當前節點是連續節點的最後一個,就用 dup 來確認是不是最後一個連續節點,是的話就刪除,由於刪除會拿掉節點,所以要用 safe 版的 for each 迴圈。
```c
bool q_delete_mid(struct list_head *head)
{
struct list_head *fast = head->next, *node;
list_for_each (node, head) {
if (fast->next == head || fast->next->next == head)
break;
fast = fast->next->next;
}
list_del(node);
q_release_element(list_entry(node, element_t, list));
return true;
}
```
## `q_sort`
按照註解描述會有遞增與遞減排序的 list,因此要加入對應的 compare function 到排序演算法。
排序的實做選擇合併排序,除了實做上較容易外,合併排序屬於穩定排序,在最差的情況下的時間複雜度扔有 $O(logN)$ 的表現。
參考 [2021 的測驗題](https://hackmd.io/@sysprog/linux2021-quiz2) 修改的遞迴版 merge sort。
```c=
void q_sort(struct list_head *head, bool descend)
{
if (list_empty(head) || list_is_singular(head))
return;
struct list_head *fast = head->next, *slow;
list_for_each (slow, head) {
if (fast->next == head || fast->next->next == head)
break;
fast = fast->next->next;
}
LIST_HEAD(sorted);
LIST_HEAD(left);
list_cut_position(&left, head, slow);
q_sort(&left, descend);
q_sort(head, descend);
merge_two_lists(&left, head, &sorted, descend);
list_splice_tail(&sorted, head);
}
```
5 ~ 10 行:divide 階段,將串列分割成左右兩半。用快慢指標的技巧找出找出左半邊串列的結尾 `slow`。
11 ~ 13 行:將 `head` 到 `slow` 的串列用 `list_cut_position` 切來並用 `left` 作為開頭的 list_head,此時 head 為右半邊的串列開頭。
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
left
slow
node1
node2
node3
node4
node5
node6
slow->node3
subgraph cluster1 {
rankdir=LR
color=none
head->node2
node2->node1
node1->node3
node3->node4
node4->node5
node5->node6
}
}
```
`list_cut_position(&left, head, slow)` 分割完後如下圖所示
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
left
node1
node2
node3
node4
node5
node6
subgraph cluster1 {
rankdir=LR
color=none
head->node4
node4->node5
node5->node6
// node6->head
}
subgraph cluster2 {
rankdir=LR
color=none
left->node1
node1->node2
node2->node3
// node3->sorted
}
}
```
14 ~ 18:呼叫 q_sort 將串列分割成 sorted lists 後,用 `merge_tow_lists` 把兩個串列合併到 `sorted` 上。
### `merge_two_lists`
`merge_two_lists` 則參考過去使用間接指標的實做,改成 list API 的版本。只要 `l1` 和 `l2` 還有節點,就用 compare function 決定下個要合併的節點移動到 `sorted` 後面,當其中一個串列合併完了就把令一個串列拼接在 sorted 後面。
依據 `descend` 判斷遞增與遞減,可以建表來紀錄兩者對應的比較函式,用以取代三元表達式(`?:`)的分支判斷。
```c
static int (*sort_cmp[])(struct list_head *l1, struct list_head *l2) = {
[true] = dec_cmp,
[false] = asc_cmp,
};
static void merge_two_lists(struct list_head *l1,
struct list_head *l2,
struct list_head *sorted,
bool descend)
{
struct list_head *next, **node;
int (*cmp)(struct list_head *l1, struct list_head *l2) = sort_cmp[descend];
for (node = NULL; !list_empty(l1) && !list_empty(l2); *node = next) {
node = cmp(l1, l2) ? &l1->next : &l2->next;
next = (*node)->next;
list_move_tail(*node, sorted);
}
list_splice_tail_init(list_empty(l1) ? l2 : l1, sorted);
}
```
為什麼最後要使用 `list_splic_tail_init` 而不是 `list_splic_tail` ?
`list_splice_tail(list, head)` 的註解表示 `list` 開頭的 head 會被加入到 `head` 這個串列,所以後面要使用到 list 的話要初始化它的 head。
>All nodes from @list are added to to the end of the list of @head.
It is similar to list_add_tail but for multiple nodes. The @list head is not
modified and has to be initialized to be used as a valid list head/node
again.
假設最後要和 sorted 合併的串列為 l2,而 l2 在 q_sort 的傳入的開頭為 head
`qsort` $\to$ `merge_two_lists(&left, head, &sorted, descend)`
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
node1
node2
node3
node4
node5
sorted
node6
subgraph cluster1 {
rankdir=LR
color=none
head->node4
node4->node5
node5->node6
}
subgraph cluster2 {
rankdir=LR
color=none
sorted->node1
node1->node2
node2->node3
}
}
```
呼叫 `list_splice_tail(head, sorted)`,`head` 並沒有被拿掉,而是留在 `sorted` 串列裡面。
這導致之後呼叫 `merge_two_lists` 的時候,會陷入無窮迴圈,迴圈判斷式 `list_empty(head)` 永遠為 false,因為 `head->next` 永遠指不到自己。
```graphviz
digraph {
rankdir=LR
node[shape=box]
head
node1
node2
node3
node4
node5
sorted
node6
subgraph cluster2 {
rankdir=LR
color=none
sorted->node1
node1->node2
node2->node3
node3->node4
node4->node5
node5->node6
node6->sorted
head->node4
head->node6
}
}
```
比較函式使用 strcmp 來比較字串,依據手冊的 [strcmp](https://man7.org/linux/man-pages/man3/strcmp.3.html) 提到回傳值的三種情況
>• 0, if the s1 and s2 are equal;
>• a negative value if s1 is less than s2;
>• a positive value if s1 is greater than s2.
若 descend 為 true,字串要由大到小排序,也就是 s2 會優先加入到 `sorted` 串列裡面,因此 l2_val 比 l1_val 大時要回傳 1,反之則回傳 0,ascend 也依此類推。
```c
static int dec_cmp(struct list_head *l1, struct list_head *l2)
{
char *l1_val = list_first_entry(l1, element_t, list)->value;
char *l2_val = list_first_entry(l2, element_t, list)->value;
return strcmp(l1_val, l2_val) <= 0 ? 0 : 1;
}
static int asc_cmp(struct list_head *l1, struct list_head *l2)
{
char *l1_val = list_first_entry(l1, element_t, list)->value;
char *l2_val = list_first_entry(l2, element_t, list)->value;
return strcmp(l1_val, l2_val) > 0 ? 0 : 1;
}
```
觀察上述比較函式的實,最後扔然要做分支判斷,該如何消除?
判斷式以 **0** 為基準,且 `strcmp` 為回傳**有號數**,可以利用 sign bit 即 MSB 來判斷數值是否為負數,在使用 not operator(`!`)轉成 0 和 1,以減少 merge sort 的分支數量。
```diff
+#define MSB(val) ((val) & 0x80000000)
static int dec_cmp(struct list_head *l1, struct list_head *l2)
{
char *l1_val = list_first_entry(l1, element_t, list)->value;
char *l2_val = list_first_entry(l2, element_t, list)->value;
- return strcmp(l1_val, l2_val) <= 0 ? 0 : 1;
+ return !MSB(strcmp(l1_val, l2_val));
}
static int asc_cmp(struct list_head *l1, struct list_head *l2)
{
char *l1_val = list_first_entry(l1, element_t, list)->value;
char *l2_val = list_first_entry(l2, element_t, list)->value;
- return strcmp(l1_val, l2_val) > 0 ? 0 : 1;
+ return !!MSB(strcmp(l1_val, l2_val));
}
```
為了之後引入 kernel 的 list_sort,顯然不能使用只有兩個參數的比較函式,合併時也不能僅用 true 和 false 來決定下個要合併哪個節點,而是要和 0 做比較,應此將 compare 函式修改成如下。
```c
static int asc_cmp(void *priv,
const struct list_head *l1,
const struct list_head *l2)
{
char *s1 = list_entry(l1, element_t, list)->value;
char *s2 = list_entry(l2, element_t, list)->value;
return strcmp(s1, s2);
}
static int dec_cmp(void *priv,
const struct list_head *l1,
const struct list_head *l2)
{
return asc_cmp(priv, l2, l1);
}
```
手冊 [strcmp(3)](https://man7.org/linux/man-pages/man3/strcmp.3.html) 提到下方描述
>strcmp() returns an integer indicating the result of the
comparison, as follows:
• 0, if the s1 and s2 are equal;
• a negative value if s1 is less than s2;
• a positive value if s1 is greater than s2.
回傳值與兩個數值相減來判斷誰大誰小的概念一樣,所以 `strcmp(s1, s2)` 回傳 -5,那麼 s1 與 s2 調過來 `strcmp(s2, s1)` 就會回傳 5,可依此特性簡化升序與降序排列,合併函式就會依照正負數對調合併的節點。
合併函式的函式指標型態改成和 kernel 的 list_sort 的 cmp 一樣,不過傳入到比較函式會變成 next,如果拿原本的比較函式,除了結果不正確,用在 list_sort 還會造成 Segementation fault。
```c
static void merge_two_lists(struct list_head *l1,
struct list_head *l2,
struct list_head *sorted,
bool descend)
{
struct list_head *next, **node;
list_cmp_func_t cmp = sort_cmp[descend];
for (node = NULL; !list_empty(l1) && !list_empty(l2); *node = next) {
node = cmp(NULL, l1->next, l2->next) <= 0 ? &l1->next : &l2->next;
next = (*node)->next;
list_move_tail(*node, sorted);
}
list_splice_tail_init(list_empty(l1) ? l2 : l1, sorted);
}
```
## `q_merge`
改寫自 [yanjiew1](https://hackmd.io/@yanjiew/linux2023q1-lab0) 的解法,利用雙向 linked list 的特性,每次迭代的時候,將頭跟尾串接到 sorted 串列上,如果頭跟尾是同一個,表示已經走完串列了,就把頭拼接到 sorted 上(選擇尾巴也可以),合併完之後在透過 `q_sort` 合併成一條 sorted list。最後把 sorted 的串列接到 head 上。
```c
int q_merge(struct list_head *head, bool descend)
{
if (!head)
return 0;
LIST_HEAD(sorted);
queue_contex_t *cur = NULL;
queue_contex_t *last = list_last_entry(head, queue_contex_t, chain);
list_for_each_entry (cur, head, chain) {
if (cur == last) {
list_splice_init(cur->q, &sorted);
break;
}
list_splice_init(cur->q, &sorted);
list_splice_init(last->q, &sorted);
last = list_entry(last->chain.prev, queue_contex_t, chain);
}
q_sort(&sorted, descend);
int size = q_size(&sorted);
list_splice_init(&sorted, list_first_entry(head, queue_contex_t, chain)->q);
return size;
}
```
---
`make test` 有時後會 95/100 有時候滿分,要開始讀論文才能把統計執行時間的函式修正確。
```
+++ TESTING trace trace-17-complexity:
# Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant
ERROR: Probably not constant time or wrong implementation
ERROR: Probably not constant time or wrong implementation
Probably constant time
Probably constant time
--- trace-17-complexity 0/5
--- TOTAL 95/100
make: *** [Makefile:60: test] Error 1
```
---
# 引入 list_sort.c
教材已寫清楚程式碼大部分的運作原理,搭配原始碼可以有效率的看懂程式碼,所以這裡改成做實驗還有驗證 paper
<!--
TODO:
study paper and experiment
和自己實做的 merge sort 做比較
-->
## 簡化程式碼,觀察輸出的指令
待整理
`merge`
原本的 merge 函式可以透過 indirect pointer 簡化原本 if-else 區塊相同操作的部份,其實就像在做 [merge two sorted lists](https://leetcode.com/problems/merge-two-sorted-lists/) 一樣
```c
__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)
{
/* cppcheck-suppress unassignedVariable */
struct list_head *head, **tail = &head;
struct list_head *node;
for (;;) {
/* if equal, take 'a' -- important for sort stability */
struct list_head **indir = cmp(priv, a, b) <= 0 ? &a : &b;
node = *indir;
*tail = node;
tail = &node->next;
*indir = node->next;
if (!node->next) {
break;
}
}
*tail = (struct list_head *) ((uintptr_t) a | (uintptr_t) b);
return head;
}
```
`merge_final`
```c
__attribute__((nonnull(2, 3, 4, 5))) static void merge_final(
void *priv,
list_cmp_func_t cmp,
struct list_head *head,
struct list_head *a,
struct list_head *b)
{
struct list_head *tail = head;
struct list_head *node;
u8 count = 0;
for (;;) {
/* if equal, take 'a' -- important for sort stability */
struct list_head **indir = cmp(priv, a, b) <= 0 ? &a : &b;
node = *indir;
tail->next = node;
node->prev = tail;
tail = node;
*indir = node->next;
if (!node->next)
break;
}
node = (struct list_head *) ((uintptr_t) a | (uintptr_t) b);
/* Finish linking remainder of list b on to tail */
tail->next = node;
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, node, node);
node->prev = tail;
tail = node;
node = node->next;
} while (node);
/* And the final links to make a circular doubly-linked list */
tail->next = head;
head->prev = tail;
}
```
<!-- GCC 編譯兩個版本的輸出的指令一模一樣, -->
## 使用 GDB 觀察排序過程
[lab0 - :loudspeaker: Linux 核心的鏈結串列排序](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-e#%E4%BD%95%E6%99%82%E5%90%88%E4%BD%B5) 何時合併的表格整理變數 count 在各階段時 pending 上的節點數量,如果是自己做實驗該如何得知 pending 每個子串列的數量,還有其節點的內含值?
首先想到的就是 codegen 生成 graphviz script,但是這麼做要在 list_sort.c 加入很多程式碼又要額外處裡字串問題,寫完後除了驗證正確性外,還要用 `#ifdef` 覆蓋額外的程式碼,導致原本乾淨有品味的程式碼被我弄得一團亂。
要解決上述問題,可以使用強大的 GDB 搭配 [Python API](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Python-API.html) 在執行時期拿到 pending 串列,並用 Python 完成 codegen、存檔和畫圖,既避免弄髒程式碼又能夠練習 GDB。
靈感來自[手写红黑树, 再用gdb画给你看](https://www.bilibili.com/video/BV1Pw411S7Ea/?spm_id_from=333.337.search-card.all.click&vd_source=fd781c26b2abd26ec39acdc116130ae6)
### 新增 dump_sort 命令到 GDB
dump_sort 用來做這次實驗的命令,格式如下
>dump_sort list [-o *png_path*] [-type *list_type*]
`list`(必填) : 傳入要觀察的串列開頭,預設是當作都傳入 pending 串列,也就是 prev 指向下個子串列開頭,next 指向子串列的下個節點,另一種則是原本的雙向鍊結串列
`-type`(可忽略): pending 和 origin 兩種類型,如上所述
`-o`(可忽略): 輸出的檔案名稱,如果有包含資料夾的路徑在裡面,會自動建立資料夾。只會輸出 png 檔,所以無論傳入的副檔名是 jpg 還是 raw 都會被改為 png
完整程式碼請參考 [dump_sort.py](https://github.com/LambertWSJ/lab0-c/blob/master/scripts/dump_sort.py)。
要添加 gdb command 大致流程如下
- 建立繼承 gdb.Command 的 class
- 物件初始化時傳給父類別 command 名稱,還有 command 類型,若沒有特別指定命令只能在什麼情況下用 (e.g. TUI),可以傳入 gdb.COMMAND_USER
- 實做 gdb.Command 的 invoke 介面。GDB 使用到自定義的命令時會呼叫到這個函式。
>This method is called by GDB when this command is invoked.
[CLI Commands In Python (Debugging with GDB)](https://sourceware.org/gdb/current/onlinedocs/gdb.html/CLI-Commands-In-Python.html)
這次 demo 只用到 gdb 函式庫的 `parse_and_eval` 和 `execute`,兩者都會讓 GDB 執行傳入的字串,但是前者會回傳結果,後者一般情況下不會有回傳值,除非 `to_string=True` 才會回傳結果。
考量到程式的可讀性,要取值時 `parse_and_eval`,其它都用 `execute`,具體使用方法如下程式碼的第 21 行走訪 pending 串列開始。
先看 26 行,從 pend 拿到子串列 list 的開頭後走訪子串列,節點的內含值使用包裝過 `container_of` 的 [node_val](https://github.com/LambertWSJ/lab0-c/blob/master/list_sort.c#L11) 取得,31 行執行完後回傳的是 `element_t` 的開頭,裡面的欄位用字典保存,因此要存取資料的話要傳入欄位名稱的字串,例 `val['value']`,回傳的結果放到 div_list,走訪完子串列,再將 div_list 加入到 sort_lists 以便後後續做 codegen 跟畫圖。
要換到下個子串列的話就要存取 `pending->prev`,也就是第 34 行的操作,如果沒有子串列就退出,有的話就重複上述過程,就能在 Python 的環境中使用整個 pending 串列。
```python=
import gdb
class DumpDivide(gdb.Command):
def reset(self):
self.png_path = None
self.list_type = "pending"
def __init__(self) -> None:
super().__init__("dump_sort", gdb.COMMAND_USER)
self.reset()
def invoke(self, args: str, from_tty: bool) -> None:
self.reset()
div_list = []
sort_lists = []
args = args.split(" ")
self.parse_args(args)
if self.list_type == "pending":
gdb.execute(f"p $pend = {args[0]}")
while True:
if str(gdb.parse_and_eval("node_val($pend)")) == "0x0":
break
else:
gdb.execute("p $list = $pend")
while True:
if str(gdb.parse_and_eval("node_val($list)")) == "0x0":
break
else:
val = gdb.parse_and_eval("node_val($list)")
div_list.append(val['value'].string())
gdb.execute("p $list = $list->next")
gdb.execute("p $pend = $pend->prev")
sort_lists.append(div_list)
div_list = []
```
### 設置斷點
在 list_sort 的分割與合併階段下斷點,用來觀察兩階段執行完的 pending 串列。
分割與合併階段斷點的參考位置如下程式碼 highlight 的位置,使用這兩處的 pending 串列傳入到 `dump_sort` 可以印出這兩階段的結果。
- [ ] list_sort.c
````diff
__attribute__((nonnull(2, 3))) void list_sort(void *priv,
struct list_head *head,
list_cmp_func_t cmp)
{
do {
/* ... */
/* Move one element from input list to pending */
list->prev = pending;
pending = list;
list = list->next;
pending->next = NULL;
+ count++; /* divide breakpoint */
} while (list);
for (;;) {
struct list_head *next = pending->prev;
if (!next)
break;
list = merge(priv, cmp, pending, list);
+ pending = next; /* merge breakpoint */
}
/* The final merge, rebuilding prev links */
merge_final(priv, cmp, head, pending, list);
}
````
- [ ] qtest.c
原本想再 list_sort 的 merge_final 用 dump_sort 印出最終的排序好串列,但是這時候的 head 還沒有完全接好,要等到 merge_final 執行完,否則會存取到 `0xdeadbeaf`,所以改成執行完 list_sort 的下一行下斷點並傳入 `current->q` 印出結果。
```diff
bool do_sort(int argc, char *argv[])
{
/* ... */
m_sort = sort_opt ? q_ksort : q_sort;
set_noallocate_mode(true);
if (current && exception_setup(true))
m_sort(current->q, descend);
+ exception_cancel();
set_noallocate_mode(false);
}
```
### GDB 測試 command
測資:建立 01~16 個節點,打亂後交由 list_sort 排序。篇幅關係不放完整 cmd 上來
- [ ] lab0/scripts/sort_step.cmd
為了避免每次執行到斷點就要停下來敲命令,這裡使用 gdb 的 command 命令規定執行到斷點時要執行的操作。檔名使用 `$cnt` 產生流水號後面加上階段,dump_sort 做完後更新流水號並繼續執行下去(c, continue)。
```bash
b list_sort.c:237
b list_sort.c:249
b qtest.c:634
p $cnt=1
# divide
command 1
eval "set $png=\"./sort_step/%d_divide.png\"", $cnt
dump_sort pending -o $png
p $cnt += 1
c
end
# merge
command 2
eval "set $png=\"./sort_step/%d_merge.png\"", $cnt
dump_sort pending -o $png
p $cnt += 1
c
end
# final
command 3
eval "set $png=\"./sort_step/%d_list.png\"", $cnt
dump_sort current->q -type origin -o $png
p $cnt += 1
c
end
```
```
$ gdb -q -x ./scripts/dump_sort.py --args ./qtest -f ./traces/demo.cmd
(gdb) source ./scripts/sort_step.cmd
Breakpoint 4 at 0x55555555fda0: file list_sort.c, line 237.
Breakpoint 5 at 0x55555555fdc4: file list_sort.c, line 249.
Breakpoint 6 at 0x555555556e10: file qtest.c, line 634.
$730 = 1
(gdb) r
Starting program: /home/lambert-wu/Repos/lab0-c/qtest -f ./traces/demo.cmd
```
假設 shuffle 完的串列入下
```
l = [11 02 12 01 05 07 03 04 14 15 16 06 10 08 13 09]
```
執行完成後,圖片和 dot 檔會存入到 lab0/sort_step/,暫時還不知道怎麼用 convert 命令把一系列的圖片大小不一 png 檔轉成每個步驟階段的串列置中顯示的 gif 檔,這樣會更生動
```
$ ls ./sort_step/
10_divide.dot 12_divide.png 15_divide.dot 17_merge.png 1_divide.dot 2_divide.png 5_divide.dot 7_divide.png
10_divide.png 13_divide.dot 15_divide.png 18_merge.dot 1_divide.png 3_divide.dot 5_divide.png 8_divide.dot
11_divide.dot 13_divide.png 16_divide.dot 18_merge.png 20_list.dot 3_divide.png 6_divide.dot 8_divide.png
11_divide.png 14_divide.dot 16_divide.png 19_merge.dot 20_list.png 4_divide.dot 6_divide.png 9_divide.dot
12_divide.dot 14_divide.png 17_merge.dot 19_merge.png 2_divide.dot 4_divide.png 7_divide.dot 9_divide.png
```
上述測資分割完的 pending list 如下圖所示,圖形呈現參考〈[Queue-Mergesort](https://www.sciencedirect.com/science/article/pii/002001909390088Q?via%3Dihub)〉。
Note: 藍色箭頭為 prev,紅色箭頭為 next。
pending 每個排序好的子串列的數量都是 2 的冪,充分利用雙向鍊結串列的特性不需要用額外的空間配置 Queue,也符合教材的表格最終結果。
```graphviz
digraph {
rankdir="LR"
null[shape=none];
node09[label="09", shape="box"]
node13[label="13", shape="box"]
node08[label="08", shape="box"]
node10[label="10", shape="box"]
node06[label="06", shape="box"]
node14[label="14", shape="box"]
node15[label="15", shape="box"]
node16[label="16", shape="box"]
node01[label="01", shape="box"]
node02[label="02", shape="box"]
node03[label="03", shape="box"]
node04[label="04", shape="box"]
node05[label="05", shape="box"]
node07[label="07", shape="box"]
node11[label="11", shape="box"]
node12[label="12", shape="box"]
node09->node13->node08->node06->node01->null[color=blue];
{ rank="same"; node08 -> node10[color=red] }
{ rank="same"; node06 -> node14 -> node15 -> node16[color=red] }
{ rank="same"; node01 -> node02 -> node03 -> node04 -> node05 -> node07 -> node11 -> node12[color=red] }
}
```
最後一次合併前的 pending 串列如下, 兩個子串列長度一樣。
```graphviz
digraph {
rankdir="LR"
null[shape=none];
node06[label="06", shape="box"]
node08[label="08", shape="box"]
node09[label="09", shape="box"]
node10[label="10", shape="box"]
node13[label="13", shape="box"]
node14[label="14", shape="box"]
node15[label="15", shape="box"]
node16[label="16", shape="box"]
node01[label="01", shape="box"]
node02[label="02", shape="box"]
node03[label="03", shape="box"]
node04[label="04", shape="box"]
node05[label="05", shape="box"]
node07[label="07", shape="box"]
node11[label="11", shape="box"]
node12[label="12", shape="box"]
node06->node01->null[color=blue];
{ rank="same"; node06 -> node08 -> node09 -> node10 -> node13 -> node14 -> node15 -> node16[color=red] }
{ rank="same"; node01 -> node02 -> node03 -> node04 -> node05 -> node07 -> node11 -> node12[color=red] }
}
```
遞增排序完的串列如下圖所。
```graphviz
digraph {
rankdir="LR"
head[shape=none];
node01[label="01", shape="box"]
node02[label="02", shape="box"]
node03[label="03", shape="box"]
node04[label="04", shape="box"]
node05[label="05", shape="box"]
node06[label="06", shape="box"]
node07[label="07", shape="box"]
node08[label="08", shape="box"]
node09[label="09", shape="box"]
node10[label="10", shape="box"]
node11[label="11", shape="box"]
node12[label="12", shape="box"]
node13[label="13", shape="box"]
node14[label="14", shape="box"]
node15[label="15", shape="box"]
node16[label="16", shape="box"]
head->node01->node02->node03->node04->node05->node06->node07->node08->node09->node10->node11->node12->node13->node14->node15->node16[color=blue];
node16->node15->node14->node13->node12->node11->node10->node09->node08->node07->node06->node05->node04->node03->node02->node01->head[color=red];
}
```