# 2019q1 Homework1 (lab0) contributed by <``jeffcarl67``> ## 要求 * 實做 queue * first in, first out * first in, last out * 完成以下函式 * q new: Create a new, empty queue * q free: Free all storage used by a queue * q insert head: Attempt to insert a new element at the head of the queue * q insert tail: Attempt to insert a new element at the tail of the queue * q remove head: Attempt to remove the element at the head of the queue * q size: Compute the number of elements in the queue * q reverse: Reorder the list so that the queue elements are reversed in order * 解釋自動評分系統運作的原理 * 提及 qtest 的行為和裡頭的技巧 ## 環境 ```shell= Linux 4.15.0-45-generic #48~16.04.1-Ubuntu SMP ``` ## 實做 ### q_new 分配一個 queue,要注意 ``malloc`` 失敗後的錯誤處理以及 ``queue_t`` 結構的初始化 ```c= /* Create empty queue. Return NULL if could not allocate space. */ queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); /* What if malloc returned NULL? */ if (!q) return NULL; q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` ### q_free 釋放 queue 使用的所有記憶體,此處的作法是每次都刪除第一個元素,直到 linked list 中沒有元素為止 ```c= /* Free all storage used by queue */ void q_free(queue_t *q) { /* How about freeing the list elements and the strings? */ /* Free queue structure */ list_ele_t *cur = NULL; if (!q) return; while (q->head) { cur = q->head; q->head = q->head->next; if (cur->value) free(cur->value); free(cur); } free(q); } ``` ### q_insert_head 在 queue 的開頭插入一個元素,要比較注意的地方是在用於儲存字串的指針 ``newh->value`` 取得記憶體失敗後,在錯誤處理時除了 ``return false`` 外還要將先前取得的記憶體一併釋放,才不會有記憶體洩漏的問題 ```c= /* 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. */ bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh = NULL; /* What should you do if the q is NULL? */ if (!q) return false; newh = malloc(sizeof(list_ele_t)); if (!newh) return false; newh->value = (char *) malloc(strlen(s) + 1); if (!newh->value) { free(newh); return false; } newh->next = NULL; memcpy(newh->value, s, strlen(s) + 1); q->size++; /* Don't forget to allocate space for the string and copy it */ /* What if either call to malloc returns NULL? */ newh->next = q->head; q->head = newh; if (!q->tail) q->tail = newh; return true; } ``` ### q_insert_tail 在 queue 的尾端插入一個元素 ```c= /* 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. */ 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 = NULL; if (!q) return false; newh = malloc(sizeof(list_ele_t)); if (!newh) return false; newh->value = malloc(strlen(s) + 1); if (!newh->value) { free(newh); return false; } memcpy(newh->value, s, strlen(s) + 1); newh->next = NULL; if (!q->head) q->head = newh; if (!q->tail) q->tail = newh; else { q->tail->next = newh; q->tail = newh; } q->size++; return true; } ``` ### q_remove_head 從 queue 前端移除一個元素,並在參數 ``char *sp`` 不為 ``NULL`` 的情況下將被刪除元素的字串複製最多 ``bufsize-1`` 長度到 ``char *sp`` 中 ```c= /* Attempt to remove element from head of queue. Return true if successful. Return false 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.) The space used by the list element and the string should be freed. */ bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { /* You need to fix up this code. */ list_ele_t *cur; size_t len; if (!q) return false; if (!q->head) return false; cur = q->head; len = strlen(cur->value) < bufsize - 1 ? strlen(cur->value) : bufsize - 1; if (sp) { memcpy(sp, cur->value, len); sp[len] = '\0'; } q->head = q->head->next; free(cur->value); free(cur); q->size--; return true; } ``` ### q_size 用於取得 queue 中元素總數 ```c= /* Return number of elements in queue. Return 0 if q is NULL or empty */ int q_size(queue_t *q) { /* You need to write the code for this function */ /* Remember: It should operate in O(1) time */ if (!q || !q->head) return 0; return q->size; } ``` ### q_reverse 在不使用額外分配記憶體的情況下翻轉 queue 中元素的順序,此處使用了三個指針``prev`` ``curr`` ``next`` ,在循環中 ``curr`` 從 queue 中第一個元素開始走訪到最後一個元素,每次 ``prev``指向 ``curr`` 前一個元素而 ``next`` 指向後一個元素,透過更改這三個元素間的連接達成逆序的目的。在迴圈結束時 ``curr`` 指向 ``NULL`` 而 ``prev`` 指向最後一個元素 ```c= /* Reverse elements in queue No effect if q is NULL or empty This function should not allocate or free any list elements (e.g., by calling q_insert_head, q_insert_tail, or q_remove_head). It should rearrange the existing ones. */ void q_reverse(queue_t *q) { /* You need to write the code for this function */ list_ele_t *prev, *curr, *next; if (!q) return; if (!q->head) return; prev = NULL; curr = q->head; q->tail = q->head; next = NULL; while (curr) { next = curr->next; curr->next = prev; prev = curr; curr = next; } q->head = prev; } ``` ## 自動評分系統 ### driver 在輸入 ``make test`` 進行測試時,Makefile 並沒有直接執行 ``qtest``, 觀察 Makefile 中的規則可以發現, Makefile 以 ``scripts/driver.py`` 調用 ``qtest`` 進行測試 ```shell=17 test: qtest scripts/driver.py scripts/driver.py ``` 再看到 ``scipts/driver.py`` 中的內容, 其中最主要的程式碼都在 ``class Tracer`` 中, 這個 class 負責按照變數 ``traceDirectory`` 與 ``traceDict`` 中紀錄的測試檔案逐一調用 ``qtest`` 進行測試,可以發現變數 ``traceDirectory``紀錄了測試檔案所在資料夾而 ``traceDict`` 中紀錄了測試檔的檔名 ```python=8 class Tracer: traceDirectory = "./traces" qtest = "./qtest" command = qtest verbLevel = 0 autograde = False useValgrind = False traceDict = { 1 : "trace-01-ops", 2 : "trace-02-ops", 3 : "trace-03-ops", 4 : "trace-04-ops", 5 : "trace-05-ops", 6 : "trace-06-string", 7 : "trace-07-robust", 8 : "trace-08-robust", 9 : "trace-09-robust", 10 : "trace-10-malloc", 11 : "trace-11-malloc", 12 : "trace-12-malloc", 13 : "trace-13-perf", 14 : "trace-14-perf", 15 : "trace-15-perf" } ...... ``` 在 ``class Tracer`` 的函式 ``run`` 中, 調用 ``runTrace`` 執行單項測試後依據 ``qtest`` 的返回值判斷是否通過測試並統計得分 ```python=94 for t in tidList: tname = self.traceDict[t] if self.verbLevel > 0: print("+++ TESTING trace %s:" % tname) ok = self.runTrace(t) maxval = self.maxScores[t] tval = maxval if ok else 0 print("---\t%s\t%d/%d" % (tname, tval, maxval)) score += tval maxscore += maxval scoreDict[t] = tval print("---\tTOTAL\t\t%d/%d" % (score, maxscore)) ``` 觀察調用 ``qtest`` 的函式 ``runTrace`` 後可知, 當執行 ``make test`` 時, 對於 ``qtest`` 的調用為命令 ``./qtest -v 1 -f ./traces/trace-XX-XXXX``, 其中 ``trace-XX-XXXX``為對應的測試檔名, 此命令的意思為設定 ``verblevel`` 為 1 並將 ``trace-XX-XXXX`` 作為輸入檔案 ```python=63 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.command, 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 ``` ### qtest * getopt 在 ``main`` 的開頭會先初始化一些區域變數, 緊接著就直接調用 ``getopt`` 函式對命令行參數進行處理, 從程式碼中可知 ``qtest`` 接受 4 種選項, 分別為 ``-h`` ``-f`` ``-v`` ``-l``, 功能如下: * h : 印出幫助消息 * f : 後接一個參數, 指名要輸入的檔案 * v : 後接一個數字參數, 指名 verblevel * l : 後接一個參數, 指名要輸出的檔案 ```c= while ((c = getopt(argc, argv, "hv:f:l:")) != -1) { switch (c) { case 'h': usage(argv[0]); break; case 'f': strncpy(buf, optarg, BUFSIZE); buf[BUFSIZE - 1] = '\0'; infile_name = buf; break; case 'v': level = atoi(optarg); break; case 'l': strncpy(lbuf, optarg, BUFSIZE); buf[BUFSIZE - 1] = '\0'; logfile_name = lbuf; break; default: printf("Unknown option '%c'\n", c); usage(argv[0]); break; } } ``` 資料參考自: > http://man7.org/linux/man-pages/man3/getopt.3.html * queue_init 設定 signal handler 用於處理 ``SIGSEGV`` 及 ``SIGALRM`` 兩種信號, ``SIGSEGV`` 為發生 segment fault 後產生的信號, ``SIGALRM`` 為設定的定時器到時觸發的信號 ```c= static void queue_init() { fail_count = 0; q = NULL; signal(SIGSEGV, sigsegvhandler); signal(SIGALRM, sigalrmhandler); } ``` ``qtest`` 中用於處理 signal 函式都在 ``harness.c`` 中, 為了學習 ``qtest`` 中對於 ``SIGSEGV``, 寫了一個簡單的小程式嘗試使用 ``SIGSEGV`` 的 signal handler, 程式碼如下: ```c= #include <stdio.h> #include <signal.h> void sigsegvhandler(int sig) { printf("sig:%d \n",sig); } int main() { int *p = NULL; signal(SIGSEGV, sigsegvhandler); *p = 100; return 0; } ``` 然而執行後卻發現 signal handler 被不停調用, 而非如預想中只執行一次, 經過資料搜尋與驗證後得知當 signal handler 返回後回到引發 ``SIGSEGV`` 的代碼繼續執行, 因此 ``SIGSEGV`` 一直被相同的程式碼觸發, 然而如此無法達到處理 ``SIGSEGV`` 的目的, 必須找出辦法在 signal handler 結束後不再執行惠引發錯誤的程式碼, 一種方法就是 ``qtest`` 中使用的 ``sigsetjmp`` 與 ``siglongjmp`` 函數, ``sigsetjmp`` 能夠存儲執行環境而 ``siglongjmp`` 能跳轉回預先除存的執行環境, 且 ``sigsetjmp`` 在直接調用時會返回 0, 在 ``siglongjmp`` 跳轉時會返回非 0 值, 在 ``qtest`` 中就利用了這個方式: ```c= bool exception_setup(bool limit_time) { if (sigsetjmp(env, 1)) { /* Got here from longjmp */ jmp_ready = false; if (time_limited) { alarm(0); time_limited = false; } if (error_message) { report_event(MSG_ERROR, error_message); } error_message = ""; return false; } else { /* Got here from initial call */ jmp_ready = true; if (limit_time) { alarm(time_limit); time_limited = true; } return true; } } ``` 資料參考自: > https://blog.csdn.net/work_msh/article/details/8470277 > http://man7.org/linux/man-pages/man2/signal.2.html * init_cmd 在 ``init_cmd`` 中會調用 ``add_cmd`` 與 `add_param`` 函式將一些命令存入一個 linked list 中, 並設定輸入檔案與時間 * console_init 在 ``console_init`` 中會將剩下的所有命令都加入 linked list 中, 至此所有在 ``qtest`` 中可用的命令都已經加入了 linked list 中 * run_console ``run_console`` 是 ``qtest`` 程式用於處理輸入並且選擇指令執行的程式, 其中最主要執行命令的函式為 ``interpret_cmda``, 其他相關函式大部分只用於分離命令與參數 * finish_cmd 這個函式負責 ``qtest`` 最後的收尾工作, 其效果與直接輸入 ``quit`` 是一樣的, 而從 ``main``函式的最後幾行可以發現, 只要在 ``run_console`` 與 ``finish_cmd`` 中發生一次諸如命令錯誤或記憶體洩漏, ``qtest`` 在最後都會返回非 0 值, 代表程式執行中發生錯誤, ``scripts/driver.py`` 就是利用這一點進行分數計算 ```c=582 bool ok = true; ok = ok && run_console(infile_name); ok = ok && finish_cmd(); return ok ? 0 : 1; ``` ### qtest 進行記憶體檢測的方式 在 ``harness.h`` 有這一段 macro: ```c= #else /* Tested program use our versions of malloc and free */ #define malloc test_malloc #define free test_free #define strdup test_strdup #endif ``` 因此在 queue.c 中所有的 ``malloc`` ``free`` ``strdup`` 都被替換為 ``qtest`` 自己實現的函數, 在分配記憶體時 ``qtest`` 會將所有的記憶體放入一個 linked list 中, 並利用 ``MAGICHEADER`` ``MAGICFREE`` ``MAGICFOOTER`` ``FILLCHAR`` 紀錄該塊記憶體的狀況,若是已分配的就用 ``MAGICHEADER`` 反之用 ``MAGICFREE``, 並在既已體的末端填入 ``MAGICFOOTER`` 且在記憶體剛分配時將payload全部初始化為 ``FILLCHAR`` ```c= /* Value at start of every allocated block */ #define MAGICHEADER 0xdeadbeef /* Value when deallocate block */ #define MAGICFREE 0xffffffff /* Value at end of every block */ #define MAGICFOOTER 0xbeefdead /* Byte to fill newly malloced space with */ #define FILLCHAR 0x55 ``` 在檢查是否存在記憶體洩漏或破壞時只須檢測 linked list 中記憶體塊的分配情況與 magic number 是否被改寫就能進行判斷, 例如在函式 ``test_free`` 中的這一段程式碼: ```c=161 block_ele_t *b = find_header(p); size_t footer = *find_footer(b); if (footer != MAGICFOOTER) { report_event(MSG_ERROR, "Corruption detected in block with address %p when " "attempting to free it", p); error_occurred = true; } b->magic_header = MAGICFREE; *find_footer(b) = MAGICFREE; memset(p, FILLCHAR, b->payload_size); ```