# 2018q3 Homework2 (lab0) contributed by < [`jason53415`](https://github.com/jason53415/lab0-c) > >commit 應切分為更小的 commit,每個 commit 都應該專注在小部份的修改 >[name=課程助教][color=red] ## 測試環境 ``` $ uname -a Linux scream-47860 4.15.0-34-generic #37-Ubuntu SMP Mon Aug 27 15:21:48 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux ``` ## 實做內容 ### queue.h * 為了要達成可以 $O(1)$ 時間取得 queue 的大小以及從尾端插入元素,需要在 queue.h 中的 `queue_t` 增加 `list_ele_t *tail` 指向 queue 的末端,以及 `int size` 記錄 queue 的大小。 ```C= typedef struct { list_ele_t *head; list_ele_t *tail; int size; } queue_t; ``` ### queue.c * q_new * 使用 `malloc()` 配置記憶體,並且初始所有的變數。 * 需要注意在 `malloc()` 失敗時要回傳 `NULL` 。 * q_free * 因為每個 queue 的節點中都有一個指向字串的指標,所以在處理時必須先釋放掉字串的記憶體,再釋放節點的記憶體。 * 確認所有節點都被釋放掉後,最後則是將 queue 本身佔用的記憶體釋放掉。 * q_insert_head * 配置節點的記憶體,並且依照傳入字串的大小配置字串的記憶體,最後使用 `strcpy()` 將字串內容複製過去。 * 如果 queue 本來就還沒建立,或者配置記憶體沒有成功,則回傳 `false` 。 * 建立完節點之後,要讓這個節點的 `next` 先指向原本的 `head` ,再將 queue 的 `head` 指向這個節點。 * 加入新的節點後,將 queue 的 `size` 增加 1 ,並且回傳 `true` 。 * 需要特別注意在配置記憶體失敗時要回傳 `NULL` ,尤其是配置字串的記憶體失敗時,還要記得把先前配置給節點的記憶體也釋放掉。 * 時間複雜度: $O(1)$ * q_insert_tail * 因為我們有新增一個指標 `tail` 指向 queue 的末端,大致內容與 `q_insert_head()` 一樣,只是建立完節點之後要先讓原本的 `tail->next` 指向新增的節點,再讓 queue 的 `tail` 指向新節點。 * 時間複雜度: $O(1)$ * q_remove_head * 先確認 queue 是否有建立以及 queue 是否是空的,如果是空的直接回傳 `false` 。 * 先使用 `memset()` 將 `sp` 的內容全部初始為 `\0 ` 後,在將 `head` 所指到的節點的字串使用 `strncpy()` 複製到 `sp` ,以避免超過 `bufsize` 。 * 最後先用一個指標存下目前 `head` 的位址,然後將 `head` 指向下一個節點後,把指標所指的節點的字串以及本身的記憶體釋放掉。 * 刪除節點後,將 queue 的 `size` 減少 1 ,並且回傳 `true` 。 * 時間複雜度: $O(1)$ * q_size * 因為在 queue 中插入或刪除資料時都會更新 `size` 的大小,所以除非 queue 本身沒有建立時要直接回傳 0 之外,都可以直接回傳 queue 中記錄的 `size` 。 * 時間複雜度: $O(1)$ * q_reverse * 如果 queue 還沒建立,或者 queue 的 `size` 小於等於 1 時,就直接 return 。 * 為了要實現在不新增節點的情況下將 queue 顛倒過來,利用了三個指標 `last` 、 `curr` 、 `next` 指向三個連續的節點,來將 `curr` 中所指的下一個節點從 `next` 改為 `last` ,並且不斷將全部的指標移往下一個節點。 * 最後要交換 `head` 及 `tail` ,讓指標正確指向 queue 的頭尾端。 * 時間複雜度: $O(n)$ ## 評分結果 ```shell $ 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, and size --- trace-04-ops 6/6 +++ TESTING trace trace-05-ops: # Test of insert_head, insert_tail, remove_head reverse, and size --- trace-05-ops 6/6 +++ TESTING trace trace-06-string: # Test of truncated strings --- trace-06-string 7/7 +++ TESTING trace trace-07-robust: # Test operations on NULL queue --- trace-07-robust 7/7 +++ TESTING trace trace-08-robust: # Test operations on empty queue --- trace-08-robust 7/7 +++ TESTING trace trace-09-robust: # Test remove_head with NULL argument --- trace-09-robust 7/7 +++ TESTING trace trace-10-malloc: # Test of malloc failure on new --- trace-10-malloc 7/7 +++ TESTING trace trace-11-malloc: # Test of malloc failure on insert_head --- trace-11-malloc 7/7 +++ TESTING trace trace-12-malloc: # Test of malloc failure on insert_tail --- trace-12-malloc 7/7 +++ TESTING trace trace-13-perf: # Test performance of insert_tail --- trace-13-perf 7/7 +++ TESTING trace trace-14-perf: # Test performance of size --- trace-14-perf 7/7 +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, and reverse --- trace-15-perf 7/7 --- TOTAL 100/100 ``` ## 自動評分系統運作原理 從 Makefile 中可以得知,執行 `$ make test` 時實際上就是執行 scripts/driver.py 這個程式。 ```bash test: qtest scripts/driver.py scripts/driver.py ``` 透過執行 `$ scripts/driver.py -h` 可以看到有許多參數可以調整。 ```shell $ scripts/driver.py -h Usage: scripts/driver.py [-h] [-p PROG] [-t TID] [-v VLEVEL] -h Print this message -p PROG Program to test -t TID Trace ID to test -v VLEVEL Set verbosity level (0-3) ``` 其中 TID 表示要使用哪一筆測試資料,預設的 TID 為 0 ,用來表示要使用所有的測試資料; verbosity level 則表示執行時所印出的訊息的多寡程度,以下利用了第一筆測試資料來實驗不同 verbosity level 的差異: ```shell $ scripts/driver.py -t 1 -v 0 --- Trace Points --- trace-01-ops 6/6 --- TOTAL 6/6 ``` ```shell $ scripts/driver.py -t 1 -v 1 --- Trace Points +++ TESTING trace trace-01-ops: # Test of insert_head and remove_head --- trace-01-ops 6/6 --- TOTAL 6/6 ``` ```shell $ scripts/driver.py -t 1 -v 2 --- Trace Points +++ TESTING trace trace-01-ops: cmd># Test of insert_head and remove_head cmd>option fail 0 cmd>option malloc 0 cmd>new cmd>ih gerbil cmd>ih bear cmd>ih dolphin cmd>rh dolphin Removed dolphin from queue cmd>rh bear Removed bear from queue cmd>rh gerbil Removed gerbil from queue cmd> --- trace-01-ops 6/6 --- TOTAL 6/6 ``` ```shell $ scripts/driver.py -t 1 -v 3 --- Trace Points +++ TESTING trace trace-01-ops: cmd># Test of insert_head and remove_head cmd>option fail 0 cmd>option malloc 0 cmd>new q = [] cmd>ih gerbil q = [gerbil] cmd>ih bear q = [bear gerbil] cmd>ih dolphin q = [dolphin bear gerbil] cmd>rh dolphin Removed dolphin from queue q = [bear gerbil] cmd>rh bear Removed bear from queue q = [gerbil] cmd>rh gerbil Removed gerbil from queue q = [] cmd> Freeing queue --- trace-01-ops 6/6 --- TOTAL 6/6 ``` 可以看出當 verbosity level 為 0 時只會印出分數,為 1 的時候會印出測試的項目,為 2 會印出執行的每一個指令,為 3 時則會將執行完每一個指令後的 queue 的內容一起印出。 處理完輸入的參數後, scripts/driver.py 接下來就會依此去執行 qtest ,讓 qtest 從 trace 資料夾中讀取相對應的 .cmd 檔來測試。 ```python= def runTrace(self, tid): if not tid in self.traceDict: print "ERROR: No trace with id %d" % tid return False fname = "%s/%s.cmd" % (self.traceDirectory, self.traceDict[tid]) vname = "%d" % self.verbLevel clist = [self.qtest, "-v", vname, "-f", fname] try: retcode = subprocess.call(clist) except Exception as e: print "Call of '%s' failed: %s" % (" ".join(clist), e) return False return retcode == 0 ``` 第 9 行的 [`subprocess.call()`](https://docs.python.org/2/library/subprocess.html#using-the-subprocess-module) 是 python 中增加子行程的函式,可以直接執行外部程式並且加上執行時需要的命令列參數,直到外部程式結束後回傳 return code 。 觀察以上整段 `runTrace()` 後可以發現只有當 qtest 回傳的 `retcode` 等於 `0` 的時候,整個函式才有可能回傳 `True`,並且存在下面 `run()` 函式第 7 行的 `ok` 中。 ```python= def run(self, tid = 0): ... score = 0 ... for t in tidList: ... ok = self.runTrace(t) maxval = self.maxScores[t] tval = maxval if ok else 0 ... score += tval ... ``` 又因為只有當 `ok` 為 `True` 時才能得到整題的分數 `maxval` ,我們可以推論是否能得到分數完全取決於 qtest 最後的回傳值是否等於 `0` 。 ## qtest 的行為與技巧 在 harness.h 中有以下的程式碼,將原本 C 語言定義的函式 `malloc()` 與 `free()` 重新定義到 `test_malloc()` 與 `test_malloc()` 。 ```css /* Tested program use our versions of malloc and free */ #define malloc test_malloc #define free test_malloc ``` 其中 `test_malloc()` 會呼叫 `fail_allocation()` 這個函式,並依據當前的 malloc 失敗機率 `fail_probability` 決定這次配置記憶體是否成功。 ```C= /* Should this allocation fail? */ static bool fail_allocation() { double weight = (double) random() / RAND_MAX; return (weight < 0.01 * fail_probability); } ``` 在 harness.c 中可以發現,實際上所有透過 `test_malloc()` 配置的記憶體會使用以下的資料結構存成一個 doubly-linked list。 ```C= typedef struct BELE { struct BELE *next; struct BELE *prev; size_t payload_size; size_t magic_header; /* Marker to see if block seems legitimate */ unsigned char payload[0]; /* Also place magic number at tail of every block */ } block_ele_t; ``` 這裡的 `payload[0]` 是一個長度為零的陣列,放在 struct 的最後面,可以用來標示其後所接的記憶體的起始位置。 ```C= void *test_malloc(size_t size) { ... block_ele_t *new_block = malloc(size + sizeof(block_ele_t) + sizeof(size_t)); ... new_block->magic_header = MAGICHEADER; new_block->payload_size = size; *find_footer(new_block) = MAGICFOOTER; void *p = (void *) &new_block->payload; memset(p, FILLCHAR, size); ... return p; } ``` 上面第四行的程式碼顯示,我們實際上配置的記憶體大小是由 `block_ele_t` 加上我們原本要配置的記憶體大小 `size` ,以及一個 footer 所組成。 因此我們以為用 `malloc()` 配置到的一整塊記憶體實際上是跟在一個 `block_ele_t` 的 struct 後面,並且最後還跟著一個 footer,而且從第十行程式碼可以看到 `test_malloc()` 回傳的指標 `p` 就是指向 payload 所在的位址。 ```C= void test_free(void *p) { ... block_ele_t *b = find_header(p); ... /* Unlink from list */ block_ele_t *bn = b->next; block_ele_t *bp = b->prev; if (bp) bp->next = bn; else allocated = bn; if (bn) bn->prev = bp; free(b); allocated_count--; } ``` 與 `test_malloc()` 相對應的,使用 `test_free()` 時則會先呼叫 `find_header()` ,並且如以下的程式碼所示,減掉 `block_ele_t` 的大小來往前找到當初宣告記憶體的起始位址,之後會再對 doubly-linked list 的指標進行操作後,把整塊配置的記憶體釋放掉。 ```C= static block_ele_t *find_header(void *p) { ... block_ele_t *b = (block_ele_t *) ((size_t) p - sizeof(block_ele_t)); ... return b; } ```