# 2024q1 Homework1 (lab0)
contributed by < `iLeafy11` >
:::danger
終於生病生完了,從今天開始,每天都來寫!!!!!!
:::
## 開發環境
新電腦安裝 Ubuntu 22.04 遇到 Failed to start Ubuntu live CD Installer,嘗試過各種方法,對於我來說,目前暫時無解,所以先使用 WSL。
安裝 WSL for Microsoft Windows 11 遇到問題時,可以參考: [Troubleshooting Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/troubleshooting),解決問題。
```
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz
CPU family: 6
Model: 165
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 3
BogoMIPS: 5807.99
```
### 在 WSL 使用 perf
如果在 WSL 使用 `perf` 會得到以下結果:
```
WARNING: perf not found for kernel 5.15.79.1-microsoft
You may need to install the following packages for this specific kernel:
linux-tools-5.15.79.1-microsoft-standard-WSL2
linux-cloud-tools-5.15.79.1-microsoft-standard-WSL2
You may also want to install one of the following packages to keep up to date:
linux-tools-standard-WSL2
linux-cloud-tools-standard-WSL2
```
如果我們想要進一步 `sudo apt install linux-tools-standard-WSL2` 會得到以下結果:
```
E: Unable to locate package linux-tools-standard-WSL2
```
不過幸好可以在 GitHub 上,找到有討論串有解決方法 [Install perf on WSL 2](https://gist.github.com/abel0b/b1881e41b9e1c4b16d84e5e083c38a13),`install-linux-perf-on-wsl2.sh`:
```shell
apt install flex bison
git clone https://github.com/microsoft/WSL2-Linux-Kernel --depth 1
cd WSL2-Linux-Kernel/tools/perf
make -j8
sudo cp perf /usr/local/bin
```
之後在 ~/.bashrc 設置路徑:
```shell
LINUX_TOOLS_DIR=/usr/lib/linux-tools
LINUX_TOOLS_MAXVER_DIR=`find $LINUX_TOOLS_DIR -maxdepth 1 -mindepth 1 -type d | sort --reverse | head --lines=1`
if [ -d $LINUX_TOOLS_MAXVER_DIR ]; then
for TOOL in `ls -r $LINUX_TOOLS_MAXVER_DIR | grep -v ".so$"`; do
alias $TOOL=$LINUX_TOOLS_MAXVER_DIR/$TOOL
done
fi
```
重啟 WSL 之後就可以順利使用 perf。
## 開發紀錄
不同於 2021 年的 lab0,這次 2024 年的 lab0,已經大幅改變,除了提早引入了參考 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 所改寫的 list.h,並且也額外加入了若干 LeetCode 題。參考我 2021 年當時在 linux 核心設計課堂上的實作,此次 lab0 光是 queue.[ch] 的內容至少就包含了 [2021q1 Homework2 (quiz2) 測驗1](https://hackmd.io/MGK7E7F4QlOAXX9kOEyY7w#%E6%B8%AC%E9%A9%97-1) 以及 [2021q1 Homework1 (lab0)](https://hackmd.io/28XwxjCOQj6PS766LQyAPQ?view)。
### `q_delete_mid` (LeetCode 2095. Delete the Middle Node of a Linked List)
關於 `q_delete_mid` 函式,如果點進去 [LeetCode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) 超連結可以發現底下有兩個 Hint:
:::info
* Hint 1
> If a point with a speed s moves n units in a given time, a point with speed 2 * s will move 2 * n units at the same time. Can you use this to find the middle node of a linked list?
* Hint 2
> If you are given the middle node, the node before it, and the node after it, how can you modify the linked list?
:::
可以發現 Hint 1 即是 [2021q1 Homework2 (quiz2) 測驗1](https://hackmd.io/MGK7E7F4QlOAXX9kOEyY7w#%E6%B8%AC%E9%A9%97-1) 中,Merge Sort 實作找中點的方式(快慢指標)。實作概念類似如下:
```c
struct list_head *q_middle(struct list_head *head)
{
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;
}
return slow;
}
```
找到中點後,就可以很輕易的移除它,只需要 `list_del` 之後再釋放其記憶體即可。
### `q_delete_dup` (LeetCode 82. Remove Duplicates from Sorted List II)
[LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/description/) 的 Discussion 中,有討論到使用 Dictionary 資料結構的方式讓這題解法達到 $O(1)$ 時間複雜度,概念類似於以下[使用 Hash Table 實作眾數問題](https://gist.github.com/iLeafy11/4ac1d4d8929aef09d5c8166dd6ab3edc)
但是如果貿然修改 lab0 的資料結構,後續勢必會面臨許多開發問題,所以或許只需要考量到 $O(N)$ 時間複雜度的解法,以走訪整個 list 的概念來實作,不過由於是 Sorted List,所以我們可以單純的做類似於以下的事:
```c
list_for_each_safe (node, safe, head)
{
if (node, node->next's str value is the same)
list_del(node);
else if (...)
list_del(node);
...
}
```
留意 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/description/) 是要將全部的 Duplicates 刪除,並非刪除到只剩下單獨一個。所以實作上可以將 Duplicates 的 `value` 先暫存起來:
```c
bool q_delete_dup(struct list_head *head)
{
...
list_for_each_entry_safe(entry, safe, head, list) {
if (&safe->list != head && !strcmp(entry->value, safe->value)) {
dup_str = safe->value;
...
} else if (dup_str && !strcmp(entry->value, dup_str)) {
...
dup_str = NULL;
}
}
...
}
```
有趣的是,題目為 "Remove" Duplicates,但是 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/description/) 題目網站上的敘述卻是:
>Given the head of a sorted linked list, ***delete*** all nodes that have duplicate numbers, leaving only distinct numbers from the original list. Return the linked list sorted as well.
### `q_swap` (LeetCode 24. Swap Nodes in Pairs)
[LeetCode 24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/description/) 的介紹是 Singly Linked List 的 Swap 操作,但是在 lab0,顯然並非 Singly Linked list。要操作的是 Doubly Linked List。
`q_swap` 函式在 queue.h 上註解的解釋是:
> Swap every two adjacent nodes
我們可以發現,`q_swap` 即是 `q_reverseK` 當 k = 2 的時候。但是這題最好不要將 `q_swap` 當成是 `q_reverseK` 的 subset 來實作,畢竟 `q_swap` 實作上比起 `q_reverseK` 可以較為簡單許多:
```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);
}
}
```
### `q_reverseK` (LeetCode 25. Reverse Nodes in k-Group)
[LeetCode 25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/description/) 這題的討論區有許多有關於測試資料的抱怨。不過在這裡的實作我就暫時不去考慮情景(k > list size),直接用最天真的方式來去思考問題實作。
可以簡單透過設立一個變數 `i` 記錄是否經過 `k` 個 node,之後再進行這個區間的 reverse 即可。要注意 reverse 的動作必須在一個暫存的 `LIST_HEAD(tmp)` 進行。
```c
void q_reverseK(struct list_head *head, int k)
{
int i = 0;
LIST_HEAD(tmp);
struct list_head *node, *safe;
struct list_head *from = head;
list_for_each_safe(node, safe, head)
{
i++;
if (i == k) {
i = 0;
list_cut_position(&tmp, from, node);
q_reverse(&tmp);
list_splice(&tmp, from);
INIT_LIST_HEAD(&tmp);
from = safe->prev;
}
}
}
```
### `q_sort`
有一個稍微惱人的地方是,如果先前沒有實作 `q_size`,直接實作 `q_sort`,在測試 `q_sort` 的過程會一直得到 segmentation fault 的報錯,但是問題並非出在 `q_sort` 本身,而是 qtest.c 的 `do_sort` 測試會呼叫 `q_size` 這個函式。所以要小心,實作 `q_sort` 前要先將 `q_size` 實作出來,否則可能會出現一整個下午除錯除到不知道到底發生什麼事的狀況。
實作上採用 Merge Sort,首先遞迴找出中點切割,最後再進行 Merge。
```c
void q_sort(struct list_head *head, bool descend)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
/* Merge Sort Implementation */
LIST_HEAD(left);
LIST_HEAD(sorted);
list_cut_position(&left, head, q_middle(head));
q_sort(&left, descend);
q_sort(head, descend);
q_sort_merge(&left, head, descend);
}
```
Merge 則使用迭代實作:
```c
void q_sorted_merge(struct list_head *lhs, struct list_head *rhs, bool descend)
{
LIST_HEAD(sorted);
while (!list_empty(lhs) && !list_empty(rhs)) {
char *lv = list_entry(lhs->next, element_t, list)->value;
char *rv = list_entry(rhs->next, element_t, list)->value;
struct list_head *tmp;
if(descend)
tmp = strcmp(lv, rv) >= 0 ? lhs->next : rhs->next;
else
tmp = strcmp(lv, rv) <= 0 ? lhs->next : rhs->next;
list_move_tail(tmp, &sorted);
}
list_splice_tail((struct list_head *) ((uintptr_t) lhs | (uintptr_t) rhs), &sorted);
INIT_LIST_HEAD(rhs);
list_splice_tail(&sorted, rhs);
}
```
這個函式做的事情其實就是將兩個 Sorted Lists 進行 Merge,也就是 Merge k Sorted Lists 當 k = 2 時的情景,所以這個函式就可以在實作 `q_merge` 的時候再次使用到。
當我們遞迴找中點切割,切到已經不能再切割的時候(切到每個 "Sub List" 只含有一個 element),這時就可以將每個只含有一個 "element" 的 "Sub List" 視為是 Sorted List,再逐一 Merge 在一起。
### `q_descend` & `q_ascend` (LeetCode 2487. Remove Nodes From Linked List)
我們從 LeetCode 網站 [2487. Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/) 給的兩個 hint:
:::info
* Hint 1
> Iterate on nodes in reversed order.
* Hint 2
> When iterating in reversed order, save the maximum value that was passed before.
:::
幾乎等於就是給了我們答案。
由於 queue.[ch] 的資料結構是 Doubly Linked List,所以可以直接做到 Hint 1:
> Iterate on nodes in reversed order.
為了保持 `list_for_each` 這類的 function-like macro 的實作風格,所以直接從 kernel 的 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 複製下來以下兩個巨集:
```c
#define list_for_each_reverse(pos, head) \
for (pos = (head)->prev; pos != (head); pos = pos->prev)
```
```c
#define list_for_each_entry_safe_reverse(pos, n, head, member) \
for (pos = list_last_entry(head, typeof(*pos), member), \
n = list_prev_entry(pos, member); \
!list_entry_is_head(pos, head, member); \
pos = n, n = list_prev_entry(n, member))
```
這樣就可以直接利用這兩個巨集達到反向走訪整個 queue 的作用。再加上 Hint 2 的提示,我們只需要在反向走訪 queue 的時候暫存 element value 的最大值,這樣就可以很直接的實作出 `q_descend`:
```c
int q_descend(struct list_head *head)
{
int counter = 0;
element_t *entry, *safe;
char *s = NULL;
list_for_each_entry_safe_reverse(entry, safe, head, list)
{
if(!s || strcmp(entry->value, s) > 0) {
s = entry->value;
counter++;
} else {
list_del(&entry->list);
q_release_element(entry);
}
}
return counter;
}
```
`q_ascend` 的實作邏輯與 `q_descend` 一致,只需要更改 element 的 value 的 compare function 即可,
```diff
int q_ascend(struct list_head *head)
{
...
- if(!s || strcmp(entry->value, s) > 0) {
+ if(!s || strcmp(entry->value, s) < 0) {
s = entry->value;
counter++;
} ...
...
}
```
### `q_merge` (LeetCode 23. Merge k Sorted Lists)
LeetCode 題目網站: [LeetCode 23. Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/)
有了 `q_sorted_merge` 這個函式,`q_merge` 實作起來就變得相對直觀:
```diff
int q_merge(struct list_head *head, bool descend)
{
queue_contex_t *first = list_first_entry(head, queue_contex_t, chain);
queue_contex_t *entry, *safe;
list_for_each_entry_safe(entry, safe, head, chain)
{
if(entry == first)
continue;
q_sorted_merge(entry->q, first->q, descend);
+ INIT_LIST_HEAD(entry->q);
}
return 0;
}
```
將除了第一個佇列以外,其餘所有的佇列都與第一個佇列進行 `q_sorted_merge`,這樣我們就可以將所有以排序的佇列合併,成為一個新的以排序佇列存放在第一個佇列的位置。
:::warning
額外加了一行 `INIT_LIST_HEAD(entry->q)` ,原因如下:
如果我們查看 `q_merge` 函式的註解:
>... There is no need to free the 'qcontext_t' and its member 'q' since they will be released externally. **However, q_merge() is responsible for making the queues to be NULL-queue, except the first one.**
這裡的 NULL-queue 我認為有些誤導人,因為這些經由 Merge 過後剩餘的空的 queues 並不是一定是要指向 `NULL`。而對於這些 queues 的處理,應取決於 `q_free` 的實作。
原因我們可以查看 qtest.c 的 `do_merge` 函式,裡面對於經由 Merge 過後剩餘的空的 queues 的處理是:
```diff
static bool do_merge( ... )
{
...
...
stuct list_head *cur = ...;
while (((uintptr_t) cur != (uintptr_t)&chain.head) {
queue_contex_t *ctx = list_entry(...);
cur = cur->next;
+ q_free(ctx->q);
free(ctx);
}
...
...
}
```
顯然是利用我們實作的 `q_free` 來釋放記憶體。因此舉例來說,假設我的 `q_free` 的實作如下:
```c
/* Free all storage used by queue */
void q_free(struct list_head *head)
{
if (!head)
return;
if (!list_empty(head)) {
element_t *entry, *safe;
list_for_each_entry_safe(entry, safe, head, list)
q_release_element(entry);
}
free(head);
}
```
如果我們在 `q_merge` 不去額外增加一行 `INIT_LIST_HEAD(entry->q)`,讓 `lhs` 的指標經由 `q_sorted_merge` 過後不去做額外處理使其剩餘骯髒值,那麼經由參數 `lhs` 傳遞的指標值,也就是那些經由 Merge 過後剩餘的空的 queues 的指標 (`entry->q`),在 `q_free` 操作之後,會進入到 `if (!list_empty(head))` 這個判斷式裡面,進而呼叫 `q_release_element(entry)`,導致存取到非法記憶體,造成 segmentation fault。
:::
到這裡,基本的 queue.c 實作暫時告一個段落,是時候開始真正 lab0 的重點了。
### qtest shuffle
https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
https://golangprojectstructure.com/the-knuth-shuffle/
### qtest option entropy 1
研讀:
https://hackmd.io/@8dSak6oVTweMeAe9fXWCPA/ryoegPgw_
### deduct
研讀:
https://github.com/oreparaz/dudect