# 2018q3 Homework2 ([lab0](https://github.com/ofAlpaca/lab-list)) contributed by < `ofAlpaca` > ###### tags: `CSIE5006` `HW` ## 實驗環境 ``` NAME="Ubuntu" VERSION="18.04.1 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.1 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic ``` --- ## Makefile 可以透過了解 `Makefile` 來得知此程式的流程,以本次作業的 `Makefile` 為例 : * 此處是變數宣告, `CC` 代表的是使用 GNU compiler , `CFLAGS` 則是相關參數。其中的 `-O0` 是[最佳化參數](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html), `-Werror` 則是把所有警告都轉為錯誤 ([source](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html))。 ```clike CC = gcc CFLAGS = -O0 -g -Wall -Werror ``` * 在 `all` target 裡的相依性項目有 `GIT_HOOKS` 與 `qtest` ,前者會透過 script 去檢查是否有安裝 git hook 需要的套件並安裝上 git hook ,後者 `qtest` target 則會繼續照法則往下去找相依性項目。 ```clike GIT_HOOKS := .git/hooks/applied all: $(GIT_HOOKS) qtest $(GIT_HOOKS): @scripts/install-git-hooks @echo ``` * `queueu.o` target 下的相依性項目有 `queue.[ch]` 與 `harness.h` ,前者是本次作業的核心檔案,後者則是作業提供的 `malloc()` 與 `free()` ;經過編譯後產生 queue 的 object 檔,同時也是 `qtest` 的相依性項目。在 `qtest` target 執行編譯命令後會產生 `qtest` ,一個可執行檔。 ```clike queue.o: queue.c queue.h harness.h $(CC) $(CFLAGS) -c queue.c qtest: qtest.c report.c console.c harness.c queue.o $(CC) $(CFLAGS) -o qtest qtest.c report.c console.c harness.c queue.o ``` * 可以使用 `make test` 或 `make clean` 來執行 Makefile 下相對應的命令,`make test` 就會去執行 `driver.py` 來跑測試並計算分數, `make clean` 則會清除過程中產生的多餘檔案。 ```clike test: qtest scripts/driver.py scripts/driver.py clean: rm -f *.o *~ qtest rm -rf *.dSYM (cd traces; rm -f *~) ``` --- ## Queue Structure * 根據作業提供的文件有提到目前的 `queue_t` 結構不完整,需要再增加其他欄位。為了能以 $O(1)$ 的時間複雜度從尾端插入節點與計算 queue 的大小,增加了 `list_ele_t *tail` 與 `size_t size` 。 > The top-level representation of a queue is a structure of type queue_t. In the starter code, this structure contains only a single field head, but you will want to add other fields. ```clike= typedef struct { list_ele_t *head; /* Linked list of elements */ /* You will need to add more fields to this structure to efficiently implement q_size and q_insert_tail */ list_ele_t *tail; // ptr to tail of the queue size_t size; // size of the queue } queue_t; ``` --- ## Operations of Queue ### `q_new()` 新增 if statement 來判斷佇列是否為 `NULL` ,並對 `tail` 與 `size` 初始化。 ```clike= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); /* What if malloc returned NULL? */ if (q == NULL) return NULL; q->head = NULL; q->tail = NULL; q->size = 0; // initialization return q; } ``` ### `q_free()` 新增 if statement 來判斷佇列是否為空,如果是,則 do nothing ,否則宣告 nptr pointer 去尋訪佇列中的每一個節點並釋放它,最後再釋放佇列。 ```clike= void q_free(queue_t *q) { /* How about freeing the list elements and the strings? */ /* Free queue structure */ if (q != NULL) { list_ele_t *nptr = q->head; list_ele_t *rm; while (nptr != NULL) { rm = nptr; nptr = nptr->next; // free(rm->value); free(rm); } free(q); } } ``` ### `q_insert_head()` 新增 if statement 來判斷佇列是否為 `NULL` ,是則回傳 false ,否則配置記憶體給 `newh` ,使用 `strdup()` 配置 string 的副本給 `newh->value` 接著把 `newh->next` 指向 `head `。如果 `head` 與 `tail` 皆為 `NULL` ,則表示目前新增的節點為佇列的第一個,故 `tail` 也需要指向此節點,最後再 `size++`。 ```clike= bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; /* What should you do if the q is NULL? */ if (q != NULL) { /* Don't forget to allocate space for the string and copy it */ /* What if either call to malloc returns NULL? */ newh = malloc(sizeof(list_ele_t)); if (newh != NULL) { newh->value = strdup(s); newh->next = q->head; /* case : the first node */ if (q->head == NULL && q->tail == NULL) q->tail = newh; q->head = newh; q->size++; return true; } } return false; } ``` ### `q_insert_tail()` 大致步驟和 `q_insert_head()` 相同,只差在換成從尾部接上。為了達到時間複雜度 $O(1)$ 的要求,所以使用 `tail` 而非從頭開始。 ```clike= bool q_insert_tail(queue_t *q, char *s) { /* You need to write the complete code for this function */ /* Remember: It should operate in O(1) time */ list_ele_t *newh; if (q != NULL) { newh = malloc(sizeof(list_ele_t)); if (newh != NULL) { newh->value = strdup(s); newh->next = NULL; /* case : the first node */ if (q->head == NULL && q->tail == NULL) q->head = newh; else q->tail->next = newh; q->tail = newh; q->size++; return true; } } return false; } ``` ### `q_remove_head()` 除了判斷佇列與 `queue->head` 是否為 `NULL`外,還須判斷 `sp` 是否為 `NULL` ,如果不是,則需要將最長為 `bufsize` - 1 的字串複製至 `sp` 並在其尾部加上 terminator ,最後再更新 `head` 。 ```clike= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { /* You need to fix up this code. */ if (q != NULL) { list_ele_t *rm_t = q->head; if (rm_t != NULL) { // if the removed node is not NULL if (sp != NULL) { memcpy(sp, rm_t->value, bufsize); // copy the removed string to sp sp[bufsize - 1] = '\0'; // add terminator at the end } q->head = q->head->next; // move head ptr to next node q->size--; // free(rm_t->value); free(rm_t); // free the removed node rm_t = NULL; // tmp ptr to NULL return true; } } return false; } ``` ### `q_size()` 因為先前有新增刪除節點時都有維護 `size` ,所以此處只要判斷佇列是否為 `NULL` 並回傳就好。 ```clike= int q_size(queue_t *q) { /* You need to write the code for this function */ /* Remember: It should operate in O(1) time */ return q != NULL ? q->size : 0; } ``` ### `q_reverse()` 這裡使用的 reverse 算是最直觀的,先準備三個指標,分別指向現在節點、過去節點、下個節點。迴圈內的執行順序為: 1. 先將現在節點的 `next` 指向過去節點( #11 ) 2. 過去節點指向現在節點( #12 ) 3. 現在節點指向下個節點( #13 ) 4. 下個節點指向現在節點的 `next` ( #14 ) 5. 檢查下個節點是否為 `NULL` ,是則停止迴圈 6. 將最後一個節點的 `next` 指向過去節點 7. 最後再更新 `head` 與 `tail` 指標 ```clike= void q_reverse(queue_t *q) { /* You need to write the code for this function */ if (q != NULL && q->size != 0) { list_ele_t *pre = NULL; // previous list_ele_t *cur = q->head; // current list_ele_t *nxt = cur->next; // next q->tail = q->head; while (nxt != NULL) { cur->next = pre; pre = cur; cur = nxt; nxt = cur->next; } cur->next = pre; q->head = cur; } } ``` --- ## 測試結果 ==TOTAL 100/100== ``` $ make test scripts/driver.py --- Trace Points +++ TESTING trace trace-01-ops: # Test of insert_head and remove_head --- trace-01-ops 6/6 ........ --- TOTAL 100/100 ``` --- ## 自動評分系統 這裡主要是說明自動評分系統 `driver.py` 的運作流程。 :::success 前言 * 根據[**說明文件**](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf)提到,可以透過下 `./driver.py` 或 `make test` 來執行自動評分。 * 再看看 Makefile 的 `test` tag 下執行的命令也是 `script/driver.py` 。 * 由此可知,自動評分的主要程式就是 `driver.py` 。 ::: ```flow st=>start: run() e=>end: print total score op=>operation: Tracer.run() op2=>operation: Tracer.runTrace(trace_id) op3=>operation: subprocess.call(qtest, cmd) cond=>condition: Run all tests ? st->op->op2->op3->cond cond(yes)->e cond(no)->op2 ``` ### `driver.py` 的運作流程 1. 當執行 `driver.py` 後,第一個被呼叫的函式為 `run()` 。 2. `run()` 主要是設定參數,例如要測試哪個程式,要測試什麼命令之類的。 * `-h` : 說明參數用途 * `-p` : 所要測試的程式, default "./qtest" * `-v` : 測試訊息的 level (0-3) 設定, default 0 * `-t` : 所要測試的命令的編號, default 0 * `-A` : 自動產生 json 格式的成績單 * 下 `-A` 參數 : ```shell $ ./scripts/driver.py -A --- Trace Points --- trace-01-ops 6/6 --- trace-02-ops 6/6 --- trace-03-ops 6/6 --- trace-04-ops 6/6 --- trace-05-ops 6/6 --- trace-06-string 7/7 --- trace-07-robust 7/7 --- trace-08-robust 7/7 --- trace-09-robust 7/7 --- trace-10-malloc 7/7 --- trace-11-malloc 7/7 --- trace-12-malloc 7/7 --- trace-13-perf 7/7 --- trace-14-perf 7/7 --- trace-15-perf 7/7 --- TOTAL 100/100 {"scores": {"Trace-01" : 6, "Trace-02" : 6, "Trace-03" : 6, "Trace-04" : 6, "Trace-05" : 6, "Trace-06" : 7, "Trace-07" : 7, "Trace-08" : 7, "Trace-09" : 7, "Trace-10" : 7, "Trace-11" : 7, "Trace-12" : 7, "Trace-13" : 7, "Trace-14" : 7, "Trace-15" : 7}} ``` > 預設參數 t = 0 會把所有的測試都跑過。 > [name=ofAlpaca][color=#bbff00] 3. 之後會產生 Tracer 的 instance ,並執行 method `Tracer.run()` ,此 method 會去把所有的測試透過 method `Tracer.runTrace()` 來執行一次,並根據回傳值決定此測試是否 "通過"。 > 所有的測試都放在 ./traces 這個資料夾裡。 > 這裡會用 "通過" 是因為程式碼裡面只有滿分跟 0 分兩種而已。 > [name=ofAlpaca][color=#bbff00] 4. `Tracer.runTrace()` 會呼叫 python 函式庫的 subprocess 來執行 `./qtest` 並傳入相關參數給 `qtest` ,通過就回傳 true 。 > subprocess 其用法為 `subprocess.call([./qtest -v 0 -f "trace-01-ops.cmd"])`[name=ofAlpaca][color=#bbff00] 6. 當所有測試都執行完後,便會印出總成績。 --- ## qtest ### qtest 行為 1. 其本身是個 command line interpreter 2. 初始化各項參數,例如 : `-f FILE` 。 3. 命令是否合理,例如在 `q == NULL` 的情況下跑 `reverse` 指令則會讓 `do_reverse()` 產生 "Warning: Calling reverse on null queue" 的警告。 4. 檢查 `queue.[ch]`裡的佇列實作是否有誤,例如 `do_reverse()` 就是去檢查 `q_reverse()` 是否有 Timeout 或 Segmentation fault 的問題。 ### Command information as linked list `qtest` 其中有個吸引我注意的技巧,那就是所有的命令都是透過 linked list 結構來儲存的。在 `console.h` 可以看到其節點結構為下 : ```clike typedef struct CELE cmd_ele, *cmd_ptr; struct CELE { char *name; // 命令名稱 cmd_function operation; // 命令的 function pointer char *documentation; // 命令的說明 cmd_ptr next; }; ``` 過去在寫需要 command line 的程式時,通常都是用 `if-else` 或 `switch` 這種 hard-coded 的方式決定命令的流程,如果使用 linked list 的話就可以省去許多冗長的程式碼,例如 : ```clike= switch(cmd) { case "add" : add(); break; case "remove" : remove(); break; case "reverse" : reverse(); break; } ``` 使用 linked list 可以改為 : ```clike= while( strcmp(cmd, cmd_list->name) != 0 ) cmd_list = cmd_list->next; cmd_list->operation(); ``` 如果命令很多,使用 `switch` 就會導致程式碼雜亂無章又不易維護,但使用 linked list 短短幾行就可以達到同樣的效果卻又有品味。另一點是,如果今天想要看到命令的說明或者其他操作,前者還要再跑一次 `switch` ,但後者卻只需要改為 `cmd_list->documentation` 就可以辦到。 ### [Signal handling](https://notes.shichao.io/apue/ch10/) 在本次作業中, `qtest` 是透過 `void (*signal(int sig, void (*func)(int)))(int)` 這個函式來檢查程式是否 Timeout 或 Segmentation fault 的,以下一一講解。 * signal 是個軟體中斷,像 Ctrl-C 就是個中斷,而 `signal()` 函式提供了一個可以處理非同步事件的方法。 * `sig` 為 signal number ,為中斷的編號,定義在 C 函式庫的 `signal.h` 裡,變數是 SIG 開頭,像是 `SIGINT` 、 `SIGSEGV` 。 * `func` 可以有三種值 : 1. `SIG_IGN` ,訊號忽略。 2. `SIG_DEF` ,訊號會照預設情況處理。 3. 當訊號發生時的 `func` 就會被呼叫,此稱為 signal handler 或 signal-catching function。 * `qtest.c` 的 `queue_init()` 就可以見到其用法 : ```clike= static void queue_init() { fail_count = 0; q = NULL; signal(SIGSEGV, sigsegvhandler); signal(SIGALRM, sigalrmhandler); } ``` * 第 4 行設置了 catch `SIGSEGV` 的訊號,當訊號發生時就呼叫 `sigsegvhandler` 。 * 第 5 行設置了 catch `SIGALRM` 的訊號,當訊號發生時就呼叫 `sigalrmhandler` 。 * 當發生了 `SIGSEGV` 、 `SIGALRM` 的中斷,便會呼叫其各自的 signal handler 。這也是為什麼每次 `queue.c` 一發生問題,就會產生這些錯誤訊息的原因。 ```clike= /* Signal handlers */ void sigsegvhandler(int sig) { trigger_exception( "Segmentation fault occurred. You dereferenced a NULL or invalid " "pointer"); } void sigalrmhandler(int sig) { trigger_exception( "Time limit exceeded. Either you are in an infinite loop, or your " "code is too inefficient"); } ```