# 2022 lab0 contributed by < [maromasamsa](https://github.com/maromaSamsa) > ###### tags: `linux2022` > [作業說明](https://hackmd.io/@sysprog/linux2022-lab0) ## 安裝環境 ```shell= $gcc --version gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.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): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz Stepping: 9 CPU MHz: 3400.000 CPU max MHz: 3800.0000 CPU min MHz: 800.0000 BogoMIPS: 6799.81 Virtualization: VT-x L1d cache: 128 KiB L1i cache: 128 KiB L2 cache: 1 MiB L3 cache: 6 MiB NUMA node0 CPU(s): 0-3 ``` ## 開發前的準備 - 學習 [git](https://hackmd.io/qB62WY7WT0qDpp0sIRtiqw) 的使用 - 先從 GitHub 上 fork [lab0-c](https://github.com/sysprog21/lab0-c) 為自己的 repo - 之後在本機的終端機使用 git clone <myRepoLink> 將檔案下載下來 - 後在該目錄 (directory) 中使用 make check 確定程式可以正常運作,但缺少相應的功能需要補足 ```shell= $./qtest cmd> ``` - 在開發過程中可以使用 gdb 與 valgrind 去除錯與分析不當的記憶體使用 ```shell= $gdb ./qtest GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2 Copyright (C) 2020 Free Software Foundation, Inc. ...... Reading symbols from ./qtest... ## 幾個常用的指令 (gdb) run // 執行./qtest (gdb) b <filename>:<l> // 於目標 file 的第 l 行設置斷點 (gdb) p <var> // 印出 var (gdb) bt // backtrace 堆疊追蹤,顯示出從 main 至當前執行的呼叫函式 ``` ```shell= $valgrind -q ./qtest cmd> // 後像一般執行程式一樣操作即可,若記憶體發生不當存取會跳出訊息 ``` ### 專案中的資料結構設計 ```graphviz digraph { rankdir = "LR"; node [shape="record"]; header [label="<top>element_t|NULL|<n>list_head"]; a [label="<top>element_t|value|<n>list_head"]; b [label="<top>element_t|value|<n>list_head"]; c [label="<top>element_t|value|<n>list_head"]; header:n -> a:n a:n -> header:n a:n -> b:n b:n -> a:n b:n -> c:n c:n -> b:n c:n -> header:n header:n -> c:n "*head" -> header:n } ``` 在 `list.h` 中我們可以看到如下的結構: ```c= struct list_head { struct list_head *prev; struct list_head *next; }; ``` 而在 `queue.h` 中,我們是使用以下的資料結構: ```c= typedef struct { char *value; struct list_head list; } element_t; ``` 而設計成如此可以將功能細部分化,使程式碼撰寫的彈性更高, `list.h` 只負責實作 doubly linked circular list 以及相關的鏈結指標操作,其餘需要開發者自行定義。例如我今天欲自行設計一個新的 doubly linked circular queue ,每一個節點會儲存兩筆資料,那我只需要引入 `list.h` ,其餘的結構內容自己設計即可。 翻看需要補齊功能的 `queue.c` 中,許多只需要進行鏈結指標操作的函式,因為不需要動用到結點內部指向的字串資料,都是將 `list_head` 做為參數引入,如若在上述的情況下需要取得 `element_t` 的地址,則可以使用 linux kernel 中的 `container_of()` ,這部分在 `list.h` 中亦有相關的實作: [Linux 核心原始程式碼巨集: container_of](https://hackmd.io/@sysprog/linux-macro-containerof) ```c= /// in list.h #ifdef __LIST_HAVE_TYPEOF #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) #else #define container_of(ptr, type, member) \ ((type *) ((char *) (ptr) -offsetof(type, member))) #endif ... // 不太明白為何要改名的理由,用意何在? #define list_entry(node, type, member) container_of(node, type, member) ``` ## 對於一些駐列 (queue) 操作的實作 以下未特別標註的程式碼均於 queue.c 中實作。 ### q_size > Get the size of the queue > @head: header of queue > Return: the number of elements in queue, zero if queue is NULL or empty ```c= 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; } ``` `list_for_each` 在 list.h 中的程式碼如下,因為 doubly linked circular list 的特性,這個巨集 (macro) 會在呼叫他的地方,展開為一個走訪全部節點的 for loop ```c= // in list.h #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` ### q_new > Create an empty queue whose next and prev pointer point to itself Return: NULL for allocation failed ```c= struct list_head *q_new() { element_t *q = malloc(sizeof(element_t)); if (!q) { return NULL; } q->value = NULL; INIT_LIST_HEAD(&q->list); return &q->list; } ``` ```c= // in list.h static inline void INIT_LIST_HEAD(struct list_head *head) { head->next = head; head->prev = head; } ``` 一開始很納悶為什麼回傳的型別不是 `element_t*` ,這樣後續要如何存取字串資料呢?直到了解 `container_of` 的存在之後, 彷彿開啟新世界大門。只要有目標結構其中任意成員的位址,便可以找到該結構的開頭地址,因此我們不需要特地在 `element_t` 中建立一個成員指向下一個 `element_t` 節點,只需在定義 `element_t` 時引入 `list_head` ,便可以「繼承」已經實作的 `list_head` ,使 `element_t` 具有 doubly linked circular list 的特性。 ### q_free > Free all storage used by queue, no effect if header is NULL >@head: header of queue ```c= void q_free(struct list_head *l) { if (!l) { // that means l == NULL return; } struct list_head *node = l->next; while (node != l) { element_t *e = container_of(node, element_t, list); node = node->next; q_release_element(e); } element_t *head = container_of(l, element_t, list); free(head); } ``` :::info $man malloc RETURN VALUE 中我們可以發現以下敘述 The malloc() and calloc() functions return a pointer to the allocated memory, which is suitably aligned for any built-in type. <span style="color:red">On error, these functions return NULL.</span> 可以做為判別該節點是否已經進行過動態記憶體配置的依據 ::: :::warning 有一個目前想不明白的問題,我以為這兩種寫法是等價的,但看起來特別加註 struct 是表示宣告一個不完整的類別類型 ```c= element_t *e = container_of(node, element_t, list); // 編輯器會提醒以下寫法錯誤 struct element_t *e = container_of(node, struct element_t, list); ``` 不得使用不完整的類別類型 "struct element_t" 指標C/C++(393) ::: ```c= // in queue.h static inline void q_release_element(element_t *e) { test_free(e->value); test_free(e); } ``` 先確認該節點已經進行過動態記憶體配置,再進入該節點執行走訪與釋放記憶體的工作,使用 `container_of` 找到實際的整個節點的頭地址,再進行釋放,要注意 head 並不存有資料字串,因此不能使用提供的 `q_release_element` ,需要單獨處理。 ### q_insert_head, q_insert_tail > Argument s points to the string to be stored. > The function must explicitly allocate space and copy the string into it. > Return: true for success, false for allocation failed or queue is NULL ```c= /* Insert an element at head of queue */ bool q_insert_head(struct list_head *head, char *s) { if (!head) { return false; } element_t *q = malloc(sizeof(element_t)); if (!q) { return false; } q->value = strdup(s); if (!q->value) { free(q); return false; } list_add(&q->list, head); return true; } /* Insert an element at tail of queue */ bool q_insert_tail(struct list_head *head, char *s) { if (!head) { return false; } element_t *q = malloc(sizeof(element_t)); if (!q) { return false; } q->value = strdup(s); if (!q->value) { free(q); return false; } list_add_tail(&q->list, head); return true; } ``` 一樣先確認 head 是否為 NULL,然後進行 `element_t` 記憶體配置,將複製到新空間的字串連結,最後將一個完整的 `element_t` 透過 `list.h` 內實作的方法連結到駐列中。 ```c= // in list.h 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; } 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; } ``` ### q_remove_head, q_remove_tail ```c= /* Remove an element from head of queue */ element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head) { return NULL; } if (list_empty(head)) { return NULL; } element_t *q = container_of(head->next, element_t, list); list_del(head->next); strncpy(sp, q->value, bufsize - 1); *(sp + bufsize - 1) = '\0'; return q; } /* Remove an element from tail of queue */ element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head) { return NULL; } if (list_empty(head)) { return NULL; } element_t *q = container_of(head->prev, element_t, list); list_del(head->prev); strncpy(sp, q->value, bufsize - 1); *(sp + bufsize - 1) = '\0'; return q; } ``` 之後測試發現若是刪除節點而不回傳字串時會出錯 ```shell= l = [r] cmd> rhq Segmentation fault occurred. You dereferenced a NULL or invalid pointer ``` 使用 gdb backtrace 發現這種情況下傳入的 `sp` 為 NULL 並非指向字元陣列,因而報錯,最後改為: ```c=10 ... if(sp) { strncpy(sp, q->value, bufsize - 1); *(sp + bufsize - 1) = '\0'; } ... ``` ### q_delete_mid [leetcode reference](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) > Delete the middle node in queue > The middle node of a linked list of size n is the ⌊n / 2⌋th node from the start using 0-based indexing. > e.g. If there're six element [0...5], the third member [2] should be return. > Return: true for success, false if list is NULL or empty. ```c= bool q_delete_mid(struct list_head *head) { if (!head) { return false; } if (list_empty(head)) { return false; } struct list_head *mid = head->next; struct list_head *end = head->next; bool step = false; while (end != head) { end = end->next; if (step) { mid = mid->next; } step = !step; } list_del(mid); q_release_element(container_of(mid, element_t, list)); return true; } ``` 最直接的思維是設立兩個指標初始指向第一個元素,第一個指標每走兩步另一個指標只走一步,當第一個指標走完時,釋放當前第二個指標指向的元素。後來觀摩其他人的做法後,得知因為是雙向環形結構,也可以一個往前走一個往後走,直到雙方相遇亦可以實現。不管什麼方法,都需要考慮元素個數為偶數時的問題。 ### q_delete_dup (list is sorted) [leetcode reference](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/) ```c= bool q_delete_dup(struct list_head *head) { if (!head) { return false; } if (head->prev == head->next) { // if list is singular or empty, return true with doing nothing return true; } struct list_head *trash = q_new(); struct list_head *start; bool isDup = false; element_t *q_iter; element_t *q_safe; list_for_each_entry_safe (q_iter, q_safe, head, list) { if (&q_safe->list != head && strcmp(q_iter->value, q_safe->value) == 0) { if (!isDup) { start = &q_iter->list; isDup = true; } } else { if (isDup) { struct list_head *trash_first = trash->next; struct list_head *head_first = start->prev; trash->next = start; start->prev = trash; head_first->next = &q_safe->list; q_safe->list.prev = head_first; q_iter->list.next = trash_first; trash_first->prev = &q_iter->list; isDup = false; } } } q_free(trash); return true; } ``` 因為是排序過的,重複的字串元素必定相鄰,我希望盡量做越少次指標鏈結的更動越好,因此會只會將重複的元素串去頭去尾,加入新創建的 header `trash` 中,最後再使用 `q_free` 將接入 `trash` 的全部元素銷毀。其中使用了 `list_for_each_entry_safe` 協助存取當前走訪的指標與下一個指標 (雖然他原先設計的目的似乎不希望開發者用到 `q_safe` ),我推測這樣子可能在某些情況下運行速度會比一個一個銷毀來的快 (例如有巨量相同的重複字串),需要做實驗驗證。 ### q_sort - 第一版為遞迴版本的 Quick sort 效能測試理所當然的失敗 ```c= static void quick_sort(struct list_head *start, struct list_head *end) { if (start == end) { return; } struct list_head *pivot = start; struct list_head *head = pivot; char *pStr = list_entry(pivot, element_t, list)->value; while (start != end) { char *sStr = list_entry(start, element_t, list)->value; start = start->next; if (strcmp(pStr, sStr) <= 0) { // to pivot right (next), do nothing } else { // to pivot left (prev) list_move_tail(start->prev, head); head = head->prev; } } quick_sort(head, pivot); // pivot left quick_sort(pivot->next, end); // pivot right } void q_sort(struct list_head *head) { if (!head || head->prev == head->next ) { return; } quick_sort(head->next, head); } ``` :::danger Worst case of quick sort bigO = O(n^2) e.g. cmd>ih tiger 10000 cmd>ih zoo 10000 ::: - 閱讀 [老師提供的教材](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) 之後,試著寫出符合分治法精神的遞迴版 Merge sort ```c= /* Sort elements of queue in ascending order */ void q_sort(struct list_head *head) { if (!head || head->prev == head->next) { /* * if there is no init list or list is singular * or list is empty */ return; } //1. uncircular the list head->prev->next = NULL; // 2. sorting, we not consider prev link when sorting struct list_head *sorted; sorted = merge_sort(head->next, q_size(head)); // 3. reconstract to doubly linked circular queue head->next = sorted; struct list_head *tmp = head; while (tmp->next) { tmp->next->prev = tmp; tmp = tmp->next; } tmp->next = head; head->prev = tmp; } ``` 因為想不到如何在維持 doubly linked circular queue 下使用 Merge sort ,我在做完一些例外判斷之後,將整個排序流程分成三大部份: ##### 1. 解除尾端鍊接 忽略 `prev` 的連接關係,在解除了尾端鍊接之後,便可以將目前的 `queue` 看作 singular linked queue ##### 2. 使用遞迴版 Merge sort 閱讀 [lambert](https://hackmd.io/@lambert-wu) 同學在討論區分享的 [自撰筆記](https://hackmd.io/@lambert-wu/modified-merge-sort#%E9%81%9E%E8%BF%B4%E7%89%88) 所紀錄下的結論,可以知道迭代風格的 merge sort 會有更好的效能,不過因為寫起來並不易讀,且需要考慮要用動態記憶體配置或是固定陣列除存分割的節點,以及更重要的主因 ( 我還不太會寫 ) ,這邊展示的是遞迴版的實作。我在分割的速度上引入當前佇列長度,可以稍微優化執行速度。 以下說明排序實作程式碼: ```c= static struct list_head *merge_sort(struct list_head *li, size_t n) { if (n == 1) { li->next = NULL; return li; } struct list_head *mid = li; for (int i = 0; i < n / 2; ++i) { mid = mid->next; } mid->prev->next = NULL; struct list_head *left = merge_sort(li, n / 2); struct list_head *right = merge_sort(mid, n / 2 + (n % 2)); return merge_two_list(left, right); } ``` :::info 使用遞迴的方式將當前佇列平分成兩等分( 嚴格來說,是分割成差距比例不超過 1:2 的佇列,在當前佇列數量為 3 時會出現此極值,隨著數量越多,分割的比例會越趨近於 1:1,若初始佇列數量為偶數,則分割比例永遠是 1:1 ),直到無法分割為止,每一層會紀錄下分割位置,後用於呼叫合併排序 `merge_two_list();`,並將排序後的結果回傳。 e.g. 當初始佇列數量為 5, `li` : `mid` = 2:3: > <span style="color:red"> mid->prev->next = NULL </span>; ```graphviz digraph { rankdir = "LR"; node0[label = "0", xlabel = "li"]; node1[label = "1"]; node2[label = "2", xlabel = "mid"]; node3[label = "3"]; node4[label = "4"]; node0->node1; node1->node2[color = red]; node2->node3; node3->node4; } ``` > left = merge_sort(li, 2); > right = merge_sort(mid, 3); ::: 因為每一層遞迴函式都可以確保兩邊佇列長度差不多,根據分治法的觀念可以知道遞迴呼叫深度為 **log<sub>2</sub><sup>n</sup>** , **n** 為初始佇列長度。在另一方面,因為有傳入長度作為依據,找尋中間斷點的走訪次數可以從 **n** 縮短一半變為 **n/2**, :::info 雖然量級不變, Merge sort 分割階段的時間複雜度暫時分析為 **O($\frac{n}{2}$*log<sub>2</sub><sup>n</sup>)*** ::: 合併排序實作程式碼,這裡使用雙重指標去操作指標 `sorted` 去做融合排序,並回傳結果。 ```c= static struct list_head *merge_two_list(struct list_head *left, struct list_head *right) { struct list_head *sorted = NULL; struct list_head **ptr = &sorted; for (; left && right; ptr = &(*ptr)->next) { char *lVal = list_entry(left, element_t, list)->value; char *rVal = list_entry(right, element_t, list)->value; if (strcmp(lVal, rVal) <= 0) { *ptr = left; left = left->next; } else { *ptr = right; right = right->next; } } *ptr = (struct list_head *) ((uintptr_t) left | (uintptr_t) right); return sorted; } ``` 因為前面的分割階段可以確保佇列 `left` 與 `right` 長度差不多,一樣根據分治法的觀念可以知道遞迴呼叫深度為 **log<sub>2</sub><sup>n</sup>** ,每一層中佇列兩兩合併,時間複雜度為 **O(n)** :::info 由此可以解釋 Merge sort 合併階段的時間複雜度為 **O(n*log<sub>2</sub><sup>n</sup>)*** ::: :::success 這確保了在一些特殊情況下,不像是 Quick sort 的時間複雜度會淪落到 **O($n^2$)** , Merge sort 的時間複雜度都可控制在 **O($\frac{n}{2}$*log<sub>2</sub><sup>n</sup> + n*log<sub>2</sub><sup>n</sup>)*** = **O($\frac{3n}{2}$*log<sub>2</sub><sup>n</sup>)*** = **O(n*log<sub>2</sub><sup>n</sup>)*** ::: ##### 3. 將排序好的佇列變回 doubly linked circular queue 將處理過後的佇列連接回 `head` ,並且重新梳理已經因為重新排序而錯亂的 `prev` 連接關係 ## 與 Linux kernel 實作的 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 進行比較 ## 於 qtest.c 中新增指令以及功能 ### shuffle > 允許藉由 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作 使用 **gdb** 觀察其中一個已經實作的功能是如何被呼叫的,這裡以 `q_swap` 為例: ```shell= $ gdb ./qtest ... ... $ (gdb) b queue.c:q_swap Breakpoint 1 at 0x6f02: file queue.c, line 200. $ (gdb) run $ cmd> swap Warning: Try to access null queue Breakpoint 1, q_swap (head=0x0) at queue.c:200 $ (gdb) bt #0 q_swap (head=0x0) at queue.c:200 #1 0x0000555555557052 in do_swap (argc=<optimized out>, argv=<optimized out>) at qtest.c:721 #2 0x00005555555594f9 in interpret_cmda (argc=argc@entry=1, argv=argv@entry=0x555555566ea0) at console.c:184 #3 0x0000555555559a45 in interpret_cmd (cmdline=<optimized out>, cmdline@entry=0x5555555669c0 "swap") at console.c:204 #4 0x000055555555a525 in run_console (infile_name=<optimized out>) at console.c:644 #5 0x00005555555589f0 in main (argc=1, argv=0x7fffffffdc08) at qtest.c:1010 ``` 其中 #2 中的 `interpret_cmda` 的程式碼如下: ```c= /* Execute a command that has already been split into arguments */ static bool interpret_cmda(int argc, char *argv[]) { if (argc == 0) return true; /* Try to find matching command */ cmd_ptr next_cmd = cmd_list; bool ok = true; while (next_cmd && strcmp(argv[0], next_cmd->name) != 0) next_cmd = next_cmd->next; if (next_cmd) { ok = next_cmd->operation(argc, argv); if (!ok) record_error(); } else { report(1, "Unknown command '%s'", argv[0]); record_error(); ok = false; } return ok; } ``` 可以發現當我們在終端機輸入命令之後會被拆解成 `argc` 和`*argv[]`,命令的第一個字串 `argv[0]` ( 在這裡是 `"swap"` )會與命令清單 `cmd_list` 逐一比較,若是有相符則執行該命令 `next_cmd->operation(argc, argv)` (在這裡會執行 `do_swap` ),因此要新增功能需要在 `cmd_list` 中新增一個命令節點。 對 `cmd_list` 使用 `find all refference` ,可以發現新增命令節點的區塊被放在這裡: ```c= // in console.c /* Add a new command */ void add_cmd(char *name, cmd_function operation, char *documentation) { cmd_ptr next_cmd = cmd_list; cmd_ptr *last_loc = &cmd_list; while (next_cmd && strcmp(name, next_cmd->name) > 0) { last_loc = &next_cmd->next; next_cmd = next_cmd->next; } cmd_ptr ele = (cmd_ptr) malloc_or_fail(sizeof(cmd_ele), "add_cmd"); ele->name = name; ele->operation = operation; ele->documentation = documentation; ele->next = next_cmd; *last_loc = ele; } ``` 新增命令之後 `makefile` , ```c= // in qtest.c static void console_init() { ... ADD_COMMAND(shuffle, " | shuffle list randomly"); ... } ``` ```c= // in console.h #define ADD_COMMAND(cmd, msg) add_cmd(#cmd, do_##cmd, msg) ``` ```c= // in qtest.c static bool do_shuffle(int argc, char *argv[]) { if (argc != 1) { report(1, "%s takes no arguments", argv[0]); return false; } if (!l_meta.l) { report(3, "Warning: Try to access null queue"); return true; } error_check(); set_noallocate_mode(true); if (exception_setup(true)) { for (int i = lcnt; i > 0; --i) { int roll = rand() % i; struct list_head *it = l_meta.l->next; for (int j = 0; j < roll; j++) it = it->next; list_move_tail(it, l_meta.l); } } exception_cancel(); set_noallocate_mode(false); show_queue(3); return !error_check(); } ``` ### web > 提供 web 伺服器功能,注意: web 伺服器運作過程中,qtest 仍可接受其他命令 > 可依據前述說明,嘗試整合 [tiny-web-server](https://github.com/7890/tiny-web-server)