# 2021q1 Homework1 (lab0) contributed by < `ngokchaoho` > ## 作業要求 需要由學員補完並逐步精進,包含以下: * `q_new`: 建立新的「空」佇列; * `q_free`: 釋放佇列所佔用的記憶體; * `q_insert_head`: 在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則); * `q_insert_tail`: 在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則); * `q_remove_head`: 在佇列開頭 (head) 移除 (remove) 給定的元素。 * `q_size`: 計算佇列中的元素數量。 * `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列元素,換言之,它只能重排已經存在的元素; * ==`q_sort`==: 「[Linux 核心設計](http://wiki.csie.ncku.edu.tw/linux/schedule)」課程所新增,以==遞增順序==來排序鏈結串列的元素,可參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 得知實作手法; ## 系統環境 * Distributor ID: Ubuntu * Description: Ubuntu 20.04.1 LTS * Release: 20.04 * Codename: focal :::danger 注意共筆書寫規範:中英文間用一個半形空白字元區隔。唯有掌握各項細節,來日才能征服 Linux 核心。 :notes: jserv ::: ## 實作流程 ### `q_size` 題目要求q_size是$O(1)$, 所以在queue.t這個struct裡面增加size這個member ```cpp int q_size(queue_t *q) { if (q == NULL || q->head == NULL) return 0; return q->size; } ``` ### `q_new` 檢查 malloc 是否成功,這裡可以看到 queue_t 除了 head 和 size, 還有一個 tail 的 member,這是為了可以在 $O(1)$ insert tail 而加的。 ```cpp queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (q != NULL) { q->head = NULL; q->size = 0; q->tail = NULL; } return q; } ``` ### `q_free` 從 Head 數過去,一個一個釋放。 ```cpp void q_free(queue_t *q) { if (q == NULL) return; if (q->head != NULL) { list_ele_t *curr_ele = q->head; while (curr_ele != NULL) { free(curr_ele->value); list_ele_t *prev_ele = curr_ele; if (curr_ele->next != NULL) { curr_ele = curr_ele->next; } else { free(curr_ele); break; } free(prev_ele); } } free(q); } ``` ### `q_insert_head` 因為要用到兩個 malloc,均有可能失敗,所以要 free 兩次。當只有一個元素時,Head 和 Tail 是指向同一記憶體的。 ```cpp bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; if (q == NULL) return false; newh = malloc(sizeof(list_ele_t)); if (newh == NULL) return false; newh->value = malloc(strlen(s) + 1); if (newh->value == NULL) { free(newh); return false; } strncpy(newh->value, s, strlen(s)+1); newh->next = q->head; q->head = newh; q->size += 1; if (q->size == 1) q->tail = q->head; return true; } ``` ### `q_remove_head` 依照題目指示把 element 中的 value 放入 sp, 限定 bufsize - 1,strncat 會自動加上 terminating null-character. 每次刪除都定義prev_head 為現在的 head 然後移動 q->head 到下一順位,最後類似過河拆橋,釋放prev_head相關的記憶體。雖然此作業沒有q_remove_tail, 因此當變成 empty queue 時將 `q->tail` 指向正確的 NULL,沒有即時需要,但考慮到完整和將來擴展,還是將此行為(empty queue 時將 `q->tail` 指向正確的 NULL)實作。 ```cpp bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (q == NULL || q->head == NULL) return false; if (sp != NULL) { strncat(sp, q->head->value, bufsize - 1); } list_ele_t *prev_head = q->head; q->head = q->head->next; free(prev_head->value); free(prev_head); q->size -= 1; if (q->size == 0) { q->tail = NULL; } return true; } ``` ### `q_insert_tail` 為了可達成 $O(1)$ 時間複雜度,如上所述地在 queue_t 這個 struct 裡面加入了 tail, 因此如果不是 empty queue 的話只要記得加 size 和 tail 要指向新的記憶體就可以了, 與 insert head 相同,malloc 有可能失敗,所以如果失敗要free兩次。當只有一個元素時,Head 和 Tail 是指向同一記憶體的。這裡一開始使用了不安全的 strcpy, 因為 destination 的空間是由 `malloc(strlen(s+1))` 所配置,如果 malloc 成功,則不會有 overflow 的問題。 然而 git commit hook 不允許 strcpy,因此改用 strncpy。 ```cpp bool q_insert_tail(queue_t *q, char *s) { if (q == NULL) return false; list_ele_t *newt; newt = malloc(sizeof(list_ele_t)); if (newt == NULL) return false; newt->value = malloc(strlen(s) + 1); if (newt->value == NULL) { free(newt); return false; } newt->next = NULL; strncpy(newt->value, s, strlen(s)+1); q->size += 1; if (q->size > 1) { q->tail->next = newt; q->tail = newt; } else { /*from empty queue*/ q->head = newt; q->tail = newt; } return true; } ``` ### `q_reverse` 走訪 queue 的每一個 element 的同時,將 curr_ele 的 next 指向到 prev_ele。prev_ele 初始化為 NULL 以作為新的結束標識。 ```cpp void q_reverse(queue_t *q) { if (q == NULL || q->head == NULL) return; list_ele_t *curr_ele = q->head; list_ele_t *prev_ele = NULL; while (curr_ele != q->tail) { list_ele_t *next_ele = curr_ele->next; curr_ele->next = prev_ele; prev_ele = curr_ele; curr_ele = next_ele; } curr_ele->next = prev_ele; list_ele_t *tmp = q->head; q->head = q->tail; q->tail = tmp; } ``` ### `q_sort` 參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/), 試作了 linked list 的 selection sort, 因為時間複雜度是 $O(N^2)$,所以沒有通過 trace-16-perf, trace-15-perf。預期可以通過改用 merge sort 解決。 Linked List 的 selection sort 想法很單純,就是從 unsorted_head 開始每次去 queue 的尾部加上考慮範圍裡最小的 element(見 j 迴圈部分),考慮範圍從最開始有 q->size 個 element 要考慮到最後剩下一個。為了要把考慮範圍內的 minNode 從其原本的位置移向最後,有必要記錄其前一個 element (變數名為 min_Prev),將min_Prev 的 next 指向 minNode的 next,以達到跳過的效果。如果 minNode 恰好是 unsorted_head (考慮範圍的開頭)則需要例外處理,將 unsorted_head 替換成其 next,因為 unsorted_head 並沒有被夾在中間,只需要直接將其從考慮範圍中剔除就好。新的 q->head 會是第一個找到的 minNode (最小的元素),因此將其指定為第一次執行 i 迴圈最後時的 sorted_tail 就好。 ```cpp void q_sort(queue_t *q) { if (q == NULL || q->head == NULL || q->size == 1) return; list_ele_t *sorted_tail = q->tail; list_ele_t *unsorted_head = q->head; list_ele_t *prev = NULL; list_ele_t *curr = NULL; list_ele_t *minNode = NULL; list_ele_t *min_Prev = NULL; for (int i = q->size; i > 0; i--) { curr = unsorted_head; minNode = unsorted_head; prev = unsorted_head; min_Prev = unsorted_head; for (int j = 0; j < i; j++) { if (strcmp(curr->value, minNode->value) < 0) { minNode = curr; min_Prev = prev; } curr = curr->next; if (j != 0) prev = prev->next; } sorted_tail->next = minNode; sorted_tail = sorted_tail->next; if (minNode == unsorted_head) { unsorted_head = unsorted_head->next; // moved comparision windows } else { min_Prev->next = minNode->next; } sorted_tail->next = NULL; if (i == q->size) q->head = sorted_tail; } q->tail = sorted_tail; } ``` #### merge sort q_sort via recursion 用遞迴實現 merge sort 可以視為兩個部分,第一部分是將 linked list 拆分為兩個 sub linked list with head l1,l2 直至到最後剩下一個元素或沒有元素時則回傳自身head, 其他的上層則開始逐層回傳merge(l1, l2),第二部分是逐層將已經自身排序好的兩個 sub linked list,逐一從兩個開頭 l1,l2 比較,有序地融合,詳情見 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/)。 ```cpp list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { if (!l2) return l1; if (!l1) return l2; if (strcmp(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 *merge_sort_list(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 = merge_sort_list(head); list_ele_t *l2 = merge_sort_list(fast); return merge(l1, l2); } /* * Sort elements of queue in ascending order * No effect if q is NULL or empty. In addition, if q has only one * element, do nothing. */ void q_sort(queue_t *q) { if (q == NULL || q->head == NULL || q->size == 1) return; q->head = merge_sort_list(q->head); list_ele_t *tmp = q->head; while (tmp->next != NULL) tmp = tmp->next; q->tail = tmp; } ``` 此時,使用`make valgrind`可見 >==10673== Process terminating with default action of signal 11 (SIGSEGV) ==10673== Access not within mapped region at address 0x1FFE8010A8 ==10673== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==10673== at 0x10E538: merge (queue.c:179) ==10673== If you believe this happened as a result of a stack ==10673== overflow in your program's main thread (unlikely but ==10673== possible), you can try to increase the size of the ==10673== main thread stack using the --main-stacksize= flag. ==10673== The main thread stack size used in this run was 8388608. ==10673== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 推測是用遞迴實作 merge 部分導致了 stack overflow,假設需要 merge 兩個合共有 n 個元素的 sub linked list,則需要 O(n) stack 記憶體。 此外,當行程收到 signal - SIGSEGV 而結束時被不會釋放由 malloc 分配存放於 heap 中的記憶體。 >==12937== 5 bytes in 1 blocks are still reachable in loss record 1 of 36 ==12937== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==12937== by 0x10CA22: strsave_or_fail (report.c:230) ==12937== by 0x10D282: parse_args (console.c:192) ==12937== by 0x10D282: interpret_cmd (console.c:243) ==12937== by 0x10DB03: cmd_select (console.c:594) ==12937== by 0x10DDD0: run_console (console.c:667) ==12937== by 0x10C283: main (qtest.c:780) 修改使用 while loop, ```cpp list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { list_ele_t *temp = NULL; list_ele_t *head = NULL; while (l1 && l2) { if (strcmp(l1->value, l2->value) < 0) { if (temp == NULL) { temp = l1; head = l1; l1 = l1->next; } else { temp->next = l1; temp = l1; l1 = l1->next; } } else { if (temp == NULL) { temp = l2; head = l2; l2 = l2->next; } else { temp->next = l2; temp = l2; l2 = l2->next; } } } if (l1) temp->next = l1; if (l2) temp->next = l2; return head; } ``` 此時,所有 test-case 都能通過了。 ### `開啟 Address Sanitizer,修正 qtest 執行過程中的錯誤` 依照課程指示 > 開啟 Address Sanitizer,修正 qtest 執行過程中的錯誤 先執行 qtest 再於命令提示列輸入 help 命令,會使 開啟 Address Sanitizer 觸發錯誤,應予以排除 得到錯誤提示 >==13821==ERROR: AddressSanitizer: global-buffer-overflow on address 0x55f5de2143c0 at pc 0x55f5de1fd7dd bp 0x7fff98faa230 sp 0x7fff98faa220 READ of size 4 at 0x55f5de2143c0 thread T0 #0 0x55f5de1fd7dc in do_help_cmd /home/ngokchaoho/linux2021/lab0-c/console.c:307 #1 0x55f5de1fd8f0 in interpret_cmda /home/ngokchaoho/linux2021/lab0-c/console.c:221 #2 0x55f5de1fe0d5 in interpret_cmd /home/ngokchaoho/linux2021/lab0-c/console.c:244 #3 0x55f5de1ff818 in run_console /home/ngokchaoho/linux2021/lab0-c/console.c:660 #4 0x55f5de1fc405 in main /home/ngokchaoho/linux2021/lab0-c/qtest.c:780 #5 0x7f07d05d80b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2) #6 0x55f5de1f9bad in _start (/home/ngokchaoho/linux2021/lab0-c/qtest+0x8bad) 可以看出,這是嘗試讀入一個 size 4 的全局變數時,遇到的錯誤。推測該段 size 4 記憶體至少沒有完全分配給此全局變數。 >SUMMARY: AddressSanitizer: global-buffer-overflow /home/ngokchaoho/linux2021/lab0-c/console.c:307 in do_help_cmd 然後,又見總結部分的提示錯誤出現在307行,於是初步猜測為 ` *plist->valp ` 引起的錯誤,因為 int 在 64bits 機器上剛好是4個 byte. 於是前往 ` param_list ` 初始化的地方 ` init_cmd ` ,發現 ` &echo ` 和 ` &simulation ` 兩個指向 bool 變數的指標被轉換成了指向 int 變數。推測此處會發生錯誤,因為 bool 型變數只會被分配 1 byte. 因此,考慮到` *plist->valp `是 int, 所以將 ` &echo ` 和 ` &simulation ` 的定義型態改成 int, 預設值為 0 。 重複上述課程指示,確認錯誤已經消失。 ### `運用 Valgrind 排除 qtest 實作的記憶體錯誤` 使用 ` make valgrind ` 可見以下由 linenoise 中為 command history 分配的記憶體沒有被釋放的錯誤。 >#Test of insert_head and remove_head ==94206== 14 bytes in 1 blocks are still reachable in loss record 1 of 3 ==94206== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==94206== by 0x4A4E50E: strdup (strdup.c:42) ==94206== by 0x1100CD: linenoiseHistoryAdd (linenoise.c:1236) ==94206== by 0x110C60: linenoiseHistoryLoad (linenoise.c:1325) ==94206== by 0x10C24C: main (qtest.c:769) ==94206== ==94206== 127 bytes in 19 blocks are still reachable in loss record 2 of 3 ==94206== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==94206== by 0x4A4E50E: strdup (strdup.c:42) ==94206== by 0x110041: linenoiseHistoryAdd (linenoise.c:1236) ==94206== by 0x110C60: linenoiseHistoryLoad (linenoise.c:1325) ==94206== by 0x10C24C: main (qtest.c:769) ==94206== ==94206== 160 bytes in 1 blocks are still reachable in loss record 3 of 3 ==94206== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==94206== by 0x11008D: linenoiseHistoryAdd (linenoise.c:1224) ==94206== by 0x110C60: linenoiseHistoryLoad (linenoise.c:1325) ==94206== by 0x10C24C: main (qtest.c:769) ==94206== 前往 linenoise.c 即可發現需要用到 [atexit](http://www.cplusplus.com/reference/cstdlib/atexit/) (linenoiseAtExit),程式才會在結束時,釋放 history 相關記憶體。而如果有用 linenoise 的 main api,設置 exit 行為的部分就會被運行,因此,當 `./qtest` 手動測試時,不會遇到此記憶體錯誤。 但事實上,當用檔案測試時根本用不到 linenoise,我們不會在測試 trace 裡用到 line editing。因此,解決方案為僅在不提供檔案名時(即執行 `qtest` 並啟動 shell 時),載入 history 以方便 line editing. ```cpp if (infile_name == NULL) { linenoiseHistorySetMaxLen(HISTORY_LEN); linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ } ``` ### `透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗` 運行`valgrind -q --tool=massif ./qtest -f traces/trace-15-perf.cmd ` 並使用 `massif-visualizer massif.out.9137` 即可得下圖 ![](https://i.imgur.com/3BQBA9F.png) 下列是實際執行的命令(command) ```shell # Test performance of insert_tail, size, reverse, and sort option fail 0 option malloc 0 new ih dolphin 1000000 it gerbil 1000000 size 1000 reverse sort size 1000 ``` 可見上圖符合預期,因為只有在前面做 insert head 或 insert tail 的時候會用malloc獲得 heap 記憶體。留意,如果直接用原本的 qtest 則會出現 ![](https://i.imgur.com/GoYeY6n.png) ```shell= $ valgrind -q --tool=massif ./qtest -f traces/trace-15-perf.cmd cmd> # Test performance of insert_tail, size, reverse, and sort cmd> option fail 0 cmd> option malloc 0 cmd> new q = [] cmd> ih dolphin 1000000 ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient ERROR: Insertion of dolphin failed (1 failures total) q = [dolphin ... ] cmd> it gerbil 1000000 ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient ERROR: Insertion of gerbil failed (2 failures total) ``` 原因是 ` massif ` 導致的超時錯誤使得 ok==0 而原本的 qtest 是在 ` ok = ok && finish_cmd(); ` 釋放記憶體的。如果 ok == 0, 則 ` finish_cmd ` 在 && 邏輯下將不再執行 ` finish_cmd ` 因此亦不會釋放任何 heap 記憶體。 更改為` ok = finish_cmd() && ok` 可以緩解這個問題,但由於錯誤在 malloc 後發生(e.g. insert_head strncpy),因此分配得到的記憶體並沒有於 queue 中記錄,結果導致程式沒法釋放該類記憶體, 預期可以在 harness 設計一個釋放記錄於 BELE 雙向鏈表的函式進一步主動釋放更多記憶體。 然而,由於 signal 可以在任何地方發生,此舉不能保證所有 system malloc 都有被記錄在雙向鏈表,最後的防線依然在 OS。 ## 研讀 Dude, is my code constant time 論文使用 [welch's t-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) 去 分辨程式是否有 time-leakage.即量測程式執行時間是否能在常數時間 $O(1)$ 完成。 利用 leakage detection test 測試程式是否是 constant time ,優點是在這個方法下不需要硬體模型,分為以下三個步驟進行 1. Repeatedly measure exeution time 分別用兩類(class) input data ,論文中提到的是 fix-vs-random , fixed class 可以選擇 corner-case 已達到檢查一些特定的情況是否於隨機的情況有顯著差異,常見於 [A/B test](https://en.wikipedia.org/wiki/A/B_testing) 2. Apply post-processing - Cropping: 去掉某些極端值,這些極值往往是一些與程式無關的情況引至的,例如 OS interruption - High-order preprocessing: 由於此論文只是用 t - test 去比較平均時間,亦即 first order mean 的比較,所以並不需要 higher-order preprocessing. 3. Apply statistical test - null hypothesis: "the two timing distributions are equal" - if statistical test reject the null hypothesis, then we know it's not run in constant time - [Welch's t-test](https://en.wikipedia.org/wiki/Welch's_t-test): 本專案使用的測試 ### Welch’s t-test - 先計算出兩個 class (class 0, class 1) 的平均值和變異數 - Welch's t-test defines the statistic t by the following formula: - $t = \frac{\bar{X_0} - \bar{X_1}}{\sqrt{\frac{Var_0}{N_0} + \frac{Var_1}{N_1}}}$ - $t$ 越接近 0 代表兩組數據的差異越小 - lab0 實作上只有到這裡,然後直接檢查 $t$ 是否大於 `t_threshold_moderate` default = 10 - 改善成用 `t_threshold_moderate` = df from Welch–Satterthwaite equation 對應的 abs(t distribution 0.05 percentile) ,實測 df 遠遠大於 1,因此 default 10 非常寬鬆,然而 Trace - 17 依然經常不通過,可能是由於的沒有 crop 的緣故。 ``` double t_compute_df(t_ctx *ctx) { double var[2] = {0.0, 0.0}; var[0] = ctx->m2[0] / (ctx->n[0] - 1); var[1] = ctx->m2[1] / (ctx->n[1] - 1); double num = pow((var[0] / ctx->n[0] + var[1] / ctx->n[1]), 2); double den = pow(var[0],2) / (pow(ctx->n[0], 2) * (ctx->n[0] - 1)) + pow(var[1],2) / (pow(ctx->n[1], 2) * (ctx->n[1] - 1)); double df = num / den; return df; } ``` TODO: Cropped t test