# 2023q1 Homework1 (lab0)
contributed by < `koty6069` >
### Reviewed by `yanjiew1`
- `q_insert_head` 和 `q_insert_tail` 及 `q_remove_head` 和 `q_remove_tail` 均有重複的程式碼出現,可以思考如何重用這些程式,避免重複的程式碼。
- 可以再思考如何利用原有佇列是已排序的特性來實作 `q_merge` 。
- Coding Style 和 commit message 均符合規範。
- 函式的實作過程和說明寫得清楚有條理。
- 仍有些作業要求未完成:
- 新增洗牌的指令 `shuffle` 及分析亂度
- 研究亂數產生器
- 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉
- 觀察與分析 `entropy`
- `web` 指令改善
## 開發環境
```shell
$ 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
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
CPU family: 6
Model: 142
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 10
CPU max MHz: 3400.0000
CPU min MHz: 400.0000
BogoMIPS: 3600.00
Caches (sum of all):
L1d: 128 KiB (4 instances)
L1i: 128 KiB (4 instances)
L2: 1 MiB (4 instances)
L3: 6 MiB (1 instance)
```
## 開發過程
### `q_new`
使用 `malloc` 配置新的記憶體空間,若空間配置成功,使用 `INIT_LIST_HEAD` 初始化 `head`,使其 `next` 和 `prev` 皆指向自身。當 `malloc` 配置空間失敗時會回傳 `NULL`,因此當配置失敗時無需額外處理,直接回傳 `NULL`。
```c
/* Create an empty queue */
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (head)
INIT_LIST_HEAD(head);
return head;
}
```
### `q_free`
<!--若傳入的 `l` 為 `NULL` 則直接 `return` 無需做任何處理。
其餘情況,使用 `list_for_each_entry_safe` 走訪整個佇列,並透過 `queue.h` 中已定義的 `q_release_element` 將佇列中型別為 `element_t` 的當前節點刪除,最後使用 `free` 將 `l` 的空間釋放。-->
`list_for_each_entry_safe` 可允許我們在走訪佇列時,對當前節點進行處理而不會導致不可預期行為<s>安全的對當前節點進行處理</s> ???,使用 `q_release_element` 將每個節點的空間釋放,最後使用 `free(l)` 將 `l` 的空間釋放。
:::warning
改進漢語表達。
:notes: jserv
:::
```c
/* Free all storage used by queue */
void q_free(struct list_head *l)
{
if (!l)
return;
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, l, list)
q_release_element(entry);
free(l);
}
```
### `q_insert_head` 和 `q_insert_tail`
1. 檢查 `head` 是否為 `NULL` ,若為 `NULL` 則直接回傳 `false`。
2. 配置空間給 `node`,若失敗回傳 `false`。
3. 配置字串長度加一(`strlen` 之回傳值不包含 `'\0'` )之空間給 `node` 的成員 `value` 用來儲存字串 `s`,若配置失敗回傳 `false`。
4. 使用 `strncpy` 複製字串並於最後補上 `'\0'`。
5. 使用 `list_add` / `list_add_tail` 將 `node` 新增到佇列的開頭/結尾。
```c
/* Insert an element at head of queue */
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *node = malloc(sizeof(element_t));
if (!node)
return false;
size_t len = strlen(s);
node->value = malloc(sizeof(char) * (len + 1));
if (!node->value) {
free(node);
return false;
} /* Need to free the space of node */
strncpy(node->value, s, len);
node->value[len] = '\0';
list_add(&node->list, head);
return true;
}
```
```c
/* Insert an element at tail of queue */
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *node = malloc(sizeof(element_t));
if (!node)
return false;
size_t len = strlen(s);
node->value = malloc(sizeof(char) * (len + 1));
if (!node->value) {
free(node);
return false;
} /* Need to free the space of node */
strncpy(node->value, s, len);
node->value[len] = '\0';
list_add_tail(&node->list, head);
return true;
}
```
:::warning
這二段程式高度相似,可以思考如何簡化。提示:`q_insert_tail` 可以直接呼叫已經實作好的 `q_insert_head` 來達成相同目的。
> [name=yanjiew1]
:::
### `q_remove_head` 和 `q_remove_tail`
* 使用 `list_first_entry` / `list_last_entry` 找到開頭/結尾的 entry。
* 使用 `list_del_init` 將其從 list 中移除,若 `sp` 不為 `NULL`,使用 `strncpy` 將該節點的 `value` 複製到 `sp`。
```c
/* Remove an element from head of queue */
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *node = list_first_entry(head, element_t, list);
list_del_init(&node->list);
if (sp) {
strncpy(sp, node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return node;
}
```
```c
/* Remove an element from tail of queue */
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *node = list_last_entry(head, element_t, list);
list_del_init(&node->list);
if (sp) {
strncpy(sp, node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return node;
}
```
:::warning
思考如何精簡程式碼。
:notes: jserv
:::
:::warning
* 若傳入的 `bufsize` 為 0 ,則 `bufsize - 1` 會變成 $2^{64} - 1$ ,可能導致一些問題。
* 這二段程式碼高度相似,應可再精簡。
> [name=yanjiew1]
:::
### `q_size`
按照[作業指示](https://hackmd.io/@sysprog/linux2023-lab0)進行實作,使用 `list_for_each` 走訪整個佇列,每經過一個節點就遞增`len`。
```c
/* Return number of elements in queue */
int q_size(struct list_head *head)
{
if (!head)
return 0;
int len = 0;
struct list_head *li;
list_for_each (li, head)
len++;
return len;
}
```
### `q_delete_mid`
老師上課提過要善用 doubly-linked list 的特性,因此使用兩個指標,`ahead` 從佇列開頭正向前進,`back` 從佇列結尾反向前進,有兩種情況代表走到中間節點:
1. `ahead` 和 `back` 走到同一個節點。
2. 當 `ahead` 與 `back` 相鄰,此時 `back` 即為中間節點。
~~`ahead` 超過 `back` ,此時 `ahead` 走到的節點即為中間節點。~~
找到目標節點後先將其從佇列中移除,再將其空間釋放。
```c
/* Delete the middle node in queue */
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *ahead = head->next, *back = head->prev;
while (ahead != back && ahead->next != back) {
ahead = ahead->next;
back = back->prev;
}
list_del_init(back);
q_release_element(list_entry(back, element_t, list));
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
return true;
}
```
:::warning
撰寫出更精簡的程式碼。
:notes: jserv
> * 將 `ahead->prev != back` 改成 `ahead-next != back` 並將後續處理節點由 `ahead` 改成 `back` 減少一次的迴圈。
> * 將 `list_entry` 放入 `q_release_element` 的參數中,避免多使用一個變數去儲存。
:::
### `q_delete_dup`
* 題目要求為傳入已排序的佇列,因此只考慮相鄰的節點是否重複。
* 佇列中所有重複的節點都應被刪除,因此使用 `bool del` 來紀錄節點是否需要刪除。
* 若當前節點與下一節點重複,即刪除當前節點並將 `del` 設為 `true` 。
(比較字串時若 `next` 為 `head` 則不進行比較。)
* 若當前節點不與下一節點重複,但 `del` 為 `true` ,代表當前節點與被刪除的前一節點重複,仍必須刪除當前節點。
```c
/* Delete all nodes that have duplicate string */
bool q_delete_dup(struct list_head *head)
{
if (!head)
return false;
element_t *cur, *safe;
bool del = false;
list_for_each_entry_safe (cur, safe, head, list) {
char *s = list_entry(cur->list.next, element_t, list)->value;
if (cur->list.next != head && strcmp(cur->value, s) == 0) {
list_del(&cur->list);
q_release_element(cur);
del = true;
} else if (del) {
list_del(&cur->list);
q_release_element(cur);
del = false;
}
}
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
return true;
}
```
### `q_swap`
節點兩兩一組互換,因此使用 `first` 和 `second` 分別記錄。
```c
/* Swap every two adjacent nodes */
void q_swap(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *first = head->next, *second = first->next;
for (; first != head && second != head;) {
first->next = second->next;
second->prev = first->prev;
first->prev->next = second;
second->next->prev = first;
first->prev = second;
second->next = first;
first = first->next;
second = first->next;
}
// https://leetcode.com/problems/swap-nodes-in-pairs/
}
```
:::warning
可以思考如何利用 `list.h` 提供的巨集和函式來操作,讓程式更簡潔。
> [name=yanjiew1]
:::
### `q_reverse`
* 執行 `q_reverse` 前: `node2->next = node3` / `node2->prev = node1`
```graphviz
digraph G {
node[shape=record];
graph[pencolor=transparent];
rankdir=LR;
p0[label="<h> Head | {<pr> prev|<ne> next}"];
p1[label="<h> Node1|{<pr> prev|<ne> next}"];
p2[label="<h> Node2|{<pr> prev|<ne> next}"];
p3[label="<h> Node3|{<pr> prev|<ne> next}"];
p0:ne -> p1:h;
p1:ne -> p2:h;
p2:ne -> p3:h;
//p3:ne -> p0:h;
//p0:pr -> p3:h;
p1:pr -> p0:h;
p2:pr -> p1:h;
p3:pr -> p2:h;
}
```
* 執行 `q_reverse` 後: `node2->next = node1` / `node2->prev = node3`
```graphviz
digraph G {
node[shape=record];
graph[pencolor=transparent];
rankdir=LR;
p0[label="<h> Head | {<pr> prev|<ne> next}"];
p3[label="<h> Node3|{<pr> prev|<ne> next}"];
p2[label="<h> Node2|{<pr> prev|<ne> next}"];
p1[label="<h> Node1|{<pr> prev|<ne> next}"];
p0:ne -> p3:h;
p3:ne -> p2:h;
p2:ne -> p1:h;
//p1:ne -> p0:h;
//p0:pr -> p1:h;
p3:pr -> p0:h;
p2:pr -> p3:h;
p1:pr -> p2:h;
}
```
* 從執行 `q_reverse` 前後的圖可以觀察到,將每個節點的 `next` 和 `prev` 互換即可達成目標。
* 最後將 `head` 的 `next` 和 `prev` 互換。
```c
/* Reverse elements in queue */
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *node, *safe, *tmp;
list_for_each_safe (node, safe, head) {
tmp = node->next;
node->next = node->prev;
node->prev = tmp;
}
tmp = head->next;
head->next = head->prev;
head->prev = tmp;
}
```
:::success
好的觀察,透過圖片解說很棒!
> [name=yanjiew1]
:::
### `q_reverseK`
起初想到使用 `list_cut_position` 將需要翻轉的 sublist 切割出來,再使用 `q_reverse` 將 sublist 進行翻轉,最後透過 `list_splice` 將其加回原本的佇列。在實作過程中發現需要紀錄一個 head 當作 `list_splice` 的插入點,因此參考其他同學的作業後,選擇紀錄 sublist 起始節點的前一個節點當作分割點(`cut`)。 `cnt` 用來記錄目前的 sublist 長度,若長度到達 `k` 則進行以下步驟:
1. 將 `cut->next` (真正的起始點)到 `node` (當前節點)移動到 `tmp`。
2. 使用 `q_reverse` 進行翻轉。
3. 將翻轉完的 sublist 加入到分割點 `cut` 後。
4. 將分割點設為當前節點,並將 `cnt` 設回 0。
<!--(原先使用 `cut = node` 但會導致順序錯亂,後改為 `cut = safe->prev`)。-->
```c
/* Reverse the nodes of the list k at a time */
void q_reverseK(struct list_head *head, int k)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *node, *safe, *cut = head;
int cnt = 0;
list_for_each_safe (node, safe, head) {
if (++cnt == k) {
LIST_HEAD(tmp);
list_cut_position(&tmp, cut->next, node);
q_reverse(&tmp);
list_splice(&tmp, cut);
cut = safe->prev;
cnt = 0;
}
}
// https://leetcode.com/problems/reverse-nodes-in-k-group/
}
```
### q_sort
參考 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 實作 `merge_sort_list` 和 `merge` 兩個函式進行排序。
```c
struct list_head *merge(struct list_head *l1, struct list_head *l2)
```
```c
struct list_head *merge_sort_list(struct list_head *head)
```
[Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 當中使用的結構為單向鏈結串列,且傳入 `merge_sort_list` 之參數為指向第一個節點的指標,因此先在 `q_sort` 中將最後一個節點指向 `NULL` 使其結構變為單向鏈結串列,並將指向第一個節點的指標傳入 `merge_sort_list`。
```c
head->prev->next = NULL;
head->next = merge_sort_list(head->next);
```
`merger_sort_list` 中使用快慢指標找出中間節點並將原本的 list 切成兩個部份,並持續以遞迴的方式進行切割,最後再透過 `merge` 將所有的 list 合併。
```c
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
fast = slow->next;
slow->next = NULL;
```
在實作 `merge` 時,因為不能配置新的記憶體,因此參考 [linked list 和非連續記憶體操作](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists) 中 [LeetCode 21. Merge Two Sorted Lists ](https://leetcode.com/problems/merge-two-sorted-lists/) 之作法,使用間接指標將 list 合併。
```c
for(node = NULL; l1 && l2; *node = (*node)->next) {
if (strcmp(list_entry(l1, element_t, list)->value,
list_entry(l2, element_t, list)->value) < 0) {
node = &l1;
} else {
node = &l2;
}
*ptr = *node;
ptr = &(*ptr)->next;
}
*ptr = (struct list_head *) ((u_int64_t) l1 | (u_int64_t) l2);
```
最後,由於前面將雙向鏈結串列改為單向,在參閱 [seasonwang0905](https://hackmd.io/@seasonwang/B1B0lVqpo) 的作法後,要將每一節點的 `prev` 重新指回前一個節點,以保持 doubly-linked list 的結構。
```c
struct list_head *cur = head, *nt = head->next;
while (nt) {
nt->prev = cur;
cur = nt;
nt = nt->next;
}
cur->next = head;
head->prev = cur;
```
### q_descend
從 `queue` 的結尾向前走訪,若該節點的字串比目前紀錄的 `str` 小,就將該節點移除,反之將 `str` 設成該節點的 `cur->value`。
```c
while (node != head) {
element_t *cur = list_entry(node, element_t, list);
node = node->prev;
if (strcmp(cur->value, str) < 0) {
list_del_init(&cur->list);
q_release_element(cur);
} else {
str = cur->value;
}
}
```
### q_merge
在此系統中,使用 `queue.c` 中定義的 `queue_contex_t` 來維護所有目前存在的佇列,在 `queue_contex_t` 中有包含 `struct list_head chain` 以 doubly-linked list 的結構來紀錄系統中的佇列,因此同樣可使用之前用在單一佇列上的巨集,來操作整個佇列。
```c
typedef struct {
struct list_head *q;
struct list_head chain;
int size;
int id;
} queue_contex_t;
```
首先紀錄 `head` 中的第一個佇列,用來將其他佇列都串接到此佇列上面,同時紀錄長度,用於最後回傳。
```c
struct list_head *list = list_first_entry(head, queue_contex_t, chain)->q;
size_t total = list_first_entry(head, queue_contex_t, chain)->size;
```
之後使用 `list_for_each_entry_safe` 走訪所有佇列,將除了第一個 queue 以外的所有成員都接到 `list` 上,同時要將每個佇列的長度都加上 `total`。
```c
list_for_each_entry_safe (node, safe, head, chain) {
if (node->id == 0)
continue;
list_splice_init(node->q, list);
total += node->size;
}
```
最後使用 `q_sort` 將串接完的 queue 進行重新排序。
:::warning
在 `queue.h` 中有說明傳入的各個佇列都是已排序的。可以思考如何利用已排序的特性來實作更高效率的程式。
> [name=yanjiew1]
:::
### 自動測試結果
```
+++ 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
ERROR: Probably not constant time or wrong implementation
Probably constant time
--- trace-17-complexity 0/5
--- TOTAL 95/100
```
:::warning
這是因為內建的 `dudect` 有瑕疵,改善 `dudect` 後即可通過測試。
> [name=yanjiew1]
:::
---
## 以 [Valgrind](https://valgrind.org/) 分析記憶體問題
執行 `$ make valgrind` 可看到輸出錯誤訊息,節錄如下:
```
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
==3785== 32 bytes in 1 blocks are still reachable in loss record 1 of 2
==3785== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==3785== by 0x10CBE0: do_new (qtest.c:146)
==3785== by 0x10DE0C: interpret_cmda (console.c:181)
==3785== by 0x10E3C1: interpret_cmd (console.c:201)
==3785== by 0x10E7C2: cmd_select (console.c:610)
==3785== by 0x10F0AE: run_console (console.c:705)
==3785== by 0x10D1FE: main (qtest.c:1224)
```
觀察後可發現每一次測試的錯誤訊息皆為相同格式:
```clike
XX bytes in X blocks are still reachable...
```
從 [Valgrind + 自動測試程式](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-b#Valgrind-%E4%BD%BF%E7%94%A8%E6%A1%88%E4%BE%8B) 和 [Valgrind Frequently Asked Questions](https://valgrind.org/docs/manual/faq.html) 的 5.2 中可得知,代表程式結束時有記憶體尚未釋放。
> "still reachable" means your program is probably ok -- it didn't free some memory it could have. This is quite common and often reasonable.
從錯誤訊息可以發現程式的進入點 main 在 qtest.c 中,並且發現當中會呼叫一些初始化的函式。
```c=1202
q_init();
init_cmd();
console_init();
```
往後繼續檢視,可發現與離開程式有關的函式。
```c=1221
add_quit_helper(q_quit);
bool ok = true;
ok = ok && run_console(infile_name);
/* Do finish_cmd() before check whether ok is true or false */
ok = finish_cmd() && ok;
```
檢查 `q_quit` 按照前面作業各函式的命名模式,可得知此函式功能應為在結束程式時對佇列作處理,觀察內容,可發現會將目前有紀錄的所有佇列透過 `q_free` 進行釋放。
```c=1063
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--;
}
}
```
但在此函式的第一行直接 `return true`,導致不會執行到後面釋放空間的行為,因此這邊應該將此行移除,移除後再次執行 `$ make valgrind` 可發現前述之錯誤訊息不再出現,代表程式結束前有正確釋放掉已配置的記憶體空間。
```c=1056
static bool q_quit(int argc, char *argv[])
{
return true;
```
開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer) 後執行自動測試也無出現錯誤訊息。
```
$ make clean
$ make SANITIZER=1
$ make test
```
## Linux 核心原始程式碼的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c)
### 研讀 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c)
初步可發現其中分為三個函式,並透過註解大致了解其功用:
* `merge`:用來將兩個 list 合併,透過此函式合併的 list 為單向鏈結串列,最後一個節點指向 `NULL`。
* `merge_final`:最後一次合併所使用的函式,會將合併完的 list 恢復成 doubly-linked list。
* `list_sort`:進行排序時呼叫的主要函式。
以下會針對上述三個函式進行解析。
#### `list_sort`
`list_sort` 需要三個參數,根據註解,其定義如下:
* `void *priv`:用來傳遞給 `cmp` 當作參數使用,`list_sort`本身不會用到。
* `struct list_head *head`:要被排序的 list。
* `list_cmp_func_t cmp`:用來比較元素大小的比較函式,支援回傳(<0 / =0 / >0)和(bool 0/1)兩種方式來決定要排序的元素大小。
```c
__attribute__((nonnull(2,3)))
void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp)
```
在排序前會先將原本的雙向鏈結串列改為單向(將最後一個節點指向 `NULL`),`pending` 用來暫存需要合併的 list,並用 `count` 紀錄當中 list 的數量。
```c
struct list_head *list = head->next, *pending = NULL;
size_t count = 0; /* Count of pending */
if (list == head->prev) /* Zero or one elements */
return;
/* Convert to a null-terminated singly-linked list. */
head->prev->next = NULL;
```