--- tags: linux2022 --- # 2022q1 Homework1 (lab0) contributed by < `sternacht` > ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.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): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 142 Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz Stepping: 10 CPU MHz: 1800.000 CPU max MHz: 3400.0000 CPU min MHz: 400.0000 BogoMIPS: 3600.00 Virtualization: VT-x L1d cache: 128 KiB L1i cache: 128 KiB L2 cache: 1 MiB L3 cache: 6 MiB NUMA node0 CPU(s): 0-7 ``` ## [作業要求](https://hackmd.io/@sysprog/linux2022-lab0#-作業要求) 依據指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改,記得也該實作 q_sort 函式。 - q_new: 創一個空的 queue - q_free: 釋放掉一個 queue - q_insert_head: 在 head 插入一個 element (LIFO) - q_insert_tail: 在 tail 插入一個 element (FIFO), O(1) - q_remove_head: 把 head 的 element 移除 - q_remove_tail: 把 tail 的 elemeny 移除 - q_size: return queue 的大小 - q_delete_mid: 把位在中間的 element 刪除 - q_reverse: 將 queue 反轉,不配置或釋放任何的 element - q_delete_dup: 將 queue 中有重複字串出現的所有節點刪除 - q_swap: 將 queue 的節點兩兩一組交換順序 - q_sort: 將 queue 由小排到大, sort by nature sort - 在 qtest 提供新的命令 shuffle,允許藉由 Fisher–Yates shuffle 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作 - 在 qtest 提供新的命令 web,提供 web 伺服器功能,注意: web 伺服器運作過程中,qtest 仍可接受其他命令 - 解釋 select 系統呼叫在本程式的使用方式,並分析 console.c 的實作,說明其中運用 CS:APP RIO 套件 的原理和考量點。 :::warning 列出其他在作業要求中出現的操作,例如 LeetCode 題目。 :notes: jserv ::: ## 開發過程 ### q_new 向記憶體索取大小為 element_t 的空間,並檢查是否成功,若非則回傳 NULL ,是則進行初始化並回傳 ```c struct list_head *q_new() { element_t *new = malloc(sizeof(element_t)); if (!new) return NULL; new->value = NULL; INIT_LIST_HEAD(&(new->list)); return &(new->list); } ``` git commit 時出現以下警告 > queue.c:25:5: error: Memory leak: new [memleak] return &(new->list); ^ 修改為 ```c struct list_head *q_new() { struct list_head *new = malloc(sizeof(struct list_head)); if (!new) return NULL; INIT_LIST_HEAD(new); return new; } ``` ### q_free 逐個走訪包括 head 在內的 entry 並將其釋放 ```c void q_free(struct list_head *l) { element_t *del = NULL, *safe = NULL; list_for_each_entry_safe(del, safe, l, list){ free(del); } free(l); } ``` 原先未考慮到 element 內還有 value 要釋放而出錯,並且在輸入值為 NULL 也沒有進行例外處理,因此修改為 ```c void q_free(struct list_head *l) { if (!l) return; element_t *del, *safe; list_for_each_entry_safe(del, safe, l, list){ list_del(&(del->list)); q_release_element(del); } free(l); } ``` ### q_insert_head 判斷佇列是否存在,否則回傳 false ,是則向記憶體索取大小為 element_t 的空間,失敗則回傳 false ,成功則將 s 的內容複製到節點內,並將該節點放入 queue 的開頭並回傳 true 。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new = malloc(sizeof(element_t)); if (!new) return false; strcpy(new->value, s); list_add(&(new->list), head); return true; } ``` new 配置時的 new->value 尚未配置記憶體空間,因此 `strcpy` 會報錯,因此需要先行計算欲放入的字串大小並向記憶體索取適當的空間,然後再用 `strncpy` 將字串放入。 > 註 : `strcpy` 與 `strncpy` 的差別在於 `strcpy` 會有 buffer overflow 的問題,因為其不考慮 *dest 是否有足夠空間放下 *src 的字串,而 `strncpy` 則會透過最後一個參數 count 來限制,但在必要時需自行補上 `\0` 來做結尾。 [參考來源](https://skylinelimit.blogspot.com/2018/02/c-2.html) ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new = malloc(sizeof(element_t)); if (!new) return false; size_t len = strlen(s) + 1; new->value = malloc(len); if (!new->value) { free(new); return false; } strncpy(new->value, s, len); list_add(&new->list, head); return true; } ``` ### q_insert_tail 大致與 q_insert_head 相同,僅節點放置之位置不同。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new = malloc(sizeof(element_t)); if (!new) return false; strcpy(new->value, s); list_add_tail(&new->list, head); return true; } ``` 同 q_insert_head 做修改 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new = malloc(sizeof(element_t)); if (!new) return false; size_t len = strlen(s) + 1; new->value = malloc(len); if (!(new->value)) { free(new); return false; } strncpy(new->value, s, len); list_add_tail(&new->list, head); return true; } ``` ### q_remove_head 將 queue 中的第一個節點移出 queue 並回傳該節點,同時將節點中的內容複製到 sp 中,若queue 不存在或為空時回傳 NULL 。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if(!head || list_empty(head)) return NULL; element_t *remove = NULL; remove = list_entry (head->next, element_t, list); strncpy(sp, remove->value, bufsize); head->next = head->next->next; return remove; } ``` 題目上表示 if sp is non-NULL... ,因此加上對 sp 的判斷式。此外,之前對strncpy有誤會,字串末的 `\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 *remove = list_entry(head->next, element_t, list); if (sp != NULL){ strncpy(sp, remove->value, bufsize); sp[bufsize - 1] = '\0'; } list_del_init(&(remove->list)); return remove; } ``` ### q_remove_tail 大致同 `q_remove_head` 。 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if(!head || list_empty(head)) return NULL; element_t *remove = list_entry(head->prev, element_t, list); strncpy(sp, remove->value, bufsize); head->prev = head->prev->prev; return remove; } ``` 同 q_remove_head 做修改 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *remove = list_entry(head->prev, element_t, list); if (sp != NULL){ strncpy(sp, remove->value, bufsize); sp[bufsize - 1] = '\0'; } list_del_init(&(remove->list)); return remove; } ``` ### q_size 利用一個 counter 逐個計算 queue 中的節點個數(不包含 head ),當 queue 不存在時回傳0 ```c { if (!head) return 0; int len = 0; struct list_head *node; list_for_each (node, head) len++; return len; } ``` ### q_delete_mid 利用快慢指標來找出位在中間的節點,因作業要求是在 queue size 為偶數時刪除第 $\lfloor \frac{n}{2} \rfloor$ 個,因此要從 head 開始,而非從 head->next 開始,刪除時先利用 `list_del_init` 將位處中間的節點 slow 從 queue 中移除,再用 q_free 將該節點的記憶體釋放。 > 註 : `list_del` 不會將該節點的 prev, next 指回自身,因此直接用使用 q_free 會有問題,需要先取得整個節點( element_t )後,再進行 free 。 ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *slow = head; for (struct list_head *fast = head; fast && fast->next; fast->next->next){ slow = slow->next; } list_del_init(slow); q_free(slow); return true; } ``` 先前撰寫時錯以 singly linked list 的方式來寫,但這樣在 doubly linked list 中會有 infinite loop 的問題發生,因此改成以個數計算的方式來做為迴圈的條件,找出第 $\lfloor \frac{n}{2} \rfloor$ 個節點 ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *node = head; for (int len = q_size(head); len > 0; len -= 2) { node = node->next; } element_t *del = list_entry(node, element_t, list); list_del(node); q_release_element(del); return true; } ``` ### q_delete_dup 將佇列裡有重複出現的所有字串刪除,當佇列不存在或為空時回傳 false ,若只有一個節點則因不會有重複出現直接回傳 true 。 ```c bool q_delete_dup(struct list_head *head) { if (!head) return false; if (list_empty(head) || list_is_singular(head)) return true; struct list_head *node = head->next; struct list_head *del_q = q_new(); element_t *entry1 = NULL, *entry2 = NULL; int len = q_size(head) - 1; while (len > 0) { entry1 = container_of(node, element_t, list); entry2 = list_entry(node->next, element_t, list); if (strcmp(entry1->value, entry2->value) == 0) { while (len && strcmp(entry1->value, entry2->value) == 0) { list_move(node->next, del_q); entry2 = list_entry(node->next, element_t, list); len--; } node = node->next; list_move(node->prev, del_q); } else { node = node->next; } len--; } q_free(del_q); return true; } ``` ### q_swap 將佇列中的節點兩兩交換位置 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *lnode = head->next; struct list_head *rnode = lnode->next; int len = q_size(head); while (len > 1) { lnode->prev->next = rnode; rnode->next->prev = lnode; lnode->next = rnode->next; rnode->prev = lnode->prev; lnode->prev = rnode; rnode->next = lnode; lnode = lnode->next; rnode = lnode->next; len -= 2; } return; } ``` :::warning 使用 List API 改寫為更精簡好讀的程式碼。 :notes: jserv ::: 先前對 list_add 有些誤會,仔細想一想才發覺改成這樣做是沒問題的,雖然以指標操作的數量來說並沒有減少,但相比於上方的程式碼來說,更為直觀且好讀。 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head)) return; bool lock = false; struct list_head *node = NULL, *safe = NULL; list_for_each_safe(node, safe, head) { if (lock){ list_del(node); list_add(node, safe->prev->prev); } lock = !lock; } return; } ``` ### q_reverse 將佇列中的節點順序反過來 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *node, *safe; list_for_each_safe (node, safe, head) { list_del(node); list_add(node, head); } return; } ``` ### q_sort 將佇列中節點依照字串值由小至大排序 ```c void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; // list_head is doubly linked list, so make it singly first head->prev->next = NULL; // go mergesort head->next = q_mergesort(head->next); // make queue doubly again head->next->prev = head; struct list_head *node = head->next; while (node->next){ node = node->next; } node->next = head; head->prev = node; } struct list_head *q_mergesort(struct list_head *head) { if (!head || !head->next) return head; struct list_head *tail = q_split(head); head = q_mergesort(head); tail = q_mergesort(tail); return q_merge(head, tail); } struct list_head *q_merge(struct list_head *head, struct list_head *tail) { if (!head) return (tail); if (!tail) return (head); if (cmp(head, tail) > 0){ tail->next = q_merge(head, tail->next); tail->next->prev = tail; tail->prev = NULL; return tail; } else { head->next = q_merge(head->next, tail); head->next->prev = head; head->prev = NULL; return head; } } struct list_head *q_split(struct list_head *head) { struct list_head *slow = head; for (struct list_head *fast = head->next; fast && fast->next; fast = fast->next->next){ slow = slow->next; } struct list_head *tmp = slow->next; slow->next = NULL; return tmp; } int cmp(struct list_head *a, struct list_head *b) { element_t *entry1 = list_entry(a, element_t, list); element_t *entry2 = list_entry(b, element_t, list); return strcmp(entry1->value, entry2->value); } ``` 以上是用 merge sort 實作,理論上時間複雜度為 $O(n \log n)$ ,而以測試的結果來說,該 sort 實作可以通過第 15 項測試,表示其時間複雜度確實在 $O(n \log n)$ ,然而此 sort 實作在第 14 項測試中會失敗。 接著引入 `lib/list_sort.c` ```c void q_sort(struct list_head *head) { // this function is rewrite from list_sort.c if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *list = head->next, *pending = NULL; size_t count = 0; head->prev->next = NULL; do { size_t bits; struct list_head **tail = &pending; for (bits = count; bits & 1; bits >>= 1) tail = &(*tail)->prev; if (likely(bits)) { struct list_head *a = *tail, *b = a->prev; a = merge(b, a); a->prev = b->prev; *tail = a; } list->prev = pending; pending = list; list = list->next; pending->next = NULL; count++; } while (list); list = pending; pending = pending->prev; for (;;) { struct list_head *next = pending->prev; if (!next) break; list = merge(pending, list); pending = next; } merge_final(head, pending, list); } int cmp(struct list_head *a, struct list_head *b) { element_t *entry1 = list_entry(a, element_t, list); element_t *entry2 = list_entry(b, element_t, list); return strcmp(entry1->value, entry2->value); } static struct list_head *merge(struct list_head *a, struct list_head *b) { struct list_head *head, **tail = &head; for (;;) { if (cmp(a, b) <= 0) { *tail = a; tail = &a->next; a = a->next; if (!a) { *tail = b; break; } } else { *tail = b; tail = &b->next; b = b->next; if (!b) { *tail = a; break; } } } return head; } static void merge_final(struct list_head *head, struct list_head *a, struct list_head *b) { struct list_head *tail = head; int count = 0; for (;;) { if (cmp(a, b) <= 0) { tail->next = a; a->prev = tail; tail = a; a = a->next; if (!a) break; } else { tail->next = b; b->prev = tail; tail = b; b = b->next; if (!b) { b = a; break; } } } tail->next = b; do { if (unlikely(!++count)) cmp(b, b); b->prev = tail; tail = b; b = b->next; } while (b); tail->next = head; head->prev = tail; } ``` 從 `list_sort.c` 改寫的 sort 實作可通過第 14 項測試,觀察兩種 sort function 在 `perf top` 中的使用情況,主要由兩項函式消耗大部分的資源,第一個是 sort 本身,第二個是 `strcmp` ,前後兩個相比 `strcmp` 消耗程度大致相同,因此合理推斷導致第 14 項測試結果出現分歧的原因,就是第一個 sort 實作使用的是 recuresive 的結構,而使得資料量大的時候會出現崩潰。 :::warning 需要更多分析和實驗,來支持你的論點。 :notes: jserv ::: ```c ==59110== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==59110== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==59110== Can't extend stack to 0x1ffe8010a8 during signal delivery for thread 1: ==59110== no stack segment ==59110== ==59110== Process terminating with default action of signal 11 (SIGSEGV) ==59110== Access not within mapped region at address 0x1FFE8010A8 ==59110== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==59110== at 0x10EED6: q_merge (queue.c:313) ==59110== If you believe this happened as a result of a stack ==59110== overflow in your program's main thread (unlikely but ==59110== possible), you can try to increase the size of the ==59110== main thread stack using the --main-stacksize= flag. ==59110== The main thread stack size used in this run was 8388608. ==59110== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==59110== ==59110== Process terminating with default action of signal 11 (SIGSEGV) ==59110== Access not within mapped region at address 0x1FFE801F70 ==59110== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==59110== at 0x4831130: _vgnU_freeres (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_core-amd64-linux.so) ==59110== If you believe this happened as a result of a stack ==59110== overflow in your program's main thread (unlikely but ==59110== possible), you can try to increase the size of the ==59110== main thread stack using the --main-stacksize= flag. ==59110== The main thread stack size used in this run was 8388608. Segmentation fault (core dumped) ``` 利用 `valgraind ./qtest` ,並執行如 14 項測試中的命令順序 : `new` -> `ih dolphin 1000000` -> `it gerbil 1000000` -> `reverse` -> `sort` 其中用 recursive 形式的 sort 實作會跑出以上的訊息,並以 `Segmentation fault (core sumped)` 的錯誤結束程式。 仔細看看錯誤訊息,可以發現出錯的原因是主程式 main 的 stack 沒有空間了,也就是大名鼎鼎的 stack overflow,這是因為函式在執行的時候,若是遇到要執行其他函式,則會把目前的參數值、變數以及其他函數結束後的返回位址 push 到該函式的 stack 中,直到要繼續執行才會 pop 出來,而也就是因為這個特性,使得 recursive 形式的函式雖然直觀但執行速度慢 (push, pop 都要花時間),而且還有發生 stack overflow 的風險。 ### shuffle 將佇列中的節點打亂,[演算法參考](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle) 以下是直接利用演算法的原理實作的,能夠運作但依然需要改進,尤其是面對巨量資料的時候。 ```c void q_shuffle(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; srand(time(0)); int len = q_size(head); struct list_head *tail = head->prev, *node = NULL; for (;len > 1; len--) { node = head->next; for (int i = rand() % len; i > 0; i--){ node = node->next; } swap_two_node(node, tail); tail = node->prev; } } void swap_two_node(struct list_head *a, struct list_head *b) { if (a->next == b){ a->prev->next = b; b->next->prev = a; a->next = b->next; b->prev = a->prev; a->prev = b; b->next = a; } else { struct list_head *tmp; a->prev->next = b; b->prev->next = a; tmp = a->prev; a->prev = b->prev; b->prev = tmp; a->next->prev = b; b->next->prev = a; tmp = a->next; a->next = b->next; b->next = tmp; } } ``` 因為 `queue.h` 不能更動的關係,需要額外寫一個 `.h` 檔來放置函式的宣告,如下,並在 `qtest.c` 中將其 include 進去 ```c #include "list.h" #include "queue.h" void q_shuffle(struct list_head *head); void swap_two_node(struct list_head *a, struct list_head *b); ``` 原本的演算法中,面對結構簡單的陣列可以達到 $O(n)$ 的時間複雜度,但在結構較複雜且記憶體不連續的 linked list 中,要還原演算法的話就必須花費 $O(n^2)$ 的時間複雜度在逐個尋找節點的過程上。 :::warning 總是書寫為 linked list,中間不要有連字號。 :notes: jserv ::: 而若不考慮完全按照演算法,不用交換 node 的方式來做,則可以不使用 swap 函式,改成如下,在指標的操作上次數會固定,而不需要進行條件判斷 ```c // swap_two_node(node, tail); // tail = node->prev; // | // v list_del(node); list_add(node, tail->prev); tail = node; ``` 完全移除 `swap_two_node`,改為用 list api 來實作 ```c void q_shuffle(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; srand(time(0)); int len = q_size(head); struct list_head *tail = head->prev, *node = NULL; for (;len > 1; len--) { node = head->next; for (int i = rand() % len; i > 0; i--){ node = node->next; } if (node->next != tail){ list_del(node); list_add(node, tail->prev); } tail = node; } } ``` ### web 使用 tiny-web-server 提供伺服器功能,目前如下的程式能符合伺服器運作的同時, `qtest` 仍可輸入<s>指令</s> 命令的要求,但是當 `tiny` 有輸出訊息時,畫面會被覆蓋掉,不影響運作。 :::danger instruction 是「指令」,command 是「命令」,二者語意不同。 :notes: jserv ::: ::: spoiler 錯誤嘗試 ```c // this code is writen in qtest.c, not queue.c static bool do_web(int argc, char *argv[]) { if (argc != 1) { report(1, "%s takes no arguments", argv[0]); return false; } // tiny is installed in different path, so path was given here int id = system("../tiny-web-server/tiny 9999 &"); if (id == -1) return false; return true; } ``` > 更新錯誤 : 當 `qtest` 結束執行時, `tiny` 程式不會結束執行,也無法用 `jobs` 命令查到。 :::danger 不要呼叫 `system` 函式,你應該引入 tiny-web-server 的原始程式碼並整合。 :notes: jserv ::: ### select 系統呼叫在本程式的使用方式 首先先看看 select 的用途,參考了[這篇](https://man7.org/linux/man-pages/man2/select.2.html)以及[這篇](http://www.unixlinux.online/unixlinux/unixzs/unixjczs/201703/94184.html),了解 select 中各項傳入參數所代表的意義。 `select(nfds, readfds, writefds, exceptfds, timeout)` 中, ndfs 用以表示可以檢查的 file descriptors 的範圍,中間三個參數個別表示指向存放 read, write, except 的三個 file descriptors set (fd_set) ,最後 timeout 參數則限制了 `select` 要等待多久,特別的是當 timeout 值為 NULL 時, `select` 會無限制的等下去。 當 select 回傳 -1 表示有錯誤發生,回傳 0 表示等待超時,而成功時則會回傳一個值,根據描述,這個值是大於0的,端看傳入的 fd_set 大小。 > the total number of bits that are set in readfds, writefds, exceptfds 回頭看 select 在程式中在何處使用,是在 console.c 的 `cmd_select` 中,並且依據 `cmd_select` 的使用, nfds, writefds, exceptfds, timeout 四個參數的傳入值都是固定的,分別是 0 以及三個 NULL ,這意味著 `select` 永遠只會看 read 的 fd_set 中的第一個,而且會無限制地等到該 fd 準備好,而該 fd 就是指向我們所輸入的命令ㄉ,或是 `source` 從檔案中讀到的第一個命令。 ### 運用 Valgrind 排除 qtest 實作的記憶體錯誤 如果輸入 `make valgrind` 來檢查程式運作,在將 queue.c 撰寫完成之後,仍可看到來自 linenoise.c 的錯誤發生,而主要是從 linenoiseHistoryAdd 的函式中所取的記憶體沒有完全釋放掉,而使部分資料仍 'reachable' ![](https://i.imgur.com/GPTLpoB.png) 先看這部分的程式碼是如何運作 ```c int linenoiseHistoryAdd(const char *line) { char *linecopy; if (history_max_len == 0) return 0; /* Initialization on first call. */ if (history == NULL) { history = malloc(sizeof(char *) * history_max_len); if (history == NULL) return 0; memset(history, 0, (sizeof(char *) * history_max_len)); } /* Don't add duplicated lines. */ if (history_len && !strcmp(history[history_len - 1], line)) return 0; /* Add an heap allocated copy of the line in the history. * If we reached the max length, remove the older line. */ linecopy = strdup(line); if (!linecopy) return 0; if (history_len == history_max_len) { free(history[0]); memmove(history, history + 1, sizeof(char *) * (history_max_len - 1)); history_len--; } history[history_len] = linecopy; history_len++; return 1; } ``` 這個函式看起來沒有問題,因此錯誤應該不是發生在函式內部,接著看發生 memory leak 的地方是在 qtest.c 呼叫 history load 時所產生的,推測是結束時 histroy 的記憶體沒有被釋放掉,所以再來看 main 結束之前的動作, TODO