# 2020q1 Homework1 (lab0) contributed by < `eric5800602` > ## 開發環境 ```shell $ uname -a Linux os2018 4.15.0-34-generic #37-Ubuntu SMP Mon Aug 27 15:21:48 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux $ gcc --version gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 1 On-line CPU(s) list: 0 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz Stepping: 9 CPU MHz: 2496.004 BogoMIPS: 4992.00 Hypervisor vendor: KVM Virtualization type: full L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 6144K NUMA node0 CPU(s): 0 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid pni pclmulqdq monitor ssse3 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase avx2 invpcid rdseed clflushopt flush_l1d ``` ## 作業要求 在 `queue.[c/h]` 中完成以下實作 * `q_new`: 建立新的空 `queue` * `q_free`: 釋放 `queue` 所佔用的記憶體 * `q_insert_head`: 在 `queue` 開頭加入給定的新元素,並以LIFO準則 * `q_insert_tail`: 在 `queue` 尾部加入給定的新元素,並以FIFO準則 * `q_remove_head`: 移除在 `queue` 開頭的元素。 * `q_size`: 計算 `queue` 中的元素數量。 * `q_reverse`: 將 `queue` 反轉,此函式不能配置或釋放任何記憶體,也就是說只能重新鏈結已經存在的元素; * `q_sort`: 以 [natural sort](https://github.com/sourcefrog/natsort)來排序鏈結的元素 ## 實作 ### `queue_t` 資料結構 為了讓 `q_insert_tail` 與 `q_size` 的實作能在時間複雜度為$O(1)$的條件下完成,所以根據 [linux2020-lab0#牛刀小試](https://hackmd.io/@sysprog/linux2020-lab0#%E7%89%9B%E5%88%80%E5%B0%8F%E8%A9%A6) 所述,在 `queue_t` 的結構中增加: - `tail` 記錄 `queue` 的尾部元素的記憶體位置 - `size` 儲存 `queue` 的總元素個數 ```cpp typedef struct { list_ele_t *head; /* Linked list of elements */ list_ele_t *tail; /* Recording the tail of queue can make q_insert_tail operate in O(1) time*/ int size; } queue_t; ``` ### q_new * 因`malloc`可能會回傳 `NULL`,為提高程式碼的穩固程度,所以必須進行處理。 * 根據[ISO/IEC 9899 (a.k.a C99 Standard)](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf)中7.20.3.3所述,`malloc` 後記憶體區塊的 value is indeterminate,所以加入 `memset` 來初始化記憶體區塊 ```clike= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); /* Return NULL if malloc return NULL. */ if (!q) { return NULL; } memset(q, 0, sizeof(queue_t)); q->head = NULL; q->tail = NULL; /* Initial the size of queue to zero. */ q->size = 0; return q; } ``` ### q_free * 因傳入的 `queue` 有可能為 `NULL`,為提高程式碼的穩固程度,所以需要加入判斷 * 為避免出現 memory leak 之情況,須確保每個元素都被釋放掉 * 利用 `tmp` 儲存當前 `head` 下一個元素的位置,並釋放 `head` 元素,再讓 `queue` 的 `head` 指到下一個元素(也就是 `tmp`),直到下一個元素位置為 `NULL` * 最後釋放 `queue` 本身的記憶體 ```clike= void q_free(queue_t *q) { /* Free queue structure */ /* Check whether q is NULL. */ if (!q) { return; } /* Free all elements in queue. */ while (q->head) { list_ele_t *tmp = q->head->next; free(q->head->value); free(q->head); q->head = tmp; } free(q); return; } ``` ### q_insert_head * 判斷 input `queue` 是否為空,因需插入新的元素,所以也須判斷 `malloc` 之回傳值 * 新元素的 `value` 申請空間為本身字數加上最後的`'\0'` 也就是 `(strlen(s) + 1) * sizeof(char)` * 新元素的 `value` 在 `malloc` 後也要記得判斷回傳值 * 使用 `memset` 確保記憶體區塊初始化 * 使用 `strncpy` 確保不會發生緩衝區溢位之問題 * 讓新創建元素的 `next` 指向當前 `queue` 中的 `head` ,再將 `queue` 中的 `head` 指向新創建的元素 ```clike= bool q_insert_head(queue_t *q, char *s) { /* Check whether q is NULL. */ if (!q) { return false; } list_ele_t *newh; newh = malloc(sizeof(list_ele_t)); /* Return NULL if malloc return NULL. */ if (!newh) { return false; } /* Allocate space for the string. */ newh->value = malloc((strlen(s) + 1) * sizeof(char)); /* Return NULL if malloc return NULL. */ if (!newh->value) { free(newh); return false; } /* Reset the space of string malloc just recently*/ memset(newh->value, 0, sizeof(char) * (strlen(s) + 1)); strncpy(newh->value, s, strlen(s)); newh->next = q->head; q->head = newh; /* If the tail in queue is NULL, assign newh to it. */ if (!q->tail) { q->tail = newh; } /* The size of queue plus one. */ q->size++; return true; } ``` ### q_insert_tail * 為在時間複雜度為$O(1)$的條件下完成,利用 `queue` 中本身已儲存之 `tail` 的記憶體位置以達成 * 基本之考慮點與 `q_insert_head` 同 * 如果在 `q_insert_tail` 之前 `queue` 中無元素,需額外更新 `queue` 中 `head` 之值 ```clike= bool q_insert_tail(queue_t *q, char *s) { /* Check whether q is NULL. */ if (!q) { return false; } list_ele_t *newt; newt = malloc(sizeof(list_ele_t)); /* Return NULL if malloc return NULL. */ if (!newt) { return false; } /* Allocate space for the string. */ newt->value = malloc(sizeof(char) * (strlen(s) + 1)); /* Return NULL if malloc return NULL. */ if (!newt->value) { free(newt); return false; } /* Reset the space of string malloc just recently*/ memset(newt->value, 0, sizeof(char) * (strlen(s) + 1)); strncpy(newt->value, s, strlen(s)); newt->next = NULL; if (q->tail) { q->tail->next = newt; } q->tail = newt; /* If the head in queue is NULL, assign newt to it. */ if (!q->head) { q->head = newt; } /* The size of queue plus one. */ q->size++; return true; } ``` ### q_remove_head * 先確認 `queue` 是否為空,以及其中是否有元素可以被移除 * 初始化 `sp` 中的內容,以免舊資料未清空造成錯誤,並全部指定為 `'\0'` * 同樣使用 `strncpy` 確保無緩衝區溢位問題,只更新到 `bufsize-1` 是因為確保字串結尾為原本初始化之 `'\0'` * 紀錄原始的 `head` 位置,並更新 `queue` 中 `head` 的位置至下一個 * 釋放原始 `head` 的記憶體空間 ```clike= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { /* Return false if queue is NULL or empty. */ if (!q || !q->size) { return false; } /* If sp is non-NULL and an element is removed, copy the removed string to sp. */ if (sp != NULL && q->head->value) { memset(sp, '\0', bufsize); strncpy(sp, q->head->value, bufsize - 1); } list_ele_t *tmp = q->head; q->head = q->head->next; /* Free the spaces used by the list element and the string. */ free(tmp->value); free(tmp); q->size--; return true; } ``` ### q_reverse * 原本沒看到不能新增釋放記憶體,使用類似 [Reversing a Queue](https://www.geeksforgeeks.org/reversing-a-queue) 的方法,結果在 `malloc` 時一直出現 `FATAL ERROR: Calls to malloc disallowed` ,最後在看 comment 時才看到不能新增及釋放記憶體,現已修正 * 在 `queue` 為 `NULL` 、 `queue` 中無元素 、 `queue` 中只有一個元素之狀況無須 reverse * 先把 `tail` 改為 `head` ,並利用 `back` 及 `front` 兩 pointer 紀錄下一個以及前一個的元素 * 記得要將 `q->tail->next` 清除為 `NULL`,不然會造成環狀結構 * 利用 while loop 將當前元素的指標方向顛倒,直到 `q->head->next` 為 `NULL` 也就是到原本 `queue` 的尾巴 * 曾試過只利用一個 `tmp` 來暫時存放前一個元素位置但發現還有原本的 `q->head->next` 需要存,不然一旦將 `q->head->next` 改變,就再也找不到它了,所以才有第 25 行錯誤寫法的 comment ```clike= void q_reverse(queue_t *q) { /* Return if queue is NULL or empty. */ if (!q || q->size == 0 || q->size == 1) { return; } /* Declare back & front pointer for reversing. */ list_ele_t *back, *front; /* Reverse elements in queue. */ /* Change the tail of queue and record previous pointer in back. */ q->tail = q->head; back = q->head; /* Move the head pointer to the next and record next pointer in fornt.*/ q->head = q->head->next; front = q->head->next; /* Clear the next of tail to NULL*/ q->tail->next = NULL; while (front) { /* Change the next pointer of current head to previous. */ q->head->next = back; /* Repeat recording back & front pointer and * move the head of q to the next. */ back = q->head; /* Wrong writage: q->head = q->head->next. * Because q->head->next had been changed before.*/ q->head = front; front = q->head->next; } q->head->next = back; } ``` ### q_size * 因為在 `queue.h` 中 queue_t 有加入 size 紀錄 queue 的大小,可以直接作為回傳值,因此時間複雜度是$O(1)$ ```clike= int q_size(queue_t *q) { if (q && q->size) { return q->size; } return 0; } ``` ### q_sort * 根據 [LinkedListSort](https://npes87184.github.io/2015-09-12-linkedListSort/) 中的 Merge Sort 製作 q_sort,用以達成時間複雜度為 $O(nlogn)$ 並通過自動測試程式 * 因不能新增或釋放記憶體,所以也不能使用 Pseudo Node 實作 * 在 2/17 前實作,所以也沒有用到 [natural sort](https://github.com/sourcefrog/natsort) ```clike= list_ele_t *mergeSortList(list_ele_t *head) { // merge sort if (!head || !head->next) return head; list_ele_t *fast = head->next; list_ele_t *slow = head; // split list while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; // sort each list list_ele_t *l1 = mergeSortList(head); list_ele_t *l2 = mergeSortList(fast); // merge sorted l1 and sorted l2 return merge(l1, l2); } list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { /* Using merge with pseudo node will cause following error * FATAL ERROR: Calls to malloc disallowed * FATAL Error. Exiting */ /* Declare pointer q as result for returning. */ list_ele_t *temp = NULL, *q = NULL; /* Initial temp and q pointers for merge starting. */ if (strnatcmp(l1->value, l2->value) < 0) { q = l1; temp = q; l1 = l1->next; } else { q = l2; temp = q; l2 = l2->next; } /* Merge two list(l1 & l2) until the end of one. */ while (l1 && l2) { if (strnatcmp(l1->value, l2->value) < 0) { temp->next = l1; temp = temp->next; l1 = l1->next; } else { temp->next = l2; temp = temp->next; l2 = l2->next; } } /* Another list may not be over yet. */ if (l1) temp->next = l1; if (l2) temp->next = l2; return q; } ``` * 以上之程式碼會造成 `Program received signal SIGSEGV, Segmentation fault.` 之錯誤,並在 valgrind 中出現`ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient` 及 `Address 0x0 is not stack'd, malloc'd or (recently) free'd` * 利用 GDB 測試程式碼在 gdb bt 發現以下情況,一直不斷地呼叫merge,查詢及看到社團中有人有同樣的情況,說明是 stack overflow 造成的 ```jsx= #3335 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f1080e0) at queue.c:224 #3336 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f108060) at queue.c:224 #3337 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107fe0) at queue.c:224 #3338 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107f60) at queue.c:224 #3339 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107ee0) at queue.c:224 #3340 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107e60) at queue.c:224 #3341 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107de0) at queue.c:224 #3342 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107d60) at queue.c:224 #3343 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107ce0) at queue.c:224 #3344 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107c60) at queue.c:224 #3345 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107be0) at queue.c:224 #3346 0x0000555555558c60 in merge (l1=l1@entry=0x555560e7ece0, l2=0x55555f107b60) ``` * 為證明此情況為 stack overflow 所造成的另外寫一個程式並同樣利用 GDB 觀察 * 程式碼: ```clike= #include <stdio.h> void foo() { int c = 1; foo(); } int main() { foo(); } ``` * gdb bt 後也發生同樣的情況,在 valgrind 中也清楚地表示發生 `Stack overflow in thread #1: can't grow stack to 0x1ffe801000` 之情況 ```jsx= #5428 0x0000555555554613 in foo () #5429 0x0000555555554613 in foo () #5430 0x0000555555554613 in foo () #5431 0x0000555555554613 in foo () #5432 0x0000555555554613 in foo () #5433 0x0000555555554613 in foo () #5434 0x0000555555554613 in foo () #5435 0x0000555555554613 in foo () #5436 0x0000555555554613 in foo () #5437 0x0000555555554613 in foo () #5438 0x0000555555554613 in foo () #5439 0x0000555555554613 in foo () #5440 0x0000555555554613 in foo () #5441 0x0000555555554613 in foo () #5442 0x0000555555554613 in foo () #5443 0x0000555555554613 in foo () #5444 0x0000555555554613 in foo () #5445 0x0000555555554613 in foo () #5446 0x0000555555554613 in foo () #5447 0x0000555555554613 in foo () #5448 0x0000555555554613 in foo () #5449 0x0000555555554613 in foo () #5450 0x0000555555554613 in foo () ``` * 為改善上述錯誤情況,將 merge sort 從遞迴形式改寫為迭代的方式,並改用 `natsort` 中的 `strnatcmp` 函式做比較 * 將 queue sort 問題 divide and conquer,最後 return 結果之 `queue->head` ```clike= list_ele_t *mergeSortList(list_ele_t *head) { // merge sort if (!head || !head->next) return head; list_ele_t *fast = head->next; list_ele_t *slow = head; // split list while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; // sort each list list_ele_t *l1 = mergeSortList(head); list_ele_t *l2 = mergeSortList(fast); // merge sorted l1 and sorted l2 return merge(l1, l2); } list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { /* Using merge with pseudo node will cause following error * FATAL ERROR: Calls to malloc disallowed * FATAL Error. Exiting */ /* Declare pointer q as result for returning. */ list_ele_t *temp = NULL, *q = NULL; /* Initial temp and q pointers for merge starting. */ if (strnatcmp(l1->value, l2->value) < 0) { q = l1; temp = q; l1 = l1->next; } else { q = l2; temp = q; l2 = l2->next; } /* Merge two list(l1 & l2) until the end of one. */ while (l1 && l2) { if (strnatcmp(l1->value, l2->value) < 0) { temp->next = l1; temp = temp->next; l1 = l1->next; } else { temp->next = l2; temp = temp->next; l2 = l2->next; } } /* Another list may not be over yet. */ if (l1) temp->next = l1; if (l2) temp->next = l2; return q; } ``` * 利用 `q_sort` 呼叫 `mergeSortList` * 呼叫完成後須變更 `q->tail` 中的位置,避免 `sort` 後呼叫 `q_insert_tail` 造成錯誤 ```clike= void q_sort(queue_t *q) { // merge sort if (!q || q->size == 1 || q->size == 0) { return; } q->head = mergeSortList(q->head); /* Check the tail of queue. */ list_ele_t *tail = q->head; while (tail->next) { tail = tail->next; } q->tail = tail; } ``` * 根據 natsort 原專案內的 test 寫出類似的 trace file * 例如: ```clike= new ih 1.2-15 ih 0.95.6-1 ih 1.2-5 ih 8.1.2-6 ih 0.10-4 ih 990522-1 ih 3.1.1-0 ih 3.22-7 . . . ih 2.1-11 ih 1.0-13 ih 0.9.4-4 sort free quit ``` * 但利用 nasort 卻會出現 `ERROR: Not sorted in ascending order` * 我認為這與自動測試程式的測試方式有關係,於是去找了 `qtest` 中測試 `q_sort` 的部分 ```clike= if (q) { for (list_ele_t *e = q->head; e && --cnt; e = e->next) { /* Ensure each element in ascending order */ /* FIXME: add an option to specify sorting order */ if (strcasecmp(e->value, e->next->value) > 0) { report(1, "ERROR: Not sorted in ascending order"); ok = false; break; } } } ``` * 其中檢查比較的函式是利用 strcasecmp 與 natsort 不同 * 在下述範例中,可看出在某些情況時兩函式之相異結果 ```clike= new ih pic02000 ih pic05 ih pic2 ih pic120 ih pic121 ih pic01 ih pic02 ih pic02a ih pic3 ih pic4 ih pic100 ih pic100a sort free quit ``` * strcasecmp 之排序結果會是: `pic01 pic02 pic02000 pic02a pic05 pic100 pic100a pic120 pic121 pic2 pic3..` * strnatcmp 之排序結果會是: `pic01 pic02 pic02a pic02000 pic05 pic2 pic3 pic4 pic100 pic100a pic120..` ## Valgrind 的運用 * 使用 `make valgrind` ```clike= scripts/driver.py -p /tmp/qtest.VCatNi --valgrind --- 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 ``` * **2/22 make valgrind 時出現重大錯誤(2/23 解決)** * 今天在 fetch upstream 後在嘗試 make valgrind 時,出現 `Call of 'valgrind /tmp/qtest.lgvfhA -v 1 -f ./traces/trace-01-ops.cmd' failed: [Errno 2] No such file or directory` 類似錯誤,檢查發現全部測試檔案都還在 traces 目錄中沒有錯 :::warning “directory” 一詞的翻譯是「目錄」,不要跟 folder (檔案夾) 搞混了,後者是 Microsoft Windows 提出的用語。“directory” 這詞彙已行之有年,我們會說 UNIX directory,而非 folder。 :notes: jserv ::: > [name=SymbolWu]瞭解,感謝提供觀念釐清 * 而我利用舊的 `driver.py` 進行 `make valgrind` 時,就回復正常,研判可能是 `driver.py` 的問題 * 在此附上正常執行與有錯誤的 `dirver.py` * 正常執行: ```clike= #!/usr/bin/python import subprocess import sys import getopt # Driver program for C programming exercise 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" } traceProbs = { 1 : "Trace-01", 2 : "Trace-02", 3 : "Trace-03", 4 : "Trace-04", 5 : "Trace-05", 6 : "Trace-06", 7 : "Trace-07", 8 : "Trace-08", 9 : "Trace-09", 10 : "Trace-10", 11 : "Trace-11", 12 : "Trace-12", 13 : "Trace-13", 14 : "Trace-14", 15 : "Trace-15" } maxScores = [0, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7] def __init__(self, qtest = "", verbLevel = 0, autograde = False, useValgrind = False): if qtest != "": self.qtest = qtest self.verbLevel = verbLevel self.autograde = autograde self.useValgrind = useValgrind 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 def run(self, tid = 0): scoreDict = { k : 0 for k in self.traceDict.keys() } print("---\tTrace\t\tPoints") if tid == 0: tidList = self.traceDict.keys() else: if not tid in self.traceDict: print("ERROR: Invalid trace ID %d" % tid) return tidList = [tid] score = 0 maxscore = 0 if self.useValgrind: self.command = 'valgrind' else: self.command = self.qtest self.qtest = '' 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)) if self.autograde: # Generate JSON string jstring = '{"scores": {' first = True for k in scoreDict.keys(): if not first: jstring += ', ' first = False jstring += '"%s" : %d' % (self.traceProbs[k], scoreDict[k]) jstring += '}}' print(jstring) def usage(name): print("Usage: %s [-h] [-p PROG] [-t TID] [-v VLEVEL] [--valgrind]" % name) print(" -h Print this message") print(" -p PROG Program to test") print(" -t TID Trace ID to test") print(" -v VLEVEL Set verbosity level (0-3)") sys.exit(0) def run(name, args): prog = "" tid = 0 vlevel = 1 levelFixed = False autograde = False useValgrind = False optlist, args = getopt.getopt(args, 'hp:t:v:A', ['valgrind']) for (opt, val) in optlist: if opt == '-h': usage(name) elif opt == '-p': prog = val elif opt == '-t': tid = int(val) elif opt == '-v': vlevel = int(val) levelFixed = True elif opt == '-A': autograde = True elif opt == '--valgrind': useValgrind = True else: print("Unrecognized option '%s'" % opt) usage(name) if not levelFixed and autograde: vlevel = 0 t = Tracer(qtest = prog, verbLevel = vlevel, autograde = autograde, useValgrind = useValgrind) t.run(tid) if __name__ == "__main__": run(sys.argv[0], sys.argv[1:]) ``` * 有錯誤: ```clike= #!/usr/bin/python import subprocess import sys import getopt # Driver program for C programming exercise 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", 16: "trace-16-perf", 17: "trace-17-complexity" } traceProbs = { 1: "Trace-01", 2: "Trace-02", 3: "Trace-03", 4: "Trace-04", 5: "Trace-05", 6: "Trace-06", 7: "Trace-07", 8: "Trace-08", 9: "Trace-09", 10: "Trace-10", 11: "Trace-11", 12: "Trace-12", 13: "Trace-13", 14: "Trace-14", 15: "Trace-15", 16: "Trace-16", 17: "Trace-17" } maxScores = [0, 6, 6, 6, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5] def __init__(self, qtest="", verbLevel=0, autograde=False, useValgrind=False): if qtest != "": self.qtest = qtest self.verbLevel = verbLevel self.autograde = autograde self.useValgrind = useValgrind 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, "-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 def run(self, tid=0): scoreDict = {k: 0 for k in self.traceDict.keys()} print("---\tTrace\t\tPoints") if tid == 0: tidList = self.traceDict.keys() else: if not tid in self.traceDict: print("ERROR: Invalid trace ID %d" % tid) return tidList = [tid] score = 0 maxscore = 0 if self.useValgrind: self.command = 'valgrind ' + self.qtest else: self.command = self.qtest 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)) if self.autograde: # Generate JSON string jstring = '{"scores": {' first = True for k in scoreDict.keys(): if not first: jstring += ', ' first = False jstring += '"%s" : %d' % (self.traceProbs[k], scoreDict[k]) jstring += '}}' print(jstring) def usage(name): print("Usage: %s [-h] [-p PROG] [-t TID] [-v VLEVEL] [--valgrind]" % name) print(" -h Print this message") print(" -p PROG Program to test") print(" -t TID Trace ID to test") print(" -v VLEVEL Set verbosity level (0-3)") sys.exit(0) def run(name, args): prog = "" tid = 0 vlevel = 1 levelFixed = False autograde = False useValgrind = False optlist, args = getopt.getopt(args, 'hp:t:v:A', ['valgrind']) for (opt, val) in optlist: if opt == '-h': usage(name) elif opt == '-p': prog = val elif opt == '-t': tid = int(val) elif opt == '-v': vlevel = int(val) levelFixed = True elif opt == '-A': autograde = True elif opt == '--valgrind': useValgrind = True else: print("Unrecognized option '%s'" % opt) usage(name) if not levelFixed and autograde: vlevel = 0 t = Tracer(qtest=prog, verbLevel=vlevel, autograde=autograde, useValgrind=useValgrind) t.run(tid) if __name__ == "__main__": run(sys.argv[0], sys.argv[1:]) ``` :::warning 用 `diff -up` 列出程式差異即可,之後請送 pull request 修正 Valgrind 使用的問題 :notes: jserv ::: * 在使用 `massif` 前要將 `.valgrindrc` 檔案中的 `--show-leak-kinds=all` 移除才能夠運作 * 下圖為利用 `massif-visualizer` 對 `trace01` 做出的記憶體行為視覺化 ![](https://i.imgur.com/2kqBS2U.png) * 嘗試錯誤: * 在撰寫 `queue.c` 的 `q_free` 過程中曾經忘記將 `queue` 的節點 `value` 的記憶體也釋放,因而造成 `memory leak`,利用 `massif` 實驗此情況 * 實驗 command: ```clike= new ih a ih b ih c sort free quit ``` * **有**將節點的 `value` 釋放: ![](https://i.imgur.com/57Vr68m.png) * **無**將節點的 `value` 釋放: ![](https://i.imgur.com/XdnPx78.png) * 根據結果圖可看出在無釋放時,最後 `memory heap size` 為 126B ;有釋放時則回到 0B ,所以根據 `valgrind` 能確確實實的看到 memory leak,並利用 `massif` 可以更清楚的觀察 ### 找出 2/22 所發生的問題點 (2/23 解決) * 先更新到最新的 `driver.py` 並根據 `diff -up` 找出程式差異,在處理一些無關緊要的差異後發現可能的錯誤點 ```clike= def runTrace(self, tid): if not tid in self.traceDict: self.printInColor("ERROR: No trace with id %d" % tid, self.RED) return False fname = "%s/%s.cmd" % (self.traceDirectory, self.traceDict[tid]) vname = "%d" % self.verbLevel clist = [self.command, "-v", vname, "-f", fname] try: retcode = subprocess.call(clist) except Exception as e: self.printInColor("Call of '%s' failed: %s" % (" ".join(clist), e), self.RED) return False return retcode == 0 def run(self, tid=0): scoreDict = {k: 0 for k in self.traceDict.keys()} print("---\tTrace\t\tPoints") if tid == 0: tidList = self.traceDict.keys() else: if not tid in self.traceDict: self.printInColor("ERROR: Invalid trace ID %d" % tid, self.RED) return tidList = [tid] score = 0 maxscore = 0 if self.useValgrind: self.command = 'valgrind ' + self.qtest else: self.command = self.qtest . . . ``` * 問題出現在第 28 行的 `self.command = 'valgrind ' + self.qtest` 使 `self.command` 值會更動為 `'valgrind /tmp/qtest.xxxxxx'` 然後放在第 7 行的 `clist` 中,並在第 9 行時利用 `subprocess.call` 來進行子進程的呼叫,但這裡的 `subprocess.call` 會直接 fail 並丟出 Exception,所以我根據這個問題進行以下測試 ### Test and resolve * 測試環境: ```shell $ python3 -V Python 3.6.5 ``` * 測試程式: 1. ```python import subprocess subprocess.call(["ls -l"]) ``` 2. ```python import subprocess subprocess.call(["ls","-l"]) ``` * 上述兩程式主要差異在於 `subprocess.call` 中的參數不同 * 測試結果: 1. ```python= Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.6/subprocess.py", line 267, in call with Popen(*popenargs, **kwargs) as p: File "/usr/lib/python3.6/subprocess.py", line 709, in __init__ restore_signals, start_new_session) File "/usr/lib/python3.6/subprocess.py", line 1344, in _execute_child raise child_exception_type(errno_num, err_msg, err_filename) FileNotFoundError: [Errno 2] No such file or directory: 'ls -l': 'ls -l' ``` 2. ```python= total 524 -rw-rw-r-- 1 os2018 os2018 15909 Feb 22 15:50 console.c -rw-rw-r-- 1 os2018 os2018 1972 Feb 22 15:50 console.h -rw-rw-r-- 1 os2018 os2018 52072 Feb 23 11:46 console.o drwxrwxr-x 2 os2018 os2018 4096 Feb 23 11:46 dudect . . . ``` * 從結果可看出第一種寫法會造成程式不可執行,也就是 `driver.py` 可能的問題,所以只要將寫法改成第二種就可以執行了 * WHY? * 在查閱一些資料後得出結論,在 `subprocess.py` 中定義`call` 函式的第一個參數 `args` ,可以是一個字串,可以是一個包含程序參數的 list。要执行的程序一般就是这个列表的第一项,或者是字串本身。 `subprocess.call(["ls","-l"])` `subprocess.call(["ls -l"])` 這兩者中,第二個將不能執行。若參數要為字串時,該字串須為程式的路徑才可以。 但是下面的程式碼可以成功執行 ` subprocess.call(["ls -l"], shell=True)` 原因是,它相當於 ` subprocess.call(["/bin/sh", "-c", "ls -l"]) ` 當 shell=False 時 (預設值),call 使用 os.execvp() 來執行子行程。args 一般須為一個 list。如果 args 是字元的話,會作為可執行檔案之路徑,無法傳入參數。 但是,在 Windows 下,兩種皆可以運作,由於 windows 下的 `CreateProcess` 接受的一個字串。即使是 list 型別的參數,也需要先合併成字串再傳遞給 api,都會變成類似下面的程式碼去執行 `subprocess.call("notepad.exe tmp.txt" shell=True)` * 結論: * 完成下述兩點後即可運作: * 將 `clist = [self.command, "-v", vname, "-f", fname]` 修改成 `clist = [self.command, self.qtest, "-v", vname, "-f", fname]` * 將 `self.command = 'valgrind ' + self.qtest` 修改成 `self.command = "valgrind"` ## Dude, is my code constant time? * 根據 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf) 所述,`dudect` 的運作步驟如下: 1. 此首先定義兩種 class 的 input data,一種是 fixed constant value ,另一種是隨機的 random input data ,一種用以測試自己的程式運作時長是否與另一種一樣,來驗證時間複雜度,而另一種則是用以 trigger certain "special" corner-case processing (若是驗證 constant time,就做一定符合的運算式,用以做比對) 2. 利用 Modern CPUs 提供的 cycle counters 做為運作時長的標準 3. Two sample t-test:比較兩組 data 的平均運作時長是否有差異 * 試著利用 t-test 去證明 null hypothesis 是錯的,反證出兩組 data 的平均運作時長是相同的 4. 計算有顯著性差異的數據筆數,若 $\dfrac{顯著性差異的數據筆數}{總數據筆數}$ 在 0.05 之上,兩組數據具備顯著性差異的可能性為95%。兩個數據所代表的樣本還有5%的可能性是沒有差異的,反之則也可用同樣的觀念表達,而其中5%的可能性是由於隨機誤差造成的 * 在 `dudect` 中,則是同時檢驗顯著性差異水平達到 0.05 以及 0.01,其中預設總筆數為 10000 ,所以若有 500 筆以上的資料具有顯著性差異則基本可以確認不符合 constant time ,若界在 500 與 10 筆之間代表其實有 99% 的機率該程式符合 contant time ```cpp #define enough_measurements 10000 // pretty arbitrary #define number_percentiles 100 // threshold values for Welch's t-test #define t_threshold_bananas 500 // test failed, with overwhelming probability #define t_threshold_moderate 10 if (max_t > t_threshold_bananas) { printf(" Definitely not constant time.\n"); exit(0); } if (max_t > t_threshold_moderate) { printf(" Probably not constant time.\n"); } if (max_t < t_threshold_moderate) { printf(" For the moment, maybe constant time.\n"); } ``` * Simulation 之實作方式 * 基本上的流程與 `dudect` 相同,主要差別在於如何定義兩種 class 的 input data 以及因為要驗證 constant time,所以需要一個一定符合 constant time 的基本運算式,後續的流程基本上無相異 * ```clike= if (mode == test_insert_tail) { for (size_t i = drop_size; i < number_measurements - drop_size; i++) { char *s = get_random_string(); dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * chunk_size) % 10000); before_ticks[i] = cpucycles(); dut_insert_tail(s, 1); after_ticks[i] = cpucycles(); dut_free(); } ``` * 在進行 Simulation 時,上面程式之第 5~7 行會執行 `q_insert_head` ,因為此函式一定為 constant time ,適合用以做實驗與比對,其插入之字串也是 random 產生的。在第 9 行時則做 simulation 的主要目標,也就是測試 `q_insert_tail`,並紀錄所使用的 cpucycles ,在總筆數達到 `number_measurements` 後進行顯著性差異的驗證,若可 disprove null hypothesis 則代表該函式時間複雜度真的為 constant time ## select 系統呼叫在本程式的使用方式 * select 的使用方式 * 在 `console.c` 中 select 是用以觀察 `run_console` 函式之參數 `infile_name` ,等待該檔案描述詞狀態的改變或者直到 `timeout` ,不過在 `console.c` 中第 630 行 `cmd_select(0, NULL, NULL, NULL, NULL)` 的 timeout 設為 NULL 表示 select() 沒有 timeout * 當 Commandline input available ,也就是 `readfds`不為空且 `FD_ISSET(infd, readfds)`為 `True` 就會讓呼叫 `readline()` ,並在成功時呼叫 `interpret_cmd(cmdline)` 用以執行該命令 * `console.c` 中處理 `fd_set*` 的函式: 1. `FD_SET(int fd, fd_set *set)`:新增 fd 至 set 中 2. `FD_CLR(int fd, fd_set *set)`:移除 set 中的 fd 3. `FD_ISSET(int fd, fd_set *set)`:確認 set 中有 fd :::warning 除了「舉燭」外,你做了實驗去確認 select 系統呼叫的行為了嗎? :notes: jserv ::: * 實驗select運作 * read 與 readline 問題: ```clike= int main() { int keyboard; fd_set readfd; char cmd[10] = ""; struct timeval timeout; keyboard = open("/dev/tty", O_RDONLY | O_NONBLOCK); assert(keyboard > 0); while (1) { FD_ZERO(&readfd); FD_SET(keyboard, &readfd); select(keyboard + 1, &readfd, NULL, NULL, NULL); if (FD_ISSET(keyboard, &readfd)) { read(keyboard, &cmd, 10); printf("You type:%s\n", cmd); } } } ``` * 從鍵盤讀入自己的輸入的文字並輸出,當運作後輸入"short count"之運作結果: ```bash= short count You type:short coun You type:t ort coun ``` * 改成使用 `console.c` 中的 `readline()` 來取代 `read()`,並定義 `RIO_BUFSIZE` 為 10 ```clike= #define RIO_BUFSIZE 10 int main() { int keyboard; int ret, i; char c; fd_set readfd; struct timeval timeout; char *cmdline; FILE * fp; keyboard = open("/dev/tty", O_RDONLY | O_NONBLOCK); assert(keyboard > 0); rio_ptr rnew = malloc(sizeof(rio_t)); rnew->fd = keyboard; rnew->cnt = 0; rnew->bufptr = rnew->buf; rnew->prev = buf_stack; buf_stack = rnew; while (1) { FD_ZERO(&readfd); FD_SET(keyboard, &readfd); ret = select(keyboard + 1, &readfd, NULL, NULL,NULL); if (FD_ISSET(keyboard, &readfd)) { cmdline = readline(); //read(keyboard,cmdline,1); printf("You type cmd : %s\n",cmdline); } } } ``` * 從鍵盤讀入自己的輸入的文字並輸出,當運作後輸入 "short count" 之運作結果: ```bash= short count You type cmd : short coun You type cmd : t ``` * 使用 `readline` 發現不會造成像 `read` 多出 `ort coun` 的問題 * select 實際函式運作 * 以一程式做實驗,並預期以下結果 * 可輸入 "dir" 來運作 "ls -l" 並輸出 * 可以藉由輸入 "sleep" 來執行 `FD_CLR(keyboard, &readfd)`,並在下一個 loop 時因 `FD_ISSET(keyboard, &readfd)` 為 false 所以會執行 `FD_SET(keyboard, &readfd)` 且輸出 `readfd is not in set` ```clike= int main() { int keyboard; int ret, i; char c; fd_set readfd; struct timeval timeout; char *cmdline; FILE * fp; keyboard = open("/dev/tty", O_RDONLY | O_NONBLOCK); assert(keyboard > 0); rio_ptr rnew = malloc(sizeof(rio_t)); rnew->fd = keyboard; rnew->cnt = 0; rnew->bufptr = rnew->buf; rnew->prev = buf_stack; buf_stack = rnew; FD_SET(keyboard, &readfd); while (1) { if (FD_ISSET(keyboard, &readfd)) { ret = select(keyboard + 1, &readfd, NULL, NULL,NULL); cmdline = readline(); if(strcmp(cmdline,"dir\n") == 0){ if ((fp = popen("ls -l","r")) == NULL){ printf("open ls fail\n"); return -1; } char buf[256]; while (fgets(buf, 255, fp) != NULL) printf("%s", buf); if (pclose(fp) == -1){ printf("open ls fail\n"); return -1; } } else if(strcmp(cmdline,"sleep\n") == 0){ if(FD_ISSET(keyboard, &readfd)){ FD_CLR(keyboard, &readfd); } } else{ printf("You type unknown cmd : %s",cmdline); } } else{ printf("readfd is not in set\n"); FD_SET(keyboard, &readfd); } } } ``` * 實際輸出結果: 1. 輸入 `sleep` 後執行 `dir`: ```bash= sleep readfd is not in set dir total 20 -rwxrwxr-x 1 os2018 os2018 13008 Feb 27 19:24 test -rw-rw-r-- 1 os2018 os2018 3329 Feb 27 19:18 test.c ``` * 運用 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的考量點 1. 避免 short count 造成的 error,在某些情況 `read` 會 transfer 比 user 請求還少的 bytes 1. 若在 read file 時遭遇 EOF 可能會造成 short count 之情況 2. 從終端機 Read line 可能會因為 text line 的大小而造成同等的 short count 2. 藉由快取到內部記憶體 buffer,使得 read text line 更有效率 * 讀取終止條件: 1. 已讀取至 `RIO_BUFSIZE` 2. 遇到 EOF 3. 遇到 Newline * 將 text 存至 buffer ```clike= c = *buf_stack->bufptr++; *lptr++ = c; buf_stack->cnt--; /* Break when encounter newline */ if (c == '\n') break; ``` * 整體 `console.c` 運作情境與流程 1. 從 `bool run_console(char *infile_name)` 進入 2. 呼叫 `push_file` 以 read only 方式開啟 file,並建立新的 `rio_ptr` 儲存 File descriptor 等資料,assign 給 `buf_stack` 3. 持續運作 `cmd_select(0, NULL, NULL, NULL, NULL)` 直至 `buf_stack` 為空或者 `quit` 指令輸入 4. 在 `cmd_select` 中會先利用 `read_ready()` check input buffer 是否真的為一整行 command line,其中就是利用是否有遇到 newline 來判斷的 5. 從 input file 中讀取指令,並在成功返回時利用 `interpret_cmd()` 執行該目標指令 6. 持續讀取指令並運作直至 quit command 或者 EOF