# 2022q1 Homework1 (lab0) contributed by < [`0n3t04ll`](https://github.com/0n3t04ll) > ## 實驗環境 ```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: 36 bits physical, 48 bits virtual CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 2 Core(s) per socket: 2 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 58 Model name: Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz Stepping: 9 CPU MHz: 1300.000 CPU max MHz: 3100.0000 CPU min MHz: 1200.0000 BogoMIPS: 4988.51 Virtualization: VT-x L1d cache: 64 KiB L1i cache: 64 KiB L2 cache: 512 KiB L3 cache: 3 MiB NUMA node0 CPU(s): 0-3 ``` ## 針對佇列的操作 依據指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改,要能滿足 $ make test 自動評分系統的所有項目。 我的思路是先盡可能的利用 `list.h` 內的各個 function/macro 完成 list node 操作,避免自己進行手動操作,滿足評分系統後再對各項操作進行最佳化。 ### q_new > Create empty queue. > Return NULL if could not allocate space. ```c struct list_head *q_new() { struct list_head *head = (struct list_head *) malloc(sizeof(struct list_head)); if (head == NULL) return NULL; INIT_LIST_HEAD(head); return head; } ``` 這個實作困擾我最久的是不知道該不該新增一個用不到的 element 然後拿裡面 list 當作 head ,但看了 `list.h` 中的細節和回想起之前寫 leetcode linked list 相關題目提到的[虛擬頭節點](https://cloud.tencent.com/developer/article/1680952),我決定用一個 `struct list_head *head` 當作 head node 就好。 ### q_free > Free all storage used by queue ```c void q_free(struct list_head *l) { if (l == NULL) return; struct list_head *node; struct list_head *safe; list_for_each_safe (node, safe, l) { list_del(node); element_t *ele = list_entry(node, element_t, list); free(ele->value); free(ele); } if (list_empty(node)) free(node); } ``` 利用 `list_for_each_safe` 走訪每個 node ,並利用 `list_entry` 拿到保存 node 的 element ,移除後 free 掉 element 所有分配的空間 ### q_insert_node > > Attempt to insert element at head of queue. > Return true if successful. > Return false if q is NULL or could not allocate space. > Argument s points to the string to be stored. > The function must explicitly allocate space and copy the string into it. ```c bool q_insert_head(struct list_head *head, char *s) { if (head == NULL) return false; element_t *ele = (element_t *) malloc(sizeof(element_t)); if (ele == NULL) return false; ele->value = strdup(s); if (ele->value == NULL) { free(ele); return false; } struct list_head *new_node = &(ele->list); list_add(new_node, head); // cppcheck-suppress memleak return true; } ``` 新增節點的部分我直接分配一個 `element_t` 的變數 `ele`, `ele` 的 value 則直接用 `strdup(const char *)` 分配空間並保存 `s` 內容。 這邊讓我最為疑惑的是 cppcheck 會出現 `memleak` 錯誤訊息,我猜是因為 cppcheck 檢查時看到 `ele` 被分配出來卻沒檢查出哪裡可以 free 掉的原因,嘗試用 valgrind ,目前是沒發現有 memleak 問題,所以先用 `//cppcheck-suppress memleak` 關掉提醒。 ### q_insert_tail > >Attempt to insert element at tail of queue. >Return true if successful. >Return false if q is NULL or could not allocate space. >Argument s points to the string to be stored. >The function must explicitly allocate space and copy the string into it. ```c bool q_insert_tail(struct list_head *head, char *s) { if (head == NULL) return false; element_t *ele = (element_t *) malloc(sizeof(element_t)); if (ele == NULL) return false; ele->value = strdup(s); if (ele->value == NULL) { free(ele); return false; } struct list_head *new_node = &(ele->list); list_add_tail(new_node, head); // cppcheck-suppress memleak return true; } ``` 作法跟 `q_insert_head` 大同小異,差別在於因為要加到 tail ,所以用 `list_add_tail`。 這邊一樣有 memleak 警告問題,一樣用 `//cppcheck-suppress memleak` 解決 ### q_remove_head >Attempt to remove element from head of queue. Return target element. Return NULL if queue is NULL or empty. If sp is non-NULL and an element is removed, copy the removed string to *sp (up to a maximum of bufsize-1 characters, plus a null terminator.) ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (head == NULL || list_empty(head)) return NULL; element_t *ele = list_first_entry(head, element_t, list); list_del_init(&(ele->list)); if (sp) { strncpy(sp, ele->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return ele; } ``` 用 `list_first_entry` 取出第一個 entry 後根據此 element 進行字串複製和移除等操作。 要注意的是記得補個 terminate byte 在 sp 最後一個 byte 上 ### q_remove_tail >Attempt to remove element from tail of queue. Other attribute is as same as q_remove_head. ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (head == NULL || list_empty(head)) return NULL; element_t *ele = list_last_entry(head, element_t, list); list_del_init(&(ele->list)); if (sp) { strncpy(sp, ele->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return ele; } ``` 跟 `q_remove_head` 大同小異,差別在於取 first_entry 還是 last_entry 而已。 ### q_size >Return number of elements in queue. Return 0 if q is NULL or empty ```c int q_size(struct list_head *head) { if (head == NULL || list_empty(head)) return 0; int size = 0; struct list_head *node; list_for_each (node, head) size++; return size; } ``` 以 `list_for_each` 走訪不包含 head 在內的所有節點並記錄數量。 ### q_delete_mid >Delete the middle node in list. The middle node of a linked list of size n is the ⌊n / 2⌋th node from the start using 0-based indexing. If there're six element, the third member should be return. Return true if successful. Return false if list is NULL or empty. ```c bool q_delete_mid(struct list_head *head) { if (head == NULL || list_empty(head)) return false; struct list_head *two, *one, *end; two = head->next; one = two; end = head->prev; while (two->next != end && two != end) { two = two->next->next; one = one->next; } list_del(one); element_t *ele = list_entry(one, element_t, list); free(ele->value); free(ele); // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/ return true; } ``` 這題可以透過 two-pointer 技巧解決,概念是一個 pointer 一次走訪兩個,一個 pointer 一次走訪一個,當走訪兩個的走到底了則走訪的一個的位置就會是 middle node 。 查看了 `list.h` ,對於走訪相關的巨集,沒有直接自己手動操作來的方便,所以就直接操作。拿到 middle 後用將其從 list 中移除並 free 掉 ele 。 ### q_delete_dup >Delete all nodes that have duplicate string, leaving only distinct strings from the original list. Return true if successful. Return false if list is NULL. ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (head == NULL) return false; if (list_empty(head) || list_is_singular(head) == 1) return true; struct list_head *curr = head->next; element_t *curr_ele = list_entry(curr, element_t, list); char *last_str = curr_ele->value; curr = curr->next; while (curr != head) { curr_ele = list_entry(curr, element_t, list); char *curr_str = curr_ele->value; if (strcmp(curr_str, last_str) == 0) { struct list_head *tmp = curr; curr = curr->next; list_del(tmp); free(curr_ele->value); free(curr_ele); } else { last_str = curr_str; curr = curr->next; } } return true; } ``` 因為註解有提到會在 sorted 後執行這個 function ,因此我假設 list 已經被 sorted ,用 `last` 和 `curr` 兩個 prefix 表示當前的 node 和 string 以及上一次的 node 和 string ,判斷一樣將 `curr` 從 list 中移除後 free 掉其空間 ### q_swap >Attempt to swap every two adjacent nodes. ```c void q_swap(struct list_head *head) { if (head == NULL || list_empty(head)) return; // https://leetcode.com/problems/swap-nodes-in-pairs/ struct list_head *fst, *sec; fst = head->next; sec = fst->next; while (sec != head && fst != head) { list_move(fst, sec); fst = fst->next; sec = fst->next; } } ``` 這個實作就是把第二個 node 用 `list_move` 移動到第一個前面即可 ### q_reverse >Reverse elements in queue No effect if q is NULL or empty This function should not allocate or free any list elements ```c void q_reverse(struct list_head *head) { if (head == NULL || list_empty(head)) return; struct list_head *last = head->prev; struct list_head *sec_last = last->prev; struct list_head *last_tail = last; while (sec_last != head) { list_move(sec_last, last_tail); last_tail = sec_last; sec_last = last->prev; } } ``` 這個實作我的想法是把最後一個 node 當 head ,然後把倒數第二個 node 移動到 head 的下一個,最後更新 head 到剛剛移動過來的 node 上,而最後一個 node 則被更新為 `last->prev` ### q_sort >No effect if q is NULL or empty. In addition, if q has only one element, do nothing. 原本評估一個沒品味的寫法應該一下子就能完成,之後再去研究怎麼最佳化,結果 commit 的時候直接被告知不能用 `malloc()` ,這個限制讓我想了好一陣子,少了任意增加 head node 對於想徹底利用 `list.h` function 的方式來說無疑會增加實作的複雜度。 [Merge two sorted linked lists](https://www.geeksforgeeks.org/merge-two-sorted-linked-lists/) 一文的作法是利用 local variable 當作 head node ,因為是 local variable 所以可以繞過 malloc 檢查,而上一層的 local variable 當作 pointer 傳遞給下一層的 function 使用也不會出現問題,不過追根究柢本質上還是有 allocate 新的空間出來,並不符合要求。 > 我覺得儘管沒品味寫法的確比較不優雅,但因為都保持有 head node 的 circular doubly-linked list 模式所以多數情況能充分利用 `list.h` 提供的 function/macro 。 #### 沒品味寫法 我主要分為三個 function: * merge_sort ```c static void merge_sort(struct list_head *head) { if (head == NULL) return; else if (list_empty(head)) return; else if (list_is_singular(head)) return; struct list_head l, r; INIT_LIST_HEAD(&l); INIT_LIST_HEAD(&r); struct list_head *mid = find_middle(head); // split list_cut_position(&l, head, mid); list_cut_position(&r, head, head->prev); merge_sort(&l); merge_sort(&r); merge(head, &l, &r); return; } ``` 利用 local variable 當作左右切割後的 list 的 head node ,用 `list_cut_position` 切割原來的 list ,將其分為左右兩邊,左右兩邊的 list 也要繼續遞迴下去,最後用 `merge` 將 l 和 r 兩個 list 作合併並放入 head node 的 list 中 * merge ```c static void merge(struct list_head *head, struct list_head *a, struct list_head *b) { struct list_head *curr_a = a->next; struct list_head *curr_b = b->next; struct list_head *last_b, *last_a; while (curr_a != a && curr_b != b) { if (cmp(curr_a, curr_b)) { last_a = curr_a; curr_a = curr_a->next; list_move_tail(last_a, head); } else { last_b = curr_b; curr_b = curr_b->next; list_move_tail(last_b, head); } } if (curr_a != a) { list_splice_tail(a, head); } if (curr_b != b) { list_splice_tail(b, head); } } ``` 根據 `cmp` 結果,決定放入哪一個 list 的當前 node * cmp * 根據 strcmp 的 return 值判斷 #### 有品味寫法 老師的講義上其實就有關於 linked list merge sort 的實作方式,不過作法是基於 singly-linked list ,這次用的 linked list 是更高階的 circular doubly-linked list ,所以可以~~自廢武功降級~~退化成 singly-linked list ,然後套用相關作法,但事後要再補回 prev pointer 和 head node 的連接。 :::warning 這不是「降級」,只是某幾個工程選擇。工程師要解決各式難題,這個作業就是藉由事先給予的限制,讓學員進行練習。 :notes: jserv ::: * q_sort ```c void q_sort(struct list_head *head) { if (head == NULL || list_empty(head) || list_is_singular(head)) return; struct list_head *list = head->next; head->prev->next = NULL; merge_sort(&list); // recovery struct list_head *curr = list->next; struct list_head *last = list; last->prev = head; head->next = last; while (curr) { curr->prev = last; curr = curr->next; last = last->next; } last->next = head; head->prev = last; } ``` q_sort 負責初始的降級成 singly-linked list 和最後的恢復動作,其餘交給 merge_sort 負責。 * merge_sort ```c void merge_sort(struct list_head **list) { *list = merge_sort_list(*list); } ``` merge_sort 本身所傳參數 list 是負責保存最後 sorted list 的第一個 node 地址。 因為要讓傳入參數保持第一個 entry node 的定位,所以用 indirect pointer ,這樣透過 `*list` 就可以將新的 entry node address 保存在 q_sort 的 `list` 內,如此 q_sort 的 `list` 在 sort 前是 first entry node ,在 sort 後依然是 first entry node 。 * merge_sort_list ```c struct list_head *merge_sort_list(struct list_head *head) { if (!head || !head->next) return head; struct list_head *fast, *slow; slow = head; fast = slow->next; for (; fast && fast->next; fast = fast->next->next) { slow = slow->next; } struct list_head *mid = slow->next; slow->next = NULL; struct list_head *l = merge_sort_list(head), *r = merge_sort_list(mid); return merge(l, r); } ``` merge_sort_list 每次切割成左右兩個 list 都會需要對 list 進行 singly link list 相關處理,最後將被排序過後的兩個 list 放到 merge 合併。 * merge ```c struct list_head *merge(struct list_head *l, struct list_head *r) { struct list_head *head = NULL; struct list_head **ptr = &head; for (; l && r; ptr = &(*ptr)->next) { element_t *ele_l = list_entry(l, element_t, list); element_t *ele_r = list_entry(r, element_t, list); if (strcmp(ele_l->value, ele_r->value) <= 0) { *ptr = l; l = l->next; } else { *ptr = r; r = r->next; } } if (l) *ptr = l; if (r) *ptr = r; return head; } ``` 第一種寫法就像是在兩個 list 之外再用第三個 list 保存前兩個 list 的結果,如果利用上面的 indirect pointer 技巧相當於將兩個 list 根據需求取其中的 node 串連在一起,無須第三的 list 保存。 > 雖然 code 看得懂,但是對於 indirect pointer 的使用還沒到很熟悉,無法順暢想到寫出來。 :::warning TODO: * 合併最佳化 * 效能評估 * 圖形化顯示 ::: ## 運用 Valgrind 排除 qtest 實作的記憶體錯誤 手動的部分在實作 queue operation 的時候就因為 cppcheck 的誤報而用 valgrind 檢測過,沒發現問題。 實作完成後自己添加了一些測試 .cmd 檔案給 `./qtest` 執行並用 `valgrind` 做 mem-leak check: `$ valgrind --leak-check=full ./qtest -f ./cmd2` 出現如下錯誤訊息: ``` starpt@ubuntu:~/lab0-c$ valgrind --leak-check=full ./qtest -f cmd2 cmd> new l = [] cmd> ih 1 l = [1] cmd> quit Freeing queue ==120066== 5 bytes in 1 blocks are still reachable in loss record 1 of 3 ==120066== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==120066== by 0x4A5750E: strdup (strdup.c:42) ==120066== by 0x1108ED: linenoiseHistoryAdd (linenoise.c:1236) ==120066== by 0x111480: linenoiseHistoryLoad (linenoise.c:1325) ==120066== by 0x10C6B0: main (qtest.c:951) ==120066== ==120066== 88 bytes in 19 blocks are still reachable in loss record 2 of 3 ==120066== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==120066== by 0x4A5750E: strdup (strdup.c:42) ==120066== by 0x110861: linenoiseHistoryAdd (linenoise.c:1236) ==120066== by 0x111480: linenoiseHistoryLoad (linenoise.c:1325) ==120066== by 0x10C6B0: main (qtest.c:951) ==120066== ==120066== 160 bytes in 1 blocks are still reachable in loss record 3 of 3 ==120066== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==120066== by 0x1108AD: linenoiseHistoryAdd (linenoise.c:1224) ==120066== by 0x111480: linenoiseHistoryLoad (linenoise.c:1325) ==120066== by 0x10C6B0: main (qtest.c:951) ==120066== ``` 原先以為是我的 code 寫錯了,所以一直修改 cmd2 ,想藉此查出是哪個操作寫錯,但用手動執行同樣的操作卻沒任何問題,透過 `valgrind` 提供的 backtrace 也可以看出問題不是出在我寫的 function 內,而是在 `linenoiseHistoryAdd`。 為了方便 trace code ,我根據 [[Linux] - 將vim打造成source insight](https://ivan7645.github.io/2016/07/12/vim_to_si/) 這篇文章安裝了 `cscope` 和 `ctags` 幫助我 debug 。 ### Tracing 從上而下追蹤下去可以發現: * main ```c #define BUFSIZE 256 int main(int argc, char *argv[]) { /* ... ignore ... */ /* Trigger call back function(auto completion) */ linenoiseSetCompletionCallback(completion); linenoiseHistorySetMaxLen(HISTORY_LEN); linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ ``` `main` 會執行 history 相關初始化 * linenoiseHistoryLoad ```c int linenoiseHistoryLoad(const char *filename) { FILE *fp = fopen(filename, "r"); char buf[LINENOISE_MAX_LINE]; if (fp == NULL) return -1; while (fgets(buf, LINENOISE_MAX_LINE, fp) != NULL) { char *p; p = strchr(buf, '\r'); if (!p) p = strchr(buf, '\n'); if (p) *p = '\0'; linenoiseHistoryAdd(buf); } fclose(fp); return 0; } ``` `linenoiseHistoryLoad` 負責讀取 `filename` 的每一行並用 terminate byte 截斷再將處理過的字串放到 `linenoiseHistoryAdd` * linenoiseHistoryAdd ```c int linenoiseHistoryAdd(const char *line) { /* ...history initial...*/ /* Add an heap allocated copy of the line in the history. * If we reached the max length, remove the older line. */ linecopy = strdup(line); /* ...linecopy check...*/ history[history_len] = linecopy; history_len++; return 1; } ``` `linenoiseHistoryAdd` 會分配一個字元指標陣列 `char *[]` 給全域的 `history` 變數保存傳進來的,參數 `line` 透過 `strdup` 自動分配 heap 空間保存 `line` 字串,而後放入 `history` 陣列中,這也符合 `valgrind` backtrace 上面提示的位置。 ### 手動操作的差異 我接著想到 `linenoiseHistoryLoad` 無論讀檔或標準輸入都會執行,所以標準輸入應該也有這個字元指標陣列,那為何不會出現 memleak ? 我的方法是用 [ag](https://github.com/ggreer/the_silver_searcher) 直接搜尋 `free(history[` ,從中發現了: ``` linenoise.c 786: free(history[history_len - 1 - l->history_index]); 910: free(history[history_len]); 935: free(history[history_len]); 1196: free(history[j]); 1240: free(history[0]); 1271: free(history[j]); ``` 逐一對每一行進行 code review ,其中 `linenoise.c:1196` 是以下的程式碼: ```c static void freeHistory(void) { if (history) { int j; for (j = 0; j < history_len; j++) free(history[j]); // line: 1196 free(history); } } ``` 這邊應該就是負責結束的時候釋放掉 `history` 內所保存的字串和 `history` ,接著就是找是誰負責呼叫這個 function ,可以發現 `freeHistory` 在 `linenoiseAtExit` 中被呼叫,而 `linenoiseAtExit` 則是在 `enableRawMode` 中被當成參數傳給 `atexit` 。 ### 產生 call graph 方便比較、查看 #### kcachegrind 根據 [Tools to get a pictorial function call graph of code [closed]](https://stackoverflow.com/questions/517589/tools-to-get-a-pictorial-function-call-graph-of-code) 這篇 stackoverflow 選用 `valgrind` + `kcachegrind` 組合輸出並查看 call graph: `$ valgrind --tool=callgrind ./qtest` 這樣會輸出 `callgrind.out.[pid]` 的檔案,再用 `kcachegrind` 開啟: `$ kcachegrind ./callgrind.out.[pid]` 根據需求選擇想要看的 function 後可以輸出 dot graph 這樣就會有 call graph 可以查看: ```graphviz digraph "callgraph" { F55a4db090870 [label="linenoise\n1 122"]; F55a4db297f90 [label="run_console\n1 122"]; F55a4db7d5710 [label="enableRawMode\n1 122"]; F55a4db7d5d10 [label="0x000000000010a520\n275"]; F55a4db7d64b0 [label="0x000000000010a790\n200"]; F55a4db7d68a0 [label="0x000000000010a7a0\n330"]; F55a4db7d6c90 [label="atexit\n78"]; F55a4db7df560 [label="0x000000000010a800\n74"]; F55a4db8152b0 [label="__cxa_atexit\n72"]; F55a4db819f90 [label="tcgetattr\n378"]; F55a4db81b6e0 [label="isatty\n265"]; F55a4db8371c0 [label="tcsetattr\n320"]; F55a4db090870 -> F55a4db7d5710 [weight=1,label="1 122 (5x)"]; F55a4db297f90 -> F55a4db090870 [weight=1,label="1 122 (1x)"]; F55a4db7d5710 -> F55a4db7d5d10 [weight=1,label="275 (5x)"]; F55a4db7d5710 -> F55a4db7d64b0 [weight=1,label="200 (5x)"]; F55a4db7d5710 -> F55a4db7d68a0 [weight=1,label="330 (5x)"]; F55a4db7d5710 -> F55a4db7d6c90 [weight=1,label="78 (1x)"]; F55a4db7d5d10 -> F55a4db81b6e0 [weight=1,label="265 (5x)"]; F55a4db7d64b0 -> F55a4db819f90 [weight=1,label="190 (5x)"]; F55a4db7d68a0 -> F55a4db8371c0 [weight=1,label="320 (5x)"]; F55a4db7d6c90 -> F55a4db7df560 [weight=1,label="74 (1x)"]; F55a4db7df560 -> F55a4db8152b0 [weight=1,label="72 (1x)"]; F55a4db81b6e0 -> F55a4db819f90 [weight=1,label="188 (5x)"]; } ``` >友情提示: kcachegrind 容量不小,要 193MB #### gprof2dot 亦或者透過 `gprof2dot` 這個 python module 也可以根據 valgrind 產生的 `callgrind` 檔案輸出 dotgraph : `$ gprof2dot -e0 -n0 -f callgrind callgrind.out.120351 -z main -l enableRawMode` 說明: * `-e0 -n0`:解除顯示限制,完整呈現所有調用鏈 * `-f callgrind` :說明檔案是經過 `callgrind` 產稱 * `-z main -l enableRawMode`:設定 root 為 `main`,leaf 為 `enableRawMode` 簡單介紹可以參考 [動態分析 C 程序函數調用關係](https://www.cntofu.com/book/46/tinyclub/dong_tai_fen_xi_c_cheng_xu_han_shu_diao_yong_guan_.md)。 ```graphviz digraph { graph [fontname=Arial, nodesep=0.125, ranksep=0.25]; node [fontcolor=white, fontname=Arial, height=0, shape=box, style=filled, width=0]; edge [fontname=Arial]; enableRawMode [color="#0d0e73", fontcolor="#ffffff", fontsize="10.00", label="qtest\nenableRawMode\n0.28%\n(0.06%)\n5×"]; linenoise [color="#0d1976", fontcolor="#ffffff", fontsize="10.00", label="qtest\nlinenoise\n2.93%\n(0.31%)\n6×"]; linenoise -> enableRawMode [arrowsize="0.35", color="#0d0e73", fontcolor="#0d0e73", fontsize="10.00", label="0.28%\n5×", labeldistance="0.50", penwidth="0.50"]; main [color="#13b809", fontcolor="#ffffff", fontsize="10.00", label="qtest\nmain\n51.39%\n(0.04%)\n1×"]; main -> "run_console" [arrowsize="0.50", color="#0c9393", fontcolor="#0c9393", fontsize="10.00", label="24.89%\n1×", labeldistance="1.00", penwidth="1.00"]; "run_console" [color="#0c9393", fontcolor="#ffffff", fontsize="10.00", label="qtest\nrun_console\n24.89%\n(0.02%)\n1×"]; "run_console" -> linenoise [arrowsize="0.35", color="#0d1976", fontcolor="#0d1976", fontsize="10.00", label="2.93%\n6×", labeldistance="0.50", penwidth="0.50"]; } ``` :exclamation: 不可以拿 `linenoiseAtExit` 當作參考搜尋 call graph ,因為 `linenoiseAtExit` 是當作參數傳入 `atexit` ,只會在 `exit` 的時候呼叫。 接著根據 call graph 分析 `run_console`: ```c= bool run_console(char *infile_name) { if (!push_file(infile_name)) { report(1, "ERROR: Could not open source file '%s'", infile_name); return false; } if (!has_infile) { char *cmdline; while ((cmdline = linenoise(prompt)) != NULL) { interpret_cmd(cmdline); linenoiseHistoryAdd(cmdline); /* Add to the history. */ linenoiseHistorySave(HISTORY_FILE); /* Save the history on disk. */ linenoiseFree(cmdline); } } else { while (!cmd_done()) cmd_select(0, NULL, NULL, NULL, NULL); } return err_cnt == 0; } ``` line 10 的 `linenoise` 內部就會調用 `enableRawMode` 然後設定 `atexit(linenoiseAtExit)` 根據 code 可以得出 `infile_name` 為空則進入 `linenoise` 。 最後就是回到 `main` 找出 `infile_name` 為空的關鍵是**有無透過 `-f` 設定讀取的檔案**。 ### 整理 簡單整理下: * 沒有 `-f`:會設定清空 `history` 字元指標陣列,操作過程也會一直修改 `history` 陣列的內容,調用 `exit` 的時候會正常清空 `history` * 有 `-f`:只會在剛開始初始化的時候用到,之後不會用到 因此我覺得把在 `main` 裡面的初始化 function :`linenoiseHistorySetMaxLen` 、 `linenoiseHistoryLoad` 放到 `run_console` 處理沒有 `-f` 的 case 中應該比較恰當,也只有他會用到這些 history 。 ### diff ```diff= diff --git a/console.c b/console.c index 6ad338a..e8e555b 100644 --- a/console.c +++ b/console.c @@ -17,6 +17,10 @@ #include "report.h" +/* Settable parameters */ + +#define HISTORY_LEN 20 + /* Some global values */ int simulation = 0; static cmd_ptr cmd_list = NULL; @@ -644,6 +648,8 @@ bool run_console(char *infile_name) } if (!has_infile) { + linenoiseHistorySetMaxLen(HISTORY_LEN); + linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ char *cmdline; while ((cmdline = linenoise(prompt)) != NULL) { interpret_cmd(cmdline); diff --git a/qtest.c b/qtest.c index dd40972..3845b55 100644 --- a/qtest.c +++ b/qtest.c @@ -947,8 +947,6 @@ int main(int argc, char *argv[]) /* Trigger call back function(auto completion) */ linenoiseSetCompletionCallback(completion); - linenoiseHistorySetMaxLen(HISTORY_LEN); - linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ set_verblevel(level); if (level > 1) { set_echo(true); ``` ### 成果 ``` starpt@ubuntu:~/lab0-c$ valgrind --leak-check=full ./qtest -f ./cmd2 cmd> new l = [] cmd> ih 1 l = [1] cmd> quit Freeing queue ``` ## 開啟 Address Sanitizer,修正 qtest 執行過程中的錯誤 重新查看作業要求,發現少看了這一條。 在完成 queue operation 的前提下,我嘗試修改 `Makefile` 中的 `CFLAGS` ,試圖加上 `-fsanitize=address` 結果會出現: ```shell /usr/bin/ld: linenoise.o: in function `_sub_D_00099_0': /home/starpt/lab0-c/linenoise.c:1329: undefined reference to `__asan_unregister_globals' /usr/bin/ld: linenoise.o: in function `_sub_I_00099_1': /home/starpt/lab0-c/linenoise.c:1329: undefined reference to `__asan_init' /usr/bin/ld: /home/starpt/lab0-c/linenoise.c:1329: undefined reference to `__asan_version_mismatch_check_v8' /usr/bin/ld: /home/starpt/lab0-c/linenoise.c:1329: undefined reference to `__asan_register_globals' collect2: error: ld returned 1 exit status make: *** [Makefile:45: qtest] Error 1 ``` 因為錯誤太多,我只節錄一小段。 根據 [How to use AddressSanitizer with GCC?](https://stackoverflow.com/questions/37970758/how-to-use-addresssanitizer-with-gcc) ,不只 `CFLAGS` 要加,連 `LD_FLAGS` 也要加,不過在尋找的 `LD_FLAGS` 的時候發現: ``` # Enable sanitizer(s) or not ifeq ("$(SANITIZER)","1") # https://github.com/google/sanitizers/wiki/AddressSanitizerFlags CFLAGS += -fsanitize=address -fno-omit-frame-pointer -fno-common LDFLAGS += -fsanitize=address endif ``` 其實只要用以下方式就可以開啟 sanitizer : ```shell= $ make SANITIZER=1 ``` 不過就算我退回到完成 queue operation 的 commit ,按照指示執行 `./qtest` 後輸入 `help` 還是沒觸發 sanitizer ,於是暫時擱置。 ## ## 在 qtest 提供新的命令 shuffle > 在 qtest 提供新的命令 shuffle,允許藉由 Fisher–Yates shuffle 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作 看了 README.md 沒找到如何新增新命令的說明,於是從現有的命令實作去推敲。 根據執行 `./qtest` 顯示的字串可以在 qtest.c 找到 `console_init` : ```c static void console_init() { ADD_COMMAND(new, " | Create new queue"); ADD_COMMAND(free, " | Delete queue"); ADD_COMMAND( ih, " str [n] | Insert string str at head of queue n times. " "Generate random string(s) if str equals RAND. (default: n == 1)"); ADD_COMMAND( it, " str [n] | Insert string str at tail of queue n times. " "Generate random string(s) if str equals RAND. (default: n == 1)"); ADD_COMMAND( rh, " [str] | Remove from head of queue. Optionally compare " "to expected value str"); /* ... ignore other lines ...*/ ``` 根據格式可以新增命令 `shuffle` 如下: ```c ADD_COMMAND(shuffle, " | Shuffle all the queue nodes"); ``` `ADD_COMMAND` macro 會把 `shuffle` 加工,實際處理完的 function 名稱會是 `do_shuffle` 。 `do_shuffle` 則仿造其他 function : ```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"); error_check(); if (l_meta.size == 1) return true; set_noallocate_mode(true); if (exception_setup(true)) q_shuffle(l_meta.l); exception_cancel(); set_noallocate_mode(false); show_queue(3); return !error_check(); } ``` `l_meta` 是負責保存 queue 和 size 的結構,可以看成 queue 的包裝, `do_shuffle` 實際核心 function 是 `q_shuffle`。 我本來想在 `queue.c` 和 `queue.h` 裡面加上 `q_shuffle` ,但是 commit 時被警告不準修改 `queue.h` ,只好直接加在 `qtest.h` 內。 `q_shuffle` 主要由 `swap_ele_value` 和 `q_shuffle` 組成。 * `swap_ele_value` 直接交換兩個 node 的 value 比較方便 ```c static void swap_ele_value(struct list_head *a, struct list_head *b) { if (a == b) return; element_t *ele_a = list_entry(a, element_t, list); element_t *ele_b = list_entry(b, element_t, list); char *tmp = ele_a->value; ele_a->value = ele_b->value; ele_b->value = tmp; } ``` * `q_shuffle` shuffle 本體,從 `/dev/urandom` 取值當作隨機數種子,拿不到再考慮用時間。 ```c static void q_shuffle(struct list_head *head) { if (!head || list_empty(head)) return; if (list_is_singular(head)) return; // set random seed int fd = open("/dev/urandom", O_RDONLY, 0); if (fd < 0) return; int seed; ssize_t nb = read(fd, &seed, sizeof(int)); if (nb < 0) srand(time(NULL)); else srand(seed); close(fd); struct list_head *last = head->prev; struct list_head *chose = head->next; int size = q_size(head); while (size) { int rand_idx = rand() % size; if (rand_idx != size - 1) { for (int i = 0; i < rand_idx; i++) { chose = chose->next; } swap_ele_value(chose, last); } size--; chose = head->next; last = last->prev; } } ``` ## 在 qtest 提供新的命令 web