# 2024q1 Homework1 (lab0)
contributed by < [`Hualing-Chiu`](https://github.com/Hualing-Chiu) >
## 開發環境
```bash
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.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) i7-7700 CPU @ 3.60GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 9
CPU max MHz: 4200.0000
CPU min MHz: 800.0000
BogoMIPS: 7200.00
```
## 佇列實作與程式碼改進
### `q_new`
透過 `malloc` 配置記憶體,並使用 `list.h` 內提供的`INIT_LIST_HEAD` 去初始化 `struct list_head`
> 需判斷是否有成功配置記憶體位址給指標
:::danger
改進你的漢語表達。
:::
```diff
struct list_head *q_new()
{
struct list_head *new = malloc(sizeof(struct list_head));
+ if(!new)
+ new = NULL;
INIT_LIST_HEAD(new); // use the function in list.h
return new;
}
```
:::danger
無論標題和內文中,中文和英文字元之間要有空白字元
> 已修正 [name=Sheena Chiu]
:::
### `q_free`
使用 `list.h` 提供的 `list_for_each_entry_safe` 走訪包含 `struct list_head` 的另外一個結構之 entry ,並透過額外的 `safe` 紀錄每個 iteration 所操作的節點的下一個節點。
```c
void q_free(struct list_head *head)
{
if(head){
element_t *entry, *safe;
list_for_each_entry_safe(entry, safe, head, list){
q_release_element(entry);
}
free(head);
}
}
```
### `q_insert_head`
使用 `strncpy` 而非 `strcpy` 來達成字串複製
> 因為`strcpy`可能會造成 buffer overflow 的問題
當 `node->value` 沒有成功配置記憶體位址的話,需要 free 掉此節點,否則在執行 `git commit -a` 時會出現 memory leak 的錯誤。
```diff
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;
int slen = strlen(s) + 1;
node->value = malloc(slen * sizeof(char));
if (node->value) {
- strcpy(node->value, s);
+ strncpy(node->value, s, slen);
list_add(&node->list, head);
return true;
} else {
+ free(node);
return false;
}
return false;
}
```
:::danger
使用 `git diff` 或 `diff -u` 命令來產生程式碼之間的變革列表,不要憑感覺填入,注意位移量。
:::
### `q_insert_tail`
做法跟 `q_insert_head()` 一樣,`list_add()` 改成 `list_add_tail()` 即可。
### `q_remove_head`
使用 `list.h` 提供的`list_first_entry` 找出佇列中第一個 `element`,再使用 `strncpy` 複製此 `element` 的 value 到 `sp` 中。
> 要注意需手動把<s>空字元</s>結尾字元 '\0' 加上去
:::danger
`'\0'` 不是「空」字元,你會把數字 0 叫做「空」數字嗎?
:::
```c
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(&node->list);
if (sp && bufsize) {
strncpy(sp, node->value, bufsize);
sp[bufsize - 1] = '\0';
}
return node;
}
```
### `q_remove_tail`
做法與 `q_remove_head` 一樣,`list_first_entry` 改成 `list_last_entry` 即可。
### `q_size`
```c
int q_size(struct list_head *head)
{
if(!head)
return 0;
int count = 0;
struct list_head *node;
list_for_each(node, head){
count++;
}
return count;
}
```
### `q_delete_mid`
:::danger
給定的佇列具備環狀且雙向的特性,不該說「反方向」,改進你的漢語表達,使用精準的詞彙。
:::
從 `list_head` 結構可知此佇列為雙向鏈結串列,除了使用快慢指標去實作之外,也可以使用兩個指標指向串列的頭尾,分別朝<s>反方向</s> 走訪節點,需要考慮兩個狀況:
* 節點個數為奇數,則兩個指標會相遇,因此判斷 `left != right`
* 節點個數為偶數,則兩個指標會相鄰,因此判斷 `left->next != right`
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *left = head->prev;
struct list_head *right = head->next;
while (left != right && left->next != right) {
left = left->prev;
right = right->next;
}
list_del(right);
element_t *element = list_entry(right, element_t, list);
q_release_element(element);
return true;
}
```
### `q_delete_dup`
此函式目的為刪除佇列中出現重複字串的節點,使用 `list_for_each_entry_safe` 迭代去紀錄下個節點,當遇到下個節點與當前節點字串相同時,刪除並釋放記憶體。
而須注意的是當刪除完後續重複節點時,當前指向的節點也必須刪除,因此使用了 `flag` 去紀錄該節點是否刪除。
```c
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
if (!head || list_empty(head)) {
return false;
}
element_t *cur, *next;
bool flag = false;
list_for_each_entry_safe (cur, next, head, list) {
if (&next->list != head && !strcmp(cur->value, next->value)) {
list_del(&cur->list);
q_release_element(cur);
flag = true;
} else if (flag) {
list_del(&cur->list);
q_release_element(cur);
flag = false;
}
}
return true;
}
```
### `q_swap`
當實作到 `q_reverseK` 時,可以發現 `q_swap` 其實就是 `q_reverseK` 的其中一種例子,因此直接套用 `q_reverseK` 的內容即可。
```c
/* Swap every two adjacent nodes */
void q_swap(struct list_head *head)
{
q_reverseK(head, 2);
}
```
### `q_reverse`
:::danger
給定的佇列具備環狀且雙向的特性,不該說「反向」,改進你的漢語表達,使用精準的詞彙。
:::
利用 `list.h` 內提供的 `list_for_each_safe` 去走訪每個節點,使用 `list_move` 將節點從原本的` linked list` 移動到另一個 `linked list head` 的開頭,就能達成<s>反向順序</s> 重新排列。
```c
struct list_head *node, *safe;
list_for_each_safe(node, safe, head){
list_move(node, head);
}
```
:::danger
無論標題和內文中,中文和英文字元之間要有空白字元
:::
### `q_reverseK`
使用 `count` 變數紀錄已走訪的節點個數,當 `count == 0` 時,使用 `list_cut_position` 切斷目前節點位址並放到 `tmp` 上,再使用前面實作好的 `q_reverse` 對 `tmp` 進行反轉,最後用 `list_splice` 把 `tmp` 接回 `tmp_head` 上。
```c
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_empty(head))
return;
int count = k;
struct list_head *node, *safe, tmp, *tmp_head;
tmp_head = head;
INIT_LIST_HEAD(&tmp);
list_for_each_safe (node, safe, head) {
count--;
if (count == 0) {
count = k;
list_cut_position(&tmp, tmp_head, node);
q_reverse(&tmp);
list_splice(&tmp, tmp_head);
tmp_head = safe->prev;
}
}
}
```
### `q_sort`
使用的排序演算法為 `Merge Sort`,首先使用 `NULL` 將最後一個節點的 `next` 與 `head` 斷開,接著使用 `mergeSort` 函式找出中間節點,分成左右兩段 `list`
```c
head->prev->next = NULL;
head->next = mergeSort(head->next);
```
實作完 `mergeSort` 後會回傳排序好的指標串列,但由於先前使用 `NULL` 將最後一個節點與 `head` 斷開,因此需要手動將 `head` 跟最後一個 `node` 的 `prev` 及 `next` 都接回去。
```c
struct list_head *last = head;
while (last->next)
last = last->next;
head->prev = last;
last->next = head;
head->next->prev = head;
```
:::danger
你如何確保目前的測試已涵蓋排序演算法的最差狀況?
:::
### `mergeSort`
採用遞迴呼叫搭配快慢指標的概念將指標串列左右對半分,直到分割成單一節點再使用 `mergeTwoLists` 函式合併成排序好的串列。
```c
struct list_head *mergeSort(struct list_head *head)
{
if (!head->next)
return head;
struct list_head *slow = head;
for (struct list_head *fast = head; fast && fast->next;
fast = fast->next->next)
slow = slow->next;
slow->prev->next = NULL;
struct list_head *left, *right;
left = mergeSort(head);
right = mergeSort(slow);
return mergeTwoLists(left, right);
}
```
### `mergeTwoLists`
此函式的目的是依照字串大小順序合併兩個指標串列。
參照 [你所不知道的C語言: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) 提供的較直觀的做法進行修改,但程式碼會顯的冗長,因尚未完整研讀 `list_sort.c`,之後改進並嘗試引入 `list_sort.c`。
```c
struct list_head *mergeTwoLists(struct list_head *L1, struct list_head *L2)
{
struct list_head *head, *ptr;
if (strcmp(list_entry(L1, element_t, list)->value,
list_entry(L2, element_t, list)->value) <= 0) {
head = L1;
ptr = head;
L1 = L1->next;
} else {
head = L2;
ptr = head;
L2 = L2->next;
}
while (L1 && L2) {
if (strcmp(list_entry(L1, element_t, list)->value,
list_entry(L2, element_t, list)->value) <= 0) {
ptr->next = L1;
L1->prev = ptr;
L1 = L1->next;
} else {
ptr->next = L2;
L2->prev = ptr;
L2 = L2->next;
}
ptr = ptr->next;
}
if (L1) {
ptr->next = L1;
L1->prev = ptr;
} else {
ptr->next = L2;
L2->prev = ptr;
}
head->prev = NULL;
return head;
}
```
### `q_descend`
> commit [f766e18](https://github.com/Hualing-Chiu/lab0-c/commit/f766e18072c8554bab111e8ed1a193d0927596e7)
從指標串列尾端走訪至 `head`,若下一個節點小於當前節點的 `value`,則刪除下一個節點。
### `q_merge`
```c
int q_merge(struct list_head *head, bool descend)
{
if (!head || list_empty(head))
return 0;
if (list_is_singular(head))
return q_size(list_first_entry(head, queue_contex_t, chain)->q);
queue_contex_t *q1 = container_of(head->next, queue_contex_t, chain);
for (struct list_head *cur = head->next->next; cur != head;
cur = cur->next) {
queue_contex_t *q = container_of(cur, queue_contex_t, chain);
list_splice_init(q->q, q1->q);
q->size = 0;
}
q_sort(q1->q, false);
q1->size = q_size(q1->q);
return q1->size;
}
```
## Valgrind + 自動測試程式
參照[ Massif: a heap profiler](https://valgrind.org/docs/manual/ms-manual.html),Massif 可以量測程式用了多少的 heap memory,除了可以幫忙加速程式,讓程式可以跟機器快取有更好的互動且避免 paging 發生,還可以減少耗盡 swap space 的機會。
參考 [var-x](https://hackmd.io/@vax-r/linux2024-homework1#Massif) 的測試步驟,首先先在 Makefile 中新增以下指令:
```diff
+ check-massif: qtest
+ valgrind --tool=massif ./$< -v 3 -f traces/trace-massif.cmd
```
`trace-massif.cmd` 是我們自己定義的 trace 檔,裡面的內容可以複製 `trace-17-complexity.cmd`,如果只要測試 `q_insert_head` 的話,稍微修改一下 `trace-17-complexity.cmd` 的內容即可。
```=
# Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant
option simulation 1
it
option simulation 0
```
接著執行 `make check-massif` 後可以獲得一個輸出檔案為 `massif.out.xxxxx`,執行 `ms_print massif.out.xxxxx` 可以顯示程式執行期間的記憶體消耗圖表,可以讓 user 更容易閱讀。
```
MB
1.231^ #
| #
| #
| # :: : :
| # : : :
| # : : :
| # : : :
| # @@ : : :
| # @ : : : :
| # :: :: @ : : : : :
| # : : @ : : :: : : : :
| # : : @ : : : : : : :
| # : : :@ : : ::: : :: :@@ @@ : : :
| # : : :@ :: :: : : : : : :@ :: @ : : ::: :
| # : : :@ :::: : : : : : : :@ : @ ::: : :: :
| # : : :@ : :: : : : : : : :@ : :@ :::::: :: : :
| # ::: : :@ : :: : :: : : : : : : :@ : ::@ :::: : :: :: :
| # : : : :@ : :: : : : : : : :::: :@ : ::@ :::: : :: :: ::@@:
| # : : : :@ :: : :: : : : : : : :: : :@ : ::@ :::: : : :: ::@ :
| # : : ::: :@ :::: ::::: : :: : : : :: : :@ : ::@ :::: ::: ::: ::@ :
0 +----------------------------------------------------------------------->Gi
0 14.71
Number of snapshots: 51
Detailed snapshots: [1 (peak), 8, 31, 35, 48]
```
以上圖表顯示 Massif 對這個程式做了56次的 snapshot,而 Massif 在 heap allocation/deallocation 時才會做 snapshot,但隨著程式運行時間增加,Massif 會減少 snapshot 的次數。
* `:` 是 normal snapshot
* `@` 是 detailed snapshots
* `#` 是 peak snapshot,紀錄 memory 消耗最大的時候
如果想要更直觀去看記憶體用量的話,可以執行 `massif-visualizer massif.out.xxxxx`,會顯示出以下圖形:
![7bf7a915-d2d4-4532-aa43-0b0619478516](https://hackmd.io/_uploads/HkrH_rr0p.jpg)
:::danger
只要摘錄你認為值得討論的訊息片段即可。
:::
## 研讀 Linux 核心原始程式碼 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試引入
將 `list_sort.c` 跟 `list_sort.h` 引入專案中。
一開始會遇到 include 相關的錯誤,檢查是否引入多餘的標頭檔,將不需要用到的全部刪除。
```diff
- #include <linux/bug.h>
- #include <linux/compiler.h>
- #include <linux/export.h>
+ #include "list_sort.h"
```
再來則會遇到 `unknown type name ‘u8’` 的錯誤,在 [/include/linux/types.h](https://github.com/torvalds/linux/blob/master/include/linux/types.h) 可以發現 `u8` 被定義為 `uint8_t`,因此將型別宣告改成 `uint8_t`。
```diff
+ uint8_t count = 0;
```
再來處理 `cmp` 參數,list_sort.c 的設計是傳入 cmp 比較函式,這邊我參考 [gaberplaysgame](https://hackmd.io/@GaberPlaysGame/linux2023-lab0#%E5%BC%95%E9%80%B2-liblist_sortc) 實作的 `q_list_cmp` 函式放入 `list_sort.c` 中,並刪掉 `priv` 與 `cmp` 參數。
```c
__attribute__((nonnull(1, 2))) int q_list_cmp(const struct list_head *a,
const struct list_head *b)
{
element_t *element_a = list_entry(a, element_t, list); // cppcheck-suppress nullPointer
element_t *element_b = list_entry(b, element_t, list); // cppcheck-suppress nullPointer
int res = strcmp(element_a->value, element_b->value);
return res;
}
```
接著在 `qtest.c` 中加入一個新的函式 `do_lsort` ,寫法與 `do_sort` 大同小異,差異在呼叫的函式需改成 Linux 核心風格的 `list_sort` 函式。
在 `qtest.c` 中新增 `lsort` <s>指令</s> 命令
:::danger
command 的翻譯是「命令」,而非「指令」(instruction)
:::
```diff
+ ADD_COMMAND(lsort, "Sort queue in ascending order by using Linux kernel sorting algorithm", "");
```
TODO: 使用 perf 分析效能