# 2020q1 Homework1 (lab0) contributed by < `Gorilla0823` > ## 實驗環境 ```shell $ uname -a Linux 5.3.0-28-generic Linux ubuntu 4.15.0-51-generic #55-Ubuntu SMP Wed May 15 14:27:21 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux $ gcc --version gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0 Copyright (C) 2017 Free Software Foundation, Inc. ``` ## 作業要求 - 詳細閱讀 [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) ,依據指示著手修改 `queue.[ch]` 和連帶的檔案,測試後用 Git 管理各項修改,記得也該實作 `q_sort` 函式 - 修改排序所用的比較函式,變更為 `natural sort`,在 “simulation” 也該做對應的修改,得以反映出 `natural sort` 的使用。 - 運用 Valgrind 排除 qtest 實作的記憶體錯誤,並透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗 - 研讀論文 Dude, is my code constant time?,解釋本程式的 “simulation” 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 Student’s t-distribution 及程式實作的原理; - 解釋 select 系統呼叫在本程式的使用方式,並分析 console.c 的實作,說明其中運用 CS:APP RIO 套件 的原理和考量點。可對照參考 CS:APP 第 10 章重點提示 :arrow_forward: 為避免「舉燭」,請比照 qtest 實作,撰寫獨立的終端機命令解譯器 (可建立新的 GitHub repository) ## 開發過程 ### queue.h ```cpp typedef struct { list_ele_t *head; list_ele_t *tail; size_t size; } queue_t; ``` 根據作業要求,增加了 `size` 來記錄 queue 的大小以及 `tail` 指向 queue 中的最後一個元素。 :::warning 考量點為何?考慮到 side effect 嗎?你需要探討 :notes: jserv ::: > 考量點如下: 不加 `q_size` 的話就需要花費 $O(N)$,因為作業要求 $O(1)$。 [name=gorilla] ### queue.c #### q_new - 建立一個空的 queue - 當 `malloc` 失敗時需要回傳 `NULL` ```cpp queue_t q_new() { queue_t *q = malloc(sizeof(queue_t)); if (!q) { return NULL; } q->tail = NULL; q->head = NULL; q->size = 0; return q; } ``` 1. 將 `tail` 及 `head` 指向 `NULL` 2. 將 queue `size` 設為 `0` #### q_size - 回傳 queue 的元素數量 - queue 為 `NULL` 時 return `0` ```cpp int q_size(queue_t *q) { if (!q) { return 0; } return q->size; } ``` :::warning 考慮以下改寫: ```cpp int q_size(queue_t *q) { return q ? q->size : 0; } ``` :notes: jserv ::: > 已修改,謝謝老師。 [name=gorilla] #### q_free - 釋放 queue 所占用的記憶體 - 當 queue 為 `NULL` 時,直接回傳 ```cpp void q_free(queue_t *q) { if (!q) { return; } list_ele_t *tmp = NULL; while (q->head) { tmp = q->head; q->head = q->head->next; free(tmp->value); free(tmp); } q->tail = NULL; q->head = NULL; q->size = 0; free(q); } ``` 1. 設立 1 個指向 `NULL` 的 `list_ele_t` 結構,從 `head` 刪到 `tail` ,並且 `free` 掉每個 Node 及其包含的字串。 2. 將 `tail` 及 `head` 指向 `NULL` ,並且將 queue 的 `size` 歸 `0` 。 #### q_insert_head - 從 queue 的 `head` 插入一個元素 - 當 queue 為 NULL 或 `malloc` 失敗時 回傳 false - `*s` 指向要被插入的元素 ```cpp bool q_insert_head(queue_t *q, char *s) { if (!q) { return false; } list_ele_t *newh; newh = malloc(sizeof(list_ele_t)); size_t length = strlen(s); char *str = malloc(length + 1); if (newh && str) { strncpy(str, s, length); str[length] = '\0'; newh->value = str; newh->next = q->head; q->head = newh; if (!q->tail) { q->tail = newh; } q->size++; return true; } else { free(newh); free(str); return false; } } ``` 1. 這個部分修改了很多次,原本採用 `strcpy` 來複製欲新增的元素,因為 Git pre-commit hook 的檢查機制警告這種行為會造成 buffer overrun ,所以改為 `strncpy` 。 2. 剛開始以為 `malloc` 失敗只會回傳 `NULL` pointer, 查了資料後發現也會回傳 `non-null` pointer , 所以改用 `if-else`來回收記憶體。 經過查詢[Linux man page](https://linux.die.net/man/3/malloc)後得知其對於`malloc` 回傳值的敘述如下: > The malloc() function allocates size bytes and returns a pointer to the allocated memory. The memory is not initialized. If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free(). #### q_insert_tail - 從 queue 的 `tail` 新增元素 - 如果成功新增則回傳 `true` - 若 queue 為 `NULL` 或配置記憶體失敗時則回傳 `false` - `*s` 指向要被插入的元素 ```cpp bool q_insert_tail(queue_t *q, char *s) { if (!q || !q->head) { return false; } list_ele_t *tmp = q->head; if (sp) { memset(sp, 0, bufsize); strncpy(sp, tmp->value, bufsize - 1); } q->head = q->head->next; free(tmp->value); free(tmp); q->size--; return true; } ``` 此 function 與 `insert_head` 類似。 #### q_remove_head - 將 queue 從 `head` 刪除 - 如果成功刪除則回傳 `true` - 當 queue 為空或為 `NULL` 時回傳 `false` - 若 `sp` 是 `non-NULL` 且一個元素被刪除時,複製被刪除的元素到 `*sp` 中。( 上限為 `bufsize-1`並且 `*sp` 的最後一位須為 null terminator。 ) - 被 list 元素使用的記憶體及其字串需要被 `free`。 ```cpp bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || !q->head) { return false; } list_ele_t *tmp = q->head; if (sp) { memset(sp, 0, bufsize); strncpy(sp, tmp->value, bufsize - 1); } q->head = q->head->next; free(tmp->value); free(tmp); q->size--; return true; } ``` #### q_reverse - 將 queue 的元素反轉 - queue 為 `NULL` 或 `empty` 時直接回傳 - 不可配置新的記憶體位址和 `free` 原有的記憶體位址 ``` cpp void q_reverse(queue_t *q) { if (!q || !q->head) { return; } list_ele_t *curr = q->head, *prev = NULL, *next = NULL; q->tail = q->head; while (curr) { next = curr->next; curr->next = prev; prev = curr; curr = next; } q->head = prev; } ``` 1. 設定 `1` 個分別指向 `head` 的 `*curr` 及 `2` 個指向 `NULL` 的 `prev` 和 `next`,並將 `head` 指向 `tail` 。 2. 接著從 `head` 走到 `NULL` ,過程中運用 pointer 將 queue 轉向。 3. 最後將 `head` 指向 `*prev` 。 #### q_sort - 將 queue 中元素小到大做排序 - 如果 queue 是 `NULL` 或 `empty`,直接回傳。 - 如果 queue 裡面只有 `1` 個元素,不做任何動作。 - 使用 natural sort ( 額外新增 ) ``` cpp void q_sort(queue_t *q) { if (!q || q->size <= 1) { return; } q->head = mergeSortList(q->head); list_ele_t *tmp = q->head; while (tmp->next) { tmp = tmp->next; } q->tail = tmp; } ``` 1. 將 `mergeSortList` function sort 的結果傳給 `head`。 2. 移動至 `tail` 並設立 `tail` 。 #### merge 和 mergeSortList ``` cpp #include "natsort/strnatcmp.c" list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { if (!l2) return l1; if (!l1) return l2; if (strnatcmp(l1->value, l2->value) < 0) { l1->next = merge(l1->next, l2); return l1; } else { l2->next = merge(l1, l2->next); return l2; } } list_ele_t *mergeSortList(list_ele_t *head) { if (!head || !head->next) { return head; } list_ele_t *fast = head->next; list_ele_t *slow = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; list_ele_t *l1 = mergeSortList(head); list_ele_t *l2 = mergeSortList(fast); return merge(l1, l2); } ``` :::warning `merge` 和 `mergeSortList` 這兩個函式都僅限於同一個 C 程式碼中使用,應該在宣告加註 `static`,使其 visibility 不會變為公開可見 (exposed),也有利編譯器施加更多最佳化。 :notes: jserv ::: >已修改,謝謝老師。[name=gorilla] 採用 merge sort 進行排序,實作的部份參考 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/);比較字串部分採用 [natsort](https://github.com/sourcefrog/natsort)。 目前得分如下: ``` scripts/driver.py --- Trace Points +++ TESTING trace trace-01-ops: # Test of insert_head and remove_head --- trace-01-ops 6/6 +++ TESTING trace trace-02-ops: # Test of insert_head, insert_tail, and remove_head --- trace-02-ops 6/6 +++ TESTING trace trace-03-ops: # Test of insert_head, insert_tail, reverse, and remove_head --- trace-03-ops 6/6 +++ TESTING trace trace-04-ops: # Test of insert_head, insert_tail, size, and sort --- trace-04-ops 6/6 +++ TESTING trace trace-05-ops: # Test of insert_head, insert_tail, remove_head, reverse, size, and sort --- trace-05-ops 5/5 +++ TESTING trace trace-06-string: # Test of truncated strings --- trace-06-string 6/6 +++ TESTING trace trace-07-robust: # Test operations on NULL queue --- trace-07-robust 6/6 +++ TESTING trace trace-08-robust: # Test operations on empty queue --- trace-08-robust 6/6 +++ TESTING trace trace-09-robust: # Test remove_head with NULL argument --- trace-09-robust 6/6 +++ TESTING trace trace-10-malloc: # Test of malloc failure on new --- trace-10-malloc 6/6 +++ TESTING trace trace-11-malloc: # Test of malloc failure on insert_head --- trace-11-malloc 6/6 +++ TESTING trace trace-12-malloc: # Test of malloc failure on insert_tail --- trace-12-malloc 6/6 +++ TESTING trace trace-13-perf: # Test performance of insert_tail --- trace-13-perf 6/6 +++ TESTING trace trace-14-perf: # Test performance of size --- trace-14-perf 6/6 +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort --- trace-15-perf 0/6 +++ TESTING trace trace-16-perf: # Test performance of sort with random and descending orders # 10000: all correct sorting algorithms are expected pass # 50000: sorting algorithms with O(n^2) time complexity are expected failed # 100000: sorting algorithms with O(nlogn) time complexity are expected pass --- trace-16-perf 6/6 +++ TESTING trace trace-17-complexity: # Test if q_insert_tail and q_size is constant time complexity Probably constant time Probably constant time --- trace-17-complexity 5/5 --- TOTAL 94/100 ``` #### Revised q_sort #### mergeSortList ```cpp= static list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { list_ele_t *temp = NULL; list_ele_t **p = &temp; while (l1 && l2) { if (strnatcmp(l1->value, l2->value) < 0) { *p = l1; l1 = l1->next; } else { *p = l2; l2 = l2->next; } p = &(*p)->next; } if (l1) *p = l1; if (l2) *p = l2; return temp; } ``` 1. 發現在 ` traces/trace-015-ops.cmd` 內 insert 大量的字串後做 sort ,所以更改了判斷方式。 #### mergeSortList ```cpp= static list_ele_t *mergeSortList(list_ele_t *head) { if (!head || !head->next) { return head; } list_ele_t *fast = head->next; list_ele_t *slow = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; list_ele_t *l1 = mergeSortList(head); list_ele_t *l2 = mergeSortList(fast); return merge(l1, l2); } ``` ``` gorilla@ubuntu:~/Desktop/linux2020/lab0-c$ make test scripts/driver.py --- Trace Points +++ TESTING trace trace-01-ops: # Test of insert_head and remove_head --- trace-01-ops 6/6 +++ TESTING trace trace-02-ops: # Test of insert_head, insert_tail, and remove_head --- trace-02-ops 6/6 +++ TESTING trace trace-03-ops: # Test of insert_head, insert_tail, reverse, and remove_head --- trace-03-ops 6/6 +++ TESTING trace trace-04-ops: # Test of insert_head, insert_tail, size, and sort --- trace-04-ops 6/6 +++ TESTING trace trace-05-ops: # Test of insert_head, insert_tail, remove_head, reverse, size, and sort --- trace-05-ops 5/5 +++ TESTING trace trace-06-string: # Test of truncated strings --- trace-06-string 6/6 +++ TESTING trace trace-07-robust: # Test operations on NULL queue --- trace-07-robust 6/6 +++ TESTING trace trace-08-robust: # Test operations on empty queue --- trace-08-robust 6/6 +++ TESTING trace trace-09-robust: # Test remove_head with NULL argument --- trace-09-robust 6/6 +++ TESTING trace trace-10-malloc: # Test of malloc failure on new --- trace-10-malloc 6/6 +++ TESTING trace trace-11-malloc: # Test of malloc failure on insert_head --- trace-11-malloc 6/6 +++ TESTING trace trace-12-malloc: # Test of malloc failure on insert_tail --- trace-12-malloc 6/6 +++ TESTING trace trace-13-perf: # Test performance of insert_tail --- trace-13-perf 6/6 +++ TESTING trace trace-14-perf: # Test performance of size --- trace-14-perf 6/6 +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort --- trace-15-perf 6/6 +++ TESTING trace trace-16-perf: # Test performance of sort with random and descending orders # 10000: all correct sorting algorithms are expected pass # 50000: sorting algorithms with O(n^2) time complexity are expected failed # 100000: sorting algorithms with O(nlogn) time complexity are expected pass --- trace-16-perf 6/6 +++ TESTING trace trace-17-complexity: # Test if q_insert_tail and q_size is constant time complexity Probably constant time Probably constant time --- trace-17-complexity 5/5 --- TOTAL 100/100 ``` 雖然說可以拿到100分了,但是有時候 `trace-15-perf` 會 `TLE` 變成 94 分,方法還有待 改進。 ## 遇到的問題 ### 1. Permission denied 不知道為什麼每次都要 `sudo make install`,改了幾次資料夾權限( `sudo chmod -R 777 lab0-c` )放久了又會出現如下的敘述: ``` gorilla@ubuntu:~/Desktop/linux2020/lab0-c$ make test CC queue.o queue.c:288:1: fatal error: opening dependency file .queue.o.d: Permission denied } ``` :::warning 因為你之前用了 superuser 的權限來編譯或測試本程式,先試著移除編譯過程中所產生的 `*.o.d` 檔案 (順便思考其作用),然後回到一般使用者的權限來開發。養成好習慣,從正確地使用權限開始。 :notes: jserv ::: > 謝謝老師的建議。 > ~~根據 [stack overflow](https://stackoverflow.com/questions/6642137/why-when-do-i-need-a-clean-target) 上的這篇文章以及 [GNU Make](http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/) 的內容提到避免在未將編譯過程所產生的`*.o .d` 檔案清除前執行 `make` ,會造成某些功能與舊版本不相容。~~ [name=gorilla] :::danger 請仔細看 stackoverflow 上的討論,絕對不是「某些功能與舊版本不相容」,依據原本討論脈絡,是指 symbol / exposed function (不可翻譯為「功能」)。 :notes: jserv ::: > 已修改,謝謝老師。 > > 根據 [stack overflow](https://stackoverflow.com/questions/6642137/why-when-do-i-need-a-clean-target)上的這篇文章, `make clean` 會刪除所有在此期間建立的 **object files** , 如果絕對的安全,必須在 `make` 前執行 `make clean` 。 > > 如果在從未執行 `make clean` 下 ( 保留舊有的 object file ) 可能會造成一些問題,假設已經存在 1 個 object file 且他與某些先前版本的 library 做鏈結 ( linked againast previous version of some library ) 。 > >而當你更新這些 library 到較新的版本後, 但是其中有一些 function 與舊版本的不兼容 ( incompatible ) ,此時你的 object file 期望使用舊的版本,導致鏈結過程( linking process ) 失敗。 >[name=gorilla] ### 2. 無法執行 `make valgrind` ``` gorilla@ubuntu:~/Desktop/linux2020/lab0-c$ make valgrind # Explicitly disable sanitizer(s) make clean SANITIZER=0 qtest make[1]: Entering directory '/home/gorilla/Desktop/linux2020/lab0-c' rm -f qtest.o report.o console.o harness.o queue.o random.o dudect/constant.o dudect/fixture.o dudect/ttest.o .qtest.o.d .report.o.d .console.o.d .harness.o.d .queue.o.d .random.o.d .dudect/constant.o.d .dudect/fixture.o.d .dudect/ttest.o.d *~ qtest /tmp/qtest.* rm -rf .dudect rm -rf *.dSYM (cd traces; rm -f *~) CC qtest.o CC report.o CC console.o CC harness.o CC queue.o CC random.o CC dudect/constant.o CC dudect/fixture.o CC dudect/ttest.o LD qtest make[1]: Leaving directory '/home/gorilla/Desktop/linux2020/lab0-c' cp qtest /tmp/qtest.xP2SMu chmod u+x /tmp/qtest.xP2SMu sed -i "s/alarm/isnan/g" /tmp/qtest.xP2SMu scripts/driver.py -p /tmp/qtest.xP2SMu --valgrind --- Trace Points +++ TESTING trace trace-01-ops: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-01-ops.cmd' failed: [Errno 2] No such file or directory --- trace-01-ops 0/6 +++ TESTING trace trace-02-ops: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-02-ops.cmd' failed: [Errno 2] No such file or directory --- trace-02-ops 0/6 +++ TESTING trace trace-03-ops: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-03-ops.cmd' failed: [Errno 2] No such file or directory --- trace-03-ops 0/6 +++ TESTING trace trace-04-ops: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-04-ops.cmd' failed: [Errno 2] No such file or directory --- trace-04-ops 0/6 +++ TESTING trace trace-05-ops: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-05-ops.cmd' failed: [Errno 2] No such file or directory --- trace-05-ops 0/5 +++ TESTING trace trace-06-string: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-06-string.cmd' failed: [Errno 2] No such file or directory --- trace-06-string 0/6 +++ TESTING trace trace-07-robust: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-07-robust.cmd' failed: [Errno 2] No such file or directory --- trace-07-robust 0/6 +++ TESTING trace trace-08-robust: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-08-robust.cmd' failed: [Errno 2] No such file or directory --- trace-08-robust 0/6 +++ TESTING trace trace-09-robust: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-09-robust.cmd' failed: [Errno 2] No such file or directory --- trace-09-robust 0/6 +++ TESTING trace trace-10-malloc: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-10-malloc.cmd' failed: [Errno 2] No such file or directory --- trace-10-malloc 0/6 +++ TESTING trace trace-11-malloc: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-11-malloc.cmd' failed: [Errno 2] No such file or directory --- trace-11-malloc 0/6 +++ TESTING trace trace-12-malloc: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-12-malloc.cmd' failed: [Errno 2] No such file or directory --- trace-12-malloc 0/6 +++ TESTING trace trace-13-perf: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-13-perf.cmd' failed: [Errno 2] No such file or directory --- trace-13-perf 0/6 +++ TESTING trace trace-14-perf: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-14-perf.cmd' failed: [Errno 2] No such file or directory --- trace-14-perf 0/6 +++ TESTING trace trace-15-perf: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-15-perf.cmd' failed: [Errno 2] No such file or directory --- trace-15-perf 0/6 +++ TESTING trace trace-16-perf: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-16-perf.cmd' failed: [Errno 2] No such file or directory --- trace-16-perf 0/6 +++ TESTING trace trace-17-complexity: Call of 'valgrind /tmp/qtest.xP2SMu -v 1 -f ./traces/trace-17-complexity.cmd' failed: [Errno 2] No such file or directory --- trace-17-complexity 0/5 --- TOTAL 0/100 Test with specific case by running command: scripts/driver.py -p /tmp/qtest.xP2SMu --valgrind -t <tid> ``` 已確認過 valgrind 可正常執行( 測試方式:[抓漏 - 使用valgrind檢查C語言memory Leak](http://wen00072.github.io/blog/2014/11/29/catching-leakage-use-valgrind-checking-c-memory-leak/) ) 。 ## 參考資料 * [2020 春季 lab0c 作業說明](https://hackmd.io/@sysprog/linux2020-lab0) * [C Programming Lab: Assessing Your C Programming Skills](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) * [Why/when do I need a 'clean' target?](https://stackoverflow.com/questions/6642137/why-when-do-i-need-a-clean-target) * [ Auto-Dependency Generation ](http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/) * [抓漏 - 使用valgrind檢查C語言memory Leak](http://wen00072.github.io/blog/2014/11/29/catching-leakage-use-valgrind-checking-c-memory-leak/)