# 2021q1 Homework1 (lab0) contributed by < `mingyen066` > ## Checklist * [x] q_new * [x] q_free * [x] q_insert_head * [x] q_insert_tail * [x] q_remove_head * [x] q_reverse * [x] q_sort * [ ] 開啟 Address Sanitizer,修正 qtest 執行過程中的錯誤 * [ ] 運用 Valgrind 排除 qtest 實作的記憶體錯誤,並透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗 * [ ] 在 qtest 中實作 coroutine,並提供新的命令 web,提供 web 伺服器功能 * [ ] 解釋 select 系統呼叫在本程式的使用方式,並分析 console.c 的實作,說明其中運用 CS:APP RIO 套件 的原理和考量點。 * [ ] 說明 antirez/linenoise 的運作原理,注意到 termios 的運用 * [ ] 研讀論文 Dude, is my code constant time?,解釋本程式的 “simulation” 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 Student’s t-distribution 及程式實作的原理。 * [ ] 指出現有程式的缺陷 (提示: 和 RIO 套件 有關),嘗試強化並提交 pull request # 安裝說明 因為去年有 fork 過 lab0 這個專案,但是原本專案內容有更新, 所以去查詢了如何使用 upstream 來更新 fork 的專案的方法 :::warning 更正:不要用 upstream 來 merge,該重新 fork 或用 rebase ::: 先進入到專案目錄底下,列出目前有的 stream ```bash= $ git remote -v origin https://github.com/mingyen066/lab0-c.git (fetch) origin https://github.com/mingyen066/lab0-c.git (push) ``` 再加入想要 Sync 的專案網址 ```bash= $ git remote add upstream https://github.com/sysprog21/lab0-c ``` 確認已經加入完成 ```bash= $ git remote -v origin https://github.com/mingyen066/lab0-c.git (fetch) origin https://github.com/mingyen066/lab0-c.git (push) upstream https://github.com/sysprog21/lab0-c (fetch) upstream https://github.com/sysprog21/lab0-c (push) ``` 再進行 fetch, fetch 下來的 code 會儲存在 upstream/`<branchname>` ```bash= $ git fetch upstream remote: Enumerating objects: 10, done. remote: Counting objects: 100% (10/10), done. remote: Total 17 (delta 10), reused 10 (delta 10), pack-reused 7 Unpacking objects: 100% (17/17), 4.44 KiB | 84.00 KiB/s, done. From https://github.com/sysprog21/lab0-c * [new branch] master -> upstream/master ``` 再 merge 到自己的專案 ```bash= $ git merge upstream/master Updating 959d72e..81079de Fast-forward .gitignore | 1 + qtest.c | 12 ++++++++++-- scripts/commit-msg.hook | 3 +++ scripts/pre-commit.hook | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) ``` :::danger 不該用 merge,改用 git rebase 或者重建 repository :notes: jserv ::: :::info 已重建 repository ::: ## 實作說明 考慮到程式碼簡潔性,把所有下列類似程式碼 ```c= if (q == NULL) if (q != NULL) ``` 改成 ```c= if (q) if (!q) ``` ### queue.h `struct list_ele_t` 維持不變 在 `struct queue_t` 中則加入指標 `tail` 以及整數`q_size`,以便達到 `insert_tail` 在時間複雜度 O(1) 完成的要求 ```c= typedef struct { list_ele_t *head; /* Linked list of elements */ list_ele_t *tail; int q_size; } queue_t; ``` ### queue.c ### q_new 需注意判斷 `malloc()` 是否回傳NULL ```c= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (!q) return NULL; q->q_size = 0; q->tail = NULL; q->head = NULL; return q; } ``` ### q_free 當有節點不為空時,我們必須先釋放節點的 value ,再釋放節點,我原本擔心萬一節點的值是 NULL 怎麼辦,所以我查了 free() 的man page , 根據free() 的 man page: > The free() function frees the memory space pointed to by ptr ... > If ptr is NULL, no operation is performed. 所以我們可以不用擔心 free 到 NULL pointer ```c= void q_free(queue_t *q) { if (!q) return; while (q->head) { list_ele_t *tmp = q->head; q->head = q->head->next; free(tmp->value); free(tmp); } free(q); } ``` ### q_insert_head 如果 malloc 節點成功,但是 malloc 節點的 value 失敗,必須把節點的記憶體釋放掉。 另外要注意在 malloc 跟 strncpy 時,都要把 C 字串最後的 null charactor('\0') 考慮進去,因為 strlen() 並沒有算到 null charactor ```c= bool q_insert_head(queue_t *q, char *s) { if (!q) return false; list_ele_t *newh = malloc(sizeof(list_ele_t)); if (!newh) return false; newh->value = malloc(strlen(s) + 1); if (!newh->value) { free(newh); return false; } strncpy(newh->value, s, strlen(s) + 1); q->q_size++; newh->next = q->head; q->head = newh; if (q->q_size == 1) { q->tail = q->head; } return true; } ``` ### q_insert_tail 跟 insert_head 要注意的地方相同,另外要考慮當 queue 為空時的特殊情況 ```c= bool q_insert_tail(queue_t *q, char *s) { if (!q) return false; list_ele_t *newt = malloc(sizeof(list_ele_t)); if (!newt) return false; newt->value = malloc(strlen(s) + 1); if (!newt->value) { free(newt); return false; } strncpy(newt->value, s, strlen(s) + 1); newt->next = NULL; if (!q->tail) { q->tail = newt; q->head = newt; } else { q->tail->next = newt; q->tail = q->tail->next; } q->q_size++; return true; } ``` ### q_remove_head 根據 strncpy 的 mag page: > The strncpy() function is similar, except that at most n bytes of src are copied. Warning: If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated. 了解當要移除字串的前 bufsize 長度裡,如果沒有 null byte,則 copy 到 sp 上也會沒有null byte,所以當要移除的字串長度 (strlen) 跟 bufsize 比,相同或較長時,必須在sp的結果加上 null charactor (`'\0'`) ```c= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || !q->head) return false; if (sp) { strncpy(sp, q->head->value, bufsize - 1); if (strlen(q->head->value) >= bufsize) sp[bufsize - 1] = '\0'; } list_ele_t *tmp = q->head; q->head = q->head->next; q->q_size--; free(tmp->value); free(tmp); return true; } ``` ### q_reverse 這裡我用 \*prev 表示要反轉節點的前一個節點, \*next表示要移除節點的下個節點, 前一個節點跟目前節點已經斷開連結,換而言之,包含 prev 以前的串列是已經反轉過的。 圖例如下: Linked List 結構為a->b->c,prev 指向 NULL, head 指向將要反轉的串列開頭 ```graphviz digraph { rankdir=LR; size=4; node [shape=record] subgraph pointer{ a [label = "{<in>NULL |<out>}"] b [label = "{<in>a |<out>}"] c [label = "{<in>b |<out>}"] d [label = "{<in>c |<out>}"] } subgraph nodes{ label="pointer" prev [label = "prev", shape=circle] head [label = "head", shape=circle] } prev->a b->c c->d head->b } ``` 接著把 next 指向 head 的下個節點 b,並把 a 的 next 改為 prev 所指的節點 NULL ```graphviz digraph { rankdir=LR; size=4; node [shape=record] subgraph pointer{ a [label = "{<in>NULL |<out>}"] b [label = "{<in>a |<out>}"] c [label = "{<in>b |<out>}"] d [label = "{<in>c |<out>}"] } subgraph nodes{ label="pointer" prev [label = "prev", shape=circle] head [label = "head", shape=circle] next [label = "next", shape=circle] } prev->a b->a head->b next->c c->d } ``` 接著把 prev 指標改指到 head 所指節點 a,把 head 改指到 next 所指節點 b,這時 prev 指向已經反轉的串列開頭,head 指向將要反轉的串列,因此可繼續進行下一個迴圈,直到最後一個迴圈結束,head 會指向 NULL,而 prev 指向已經反轉完成的串列開頭 ```graphviz digraph { rankdir=LR; size=4; node [shape=record] subgraph pointer{ a [label = "{<in>NULL |<out>}"] b [label = "{<in>a |<out>}"] c [label = "{<in>b |<out>}"] d [label = "{<in>c |<out>}"] } subgraph nodes{ label="pointer" prev [label = "prev", shape=circle] head [label = "head", shape=circle] } prev->b b->a head->c c->d } ``` ```c= void q_reverse(queue_t *q) { if (!q || !q->head) { return; } list_ele_t *prev = NULL, *next; q->tail = q->head; while (q->head) { next = q->head->next; q->head->next = prev; prev = q->head; q->head = next; } q->head = prev; } ``` ### q_sort [Reference](https://www.techiedelight.com/merge-sort-singly-linked-list/) 使用 Merge Sort,主程式說明如下: 使用完 mergeSort 之後,需要額外從頭到尾掃描一遍,來找出並維護 tail 指標 ```c= void q_sort(queue_t *q) { if (!q || !q->head) return; mergeSort(&(q->head)); list_ele_t *cur = q->head; while (cur->next) cur = cur->next; q->tail = cur; } ``` 跟一般的mergeSort想法一樣,我們使用 frontBackSplit() 來找出鍊節串列的中點, 對左右各半的鍊節串列分別再做mergeSort,最後使用sortedMerge()合併兩個已排序好的練節串列 ```c= void mergeSort(list_ele_t **head) { if (!(*head) || !(*head)->next) return; list_ele_t *front, *back; frontBackSplit(*head, &front, &back); mergeSort(&front); mergeSort(&back); (*head) = sortedMerge(front, back); } ``` frontBackSplit說明如下: slow指標從頭開始一次走一步,fast指標則一次走兩步,當fast指標走到串列最尾端時,slow指標剛好走了串列一半的節點,slow->next也就是後半串列的開頭 ```c void frontBackSplit(list_ele_t *head, list_ele_t **front, list_ele_t **back) { list_ele_t *slow = head; list_ele_t *fast = head->next; while (fast) { fast = fast->next; if (fast) { fast = fast->next; slow = slow->next; } } *front = head; *back = slow->next; slow->next = NULL; } ``` sortedMerge合併兩個排序好的練結串列,這個問題剛好在 [Leetcode](https://leetcode.com/problems/merge-two-sorted-lists/) 上有出現過,我採用的是iterative 方法而不是 Reference 使用的遞迴方法。 比較特別的技巧是我們在串列前頭創建一個dummyHead,這樣就不用特殊處理初始串列為空的情況,當我們比較完任意一個串列時,只要再接上另外一個串列的開頭即可,相比於陣列則需要讀取完所有元素。 ```c= list_ele_t *sortedMerge(list_ele_t *a, list_ele_t *b) { list_ele_t dummyHead; list_ele_t *cur = &dummyHead; while (a && b) { if (strcmp(a->value, b->value) <= 0) { cur->next = a; a = a->next; } else { cur->next = b; b = b->next; } cur = cur->next; } cur->next = a? a: b; return dummyHead.next; } ``` ## Address Sanitizer 在使用 Sanitizer 之後,執行 qtest 並輸入 help,會出現以下錯誤訊息: ```bash= ==6032==ERROR: AddressSanitizer: global-buffer-overflow on address 0x562c450ca400 at pc 0x562c450b390f bp 0x7ffccffa3db0 sp 0x7ffccffa3da0 READ of size 4 at 0x562c450ca400 thread T0 #0 0x562c450b390e in do_help_cmd /home/ming/linux2021/lab0-c/console.c:307 #1 0x562c450b3a22 in interpret_cmda /home/ming/linux2021/lab0-c/console.c:221 #2 0x562c450b4207 in interpret_cmd /home/ming/linux2021/lab0-c/console.c:244 #3 0x562c450b594a in run_console /home/ming/linux2021/lab0-c/console.c:660 #4 0x562c450b2531 in main /home/ming/linux2021/lab0-c/qtest.c:788 #5 0x7f47214880b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2) #6 0x562c450afb8d in _start (/home/ming/linux2021/lab0-c/qtest+0x8b8d) 0x562c450ca401 is located 0 bytes to the right of global variable 'echo' defined in 'console.c:59:13' (0x562c450ca400) of size 1 SUMMARY: AddressSanitizer: global-buffer-overflow /home/ming/linux2021/lab0-c/console.c:307 in do_help_cmd ``` 但目前還不知道如何判讀錯誤訊息並排解錯誤