# 2023q1 Homework1 (lab0) contributed by < `gaberplaysgame` > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 12 On-line CPU(s) list: 0-11 Vendor ID: GenuineIntel Model name: 11th Gen Intel(R) Core(TM) i5-11400 @ 2.60GHz CPU family: 6 Model: 167 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 1 CPU max MHz: 4400.0000 CPU min MHz: 800.0000 BogoMIPS: 5184.00 ``` :::warning 在終端機執行 `export LC_ALL=C`,再執行上述命令。`lscpu` 套件目前的中文翻譯品質不佳。 :notes: jserv ::: ## 開發過程 ### 嘗試改進 lab0-c :::danger 注意書寫規範,中英文間用一個半形空白字元區隔 :notes: jserv ::: #### q_new() 依據作業規範,`q_new()` 應配置可容納 `list_head` 的記憶體空間,並對節點進行設定,這部份`list.h`有`INIT_LIST_HEAD(head)`可以做到,故這裡就直接用內建函式處理。考慮到 malloc 有可能無法配置空間,所以用一個if處理空間分配失敗的狀況,若失敗則 head 為 NULL 。 ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (head) INIT_LIST_HEAD(head); return head; } ``` #### q_insert_head() 與下面的 insert_tail 是同樣道理,分了三次 if 來去排除掉可能 malloc 錯誤或者 head 為 NULL 的情況。 :::danger 注意書寫規範,中英文間用一個半形空白字元區隔 :notes: jserv ::: ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; //if head == NULL, return false element_t *new = malloc(sizeof(element_t)); if (!new) return false; //if malloc() failed size_t len_s = strlen(s) + 1; new->value = (char *) malloc(len_s * sizeof(char)); if (!new->value){ free(new); return false; //if malloc() failed, free new and return false } //copy string value memcpy(new->value, s, len_s); //utilize function of list.h list_add(&new->list, head); return true; } ``` #### q_insert_tail() ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; //if head == NULL, return false element_t *new = malloc(sizeof(element_t)); if (!new) return false; //if malloc() failed size_t len_s = strlen(s) + 1; new->value = (char *) malloc(len_s); if (!new->value){ free(new); return false; //if malloc() failed, free new and return false } //copy string value memcpy(new->value, s, len_s); //utilize function of list.h list_add_tail(&new->list, head); return true; } ``` #### q_size() ```c int q_size(struct list_head *head) { //if head is NULL or head->next = head, the queue has no element if (!head || list_empty(head)) return 0; int count = 0; struct list_head *iter; list_for_each(iter, head) count++; return count; } ``` #### q_free() 這邊利用內建的函式來做 free ,用 for 迴圈<s>遍歷</s> --- 整個鏈結串列,複雜度為 O(n)。 :::warning 注意 [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary) 並改進漢語表達。 :notes: jserv ::: ```c void q_free(struct list_head *l) { if (!l) return; // if head is NULL element_t *iter, *next; list_for_each_entry_safe (iter, next, l, list) { list_del(&iter->list); // delete node q_release_element(iter); // free element } free(l); } ``` #### q_remove_head() 與下方的`remove_tail`是一樣的,只是改`head->next`或`head->prev`。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) //there is no element in list return NULL; element_t *remove = list_entry(head->next, element_t, list); list_del(&remove->list); //an element in list is removed if (sp && bufsize) { //sp != NULL and bufsize != 0 memcpy(sp, remove->value, bufsize); sp[bufsize - 1] = '\0'; } return remove; } ``` #### q_remove_tail() ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) //there is no element in list return NULL; element_t *remove = list_entry(head->prev, element_t, list); list_del(&remove->list); //an element in list is removed if (sp && bufsize) { //sp != NULL and bufsize != 0 memcpy(sp, remove->value, bufsize); sp[bufsize - 1] = '\0'; } return remove; } ``` #### q_delete_mid() 利用 [Floyd Tortoise and Hare Algorithm](https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare) ,原理是給定兩個迭代節點分別跑一步與兩步,若兩個節點同時在除起點外同個位置時則代表鏈結串列有閉環。因為 linux 的鏈結串列是用循環雙向的方式做的,因此可以利用該演算法,在 Hare 第一次跑到鏈結串列的尾端時,Tortoise 會剛好在中間節點。 由於鏈結串列的長度會有分單雙數的情況,單數時 Hare 可以剛好跑到尾端,雙數則會多跑一<s>格</s> ?? 到 head 指標。由於 Hare 一次跑兩格,所以跑到尾端和跑到 Head 的事件不會同時發生,可以當作 while 判斷式的中止條件。 :::warning 注意量詞,翻閱辭典以理解「格」的使用時機。 :notes: jserv ::: ```c bool q_delete_mid(struct list_head *head) { // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/ if (!head || list_empty(head)) return false; struct list_head *tortoise = head->next, *hare = head->next; // given two node points to 0th node, using floyd's // tortoise and hare algorithm. while (hare != head && hare != head->prev) { // when hare reaches end of list or head, // tortoise will be in the middle of the list tortoise = tortoise->next; hare = hare->next->next; } element_t *element = list_entry(tortoise, element_t, list); list_del(&element->list); q_release_element(element); return true; } ``` #### q_delete_dup() 原本寫的程式碼:利用一個`flag`和`strcmp`進行兩個元素的比較,若`strcmp`出來0則`flag`為 true 並刪除`cur`的元素,下次若是`strcmp`非0,`flag`為 true 時亦會刪除元素。如此便有三點要處理: 1. 當`next == head`時,由於`head`沒有數值,不能進行`strcmp`,因此只能進到`flag`判斷。 2. 當`strcmp == 0`時,進行元素刪除。 3. 當`flag == true`時,進行元素刪除。 下面是第一版的程式碼(節錄): ```c element_t *cur, *next; bool flag = false; list_for_each_entry_safe (cur, next, head, list) { if (&next->head != head) { int cmp = strcmp(cur->value, next->value); if (!cmp) { flag = true; list_del(&cur->list); q_release_element(cur); } } else if (flag) { flag = false; list_del(&cur->list); q_release_element(cur); } } ``` 可以看到上面程式碼用了很多 if,尤其`list_del`的部份有重複,自己都認為沒有品味,因此花了一些時間在改進程式碼方面。 首先嘗試了把上面提到的重複部份整理一頓,先忽略了`next->list`可能為`head`的情形,利用`cmp`與`flag`兩個數值相反的特性將區塊合併。 1. 當 cmp 為0時,`flag`會被設為 true ,這代表如果下一圈 cmp 不是0時仍可以刪除 cur 的元素 2. 沿述,當 cmp 不是0時,若 flag 為 true ,則刪除元素;若 flag 為 false ,則代表沒有需要刪除元素,可以進到下一圈。 排除掉未刪除元素的可能性後,可以看到`flag = !cmp`的性質,且可以放在程式最後面執行,因此只要把 cmp 非0,flag 為 false 的情況先擋掉,就可以把兩個區塊合併。 ```c if (cmp && !flag) continue; list_del(&cur->list); q_release_element(cur); flag = !cmp; ``` 再來就是解決`next->list`可能為 head 的情形了。若此狀況發生,cmp 沒辦法決定值,因此給這段加上了一個三元判斷式判定若該狀況發生,指定 cmp 為 true 。如此可以保證`if (cmp && !flag)`能被觸發,再來若 flag 為 true 可以保證元素能被刪除。 改進過後的程式碼: :::danger 不用張貼完整程式碼,只要列出差異的部分,善用 `diff` :notes: jserv ::: <s> bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head) return false; // list is NULL element_t *cur, *next; bool flag = false; list_for_each_entry_safe (cur, next, head, list) { int cmp = &next->list != head ? strcmp(cur->value, next->value) : true; // no comparison if next is head, // comparison gives 0 or nonzero value if (cmp && !flag) continue; list_del(&cur->list); q_release_element(cur); flag = !cmp; } return true; } </s> ~~目前只有這個函式還沒有去做正確性的測試,等sort與reverse等做完後才能進一步確認是否運行順利。~~ 經過測試後確定可以順利通過<s>autojudge測資。</s> :::danger 注意看程式碼,`lab0-c` 使用 `autograde` 一詞,「測試案例」不該簡稱「測資」。 :notes: jserv ::: #### q_swap() 因為下方的 reverse 函式會用到 swap <s>的技術</s>,所以將實際將兩個節點進行交換的步驟給獨立出來。 :::warning 改進你的漢語表達。 :notes: jserv ::: ```c void swap(struct list_head *n1, struct list_head *n2) { struct list_head *tmp = n1->next; n1->next = n2->next; n2->next->prev = n1; n2->next = tmp; n2->next->prev = n2; tmp = n2->prev; n2->prev = n1->prev; n1->prev->next = n2; n1->prev = tmp; n1->prev->next = n1; } void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *n1 = head->next, *n2 = n1->next; while (n1 != head && n2 != head) { swap(n1, n2); n1 = n1->next; n2 = n1->next; } } ``` #### q_reverse() 本意上用到的是頭尾交換的想法,由於是循環陣列容易找到鏈結串列尾,故用這個方法實作。 ```c void reverse(struct list_head *start, struct list_head *end) { while (start != end && start != end->next) { swap(start, end); struct list_head *tmp = end->next; end = start->prev; start = tmp; } } void q_reverse(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; reverse(head->next, head->prev); } ``` #### q_reverseK() 方法目前只有想到類似硬破法,將佇列拆為 k 個(或更少)一組,然後分別進行 reverse 操作。 ```c void q_reverseK(struct list_head *head, int k) { // https://leetcode.com/problems/reverse-nodes-in-k-group/ if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *start = head->next, *end = start; while (end != head) { int count = 1; while (count != k && end->next != head) { end = end->next; count++; } struct list_head *tmp = end->next; reverse(start, end); start = tmp, end = start; } } ``` #### q_sort() 這裡的 sort 是使用 mergesort 來實作,方法有參考[你所不知道的C語言:Merge Sort的實作](https://hackmd.io/@sysprog/c-linked-list#Merge-Sort-%E7%9A%84%E5%AF%A6%E4%BD%9C)與間接指標的應用,但有用到將循環鏈結串列斷開成一般鏈結串列的操作,還沒有想到有沒有可以保持循環鏈結串列的實作方式。 ```c struct list_head *merge(struct list_head *l1, struct list_head *l2) { struct list_head *new_head = NULL, **indirect = &new_head; while (l1 && l2) { if (strcmp(list_entry(l1, element_t, list)->value, list_entry(l2, element_t, list)->value) < 0) { *indirect = l1; l1 = l1->next; } else { *indirect = l2; l2 = l2->next; } indirect = &(*indirect)->next; } *indirect = (struct list_head *) ((uintptr_t) l1 | (uintptr_t) l2); return new_head; } struct list_head *mergeSort(struct list_head *head, struct list_head *tail) { if (head == tail) return head; struct list_head *start = head, *end = tail; while (start != end && start != end->prev) { start = start->next; end = end->prev; } if (start == end) end = end->next; start->next = NULL; end->prev = NULL; struct list_head *l1 = mergeSort(head, start), *l2 = mergeSort(end, tail); return merge(l1, l2); } void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) // if head == NULL or empty or one element return; struct list_head *start = head->next, *end = head->prev; // Break the circular linked list start->prev = NULL; end->next = NULL; // Mergesort struct list_head *new_head = mergeSort(start, end); start = new_head; end = start->next; // Rebuild circular linked list for (; end != NULL; start = start->next, end = end->next) { end->prev = start; } start->next = head; new_head->prev = head; head->next = new_head; head->prev = start; } ``` #### q_merge() 有用到前面 merge 的想法,並且用 Devide & Conquer 的方式處理合併,使時間複雜到可以降到 O(NlogK)。 :::danger 注意書寫規範,中英文間用一個半形空白字元區隔 :notes: jserv ::: ```c int q_merge(struct list_head *head) { // https://leetcode.com/problems/merge-k-sorted-lists/ int interval = 1, list_amount = 0; queue_contex_t *contex = NULL; list_for_each_entry (contex, head, chain) { list_amount++; contex->q->prev->next = NULL; } struct list_head *l1 = NULL, *l2 = NULL; while (interval < list_amount) { for (int i = 0; i < list_amount - interval; i += interval * 2) { list_for_each_entry (contex, head, chain) { if (contex->id == i) l1 = contex->q; else if (contex->id == i + interval) l2 = contex->q; } l1->next = merge(l1->next, l2->next); l2->prev = l2; l2->next = l2; } interval *= 2; } struct list_head *start, *end; head = l1; start = head->next; end = start->next; // Rebuild circular linked list for (; end != NULL; start = start->next, end = end->next) { end->prev = start; } start->next = head; head->next->prev = head; head->prev = start; return q_size(head); } ``` #### q_descend() 這函式用了兩個指標 `left` 與 `right` 來處理數值比較,當 right 指到 head 時結束操作並回傳佇列大小。其餘實作方式基本可以參考 remove 系列。 :::danger 注意書寫規範,中英文間用一個半形空白字元區隔 :notes: jserv ::: ```c int q_descend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || list_empty(head)) // if head == NULL or empty or one element return 0; if (list_is_singular(head)) return 1; int count = 1; struct list_head *left = head->next, *right = left->next; while (right != head) { element_t *left_entry = list_entry(left, element_t, list); if (strcmp(left_entry->value, list_entry(right, element_t, list)->value) < 0) { left = left->next; list_del(&left_entry->list); q_release_element(left_entry); count--; } else { right = right->next; count++; } } return count; } ``` :::danger 注意書寫規範,中英文間用一個半形空白字元區隔 :notes: jserv ::: ### 引進 lib/list_sort.c 引入`linux/list_sort.[c, h]` 並做了些特別的修改,主要是為了繞開不能修改 `queue.h` 的限制,所以將 `list_sort.[c, h]` 作為新的引入檔,以 q_list_sort 作為在 `qtest.c` 的呼叫函式。 ```c ADD_COMMAND(lsort, "Sort queue in ascending order in linux kernel way", ""); ``` cmp 函式以此方式實作,由於不太確定 priv 傳入值的用處,加上可以為 NULL 值,cmp 函式就將此刪除: ```c __attribute__((nonnull(1,2))) int q_list_cmp(const struct list_head *a, const struct list_head *b) { element_t *element_a = list_entry(a, element_t, list); element_t *element_b = list_entry(b, element_t, list); int res = strcmp(element_a->value, element_b->value); return res; } ``` 這邊以原本進行 make 是沒有問題的,但在更新 GitHub 時有遇到 cppcheck 的 "Null pointer dereference" ,指向 list_entry 的用法。 ```shell list_sort.c:24:28: error: Null pointer dereference: (struct element_t*)0 [nullPointer] element_t *element_a = list_entry(a, element_t, list); ``` 起初不太知道這個錯誤應該如何解決,以為是程式碼本身的錯誤,在這個程式碼上面花了一點時間。觀摩 [jasperlin1996](https://hackmd.io/@jasperlin1996/linux2022-lab0) 後發現說可能是 cppcheck 的問題。由於學長已經做過了 cppcheck 1.9~2.7 的測試,自己把最新版的 2.10 安裝之後這個錯誤仍存在,最後只好使用 cppcheck suppression 的方式解決,即使用 `cppcheck-suppress nullPointer`繞過。這邊引用學長對於這個錯誤的敘述以及當屆[討論](https://www.facebook.com/groups/system.software2022/permalink/1983461425168591/): > 這邊是因為 list.h 中 container_of 的實作中,有一行是 __typeof__(((type *) 0)->member) 而 (type *) 0 的確是 null pointer。不過這邊的用法是為了取得 member 的 type,且這個用法是符合 ANSI C 的標準的,詳細的說明可以看這篇:https://stackoverflow.com/questions/13723422/why-this-0-in-type0-member-in-c 以及規格書的第 6.3.2 章關於 pointer 的說明:http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf 另外直接引入 `list_sort.[c, h]` 會遇到 [likely & unlikely](https://meetonfriday.com/posts/cecba4ef/) 巨集的問題,雖然這段編碼只是為了優化 gcc ,但為了不過度影響程式效能自己還是從`compiler.h` 抓入相關部份並放入`list_sort.c`。 :::danger 注意書寫規範,中英文間用一個半形空白字元區隔。 改進你的漢語表達。 :notes: jserv ::: ### 對自撰 sort 與 list_sort 進行效能分析 利用 [Perf](https://en.wikipedia.org/wiki/Perf_(Linux)) 進行效能分析,執行 10 次並取平均,測試程式如下: ``` new ih RAND <number> time sort/lsort time free ``` <s>Perf 似乎無法支援筆電 CPU ,導致部份如 instruction, cycles 的測試無法進行,這邊先貼上部份的測試結果,之後預計會換回桌機進行測試。</s>筆電使用 i7-7700 CPU 無法進行有關 instruction 與 cycles 等的測試,換回桌機使用 i5-11400 已解決問題,故以下會使用桌機數據重新進行紀錄。 :::warning 清楚列舉硬體型號和相關系統資訊,這樣他人才能協助排除問題。 :notes: jserv ::: - 註:q 為 `q_sort` ,l 為 `q_list_sort` | number | instruction | time | cycles | page fault | branch | | - | - | - | - | - | - | | **1024_q** | 5,934,943 | 0.91 ms | 3,884,975 | 112 | 911,195 | | **2048_q** | 10,164,886 | 1.48 ms | 6,164,264 | 150 | 1,509,771 | | **4096_q** | 18,852,459 | 4.76 ms | 19,931,933 | 220 | 2,755,409 | | **10240_q** | 45,017,257 | 6.47 ms | 27,146,322 | 438 | 6,533,543 | | **102400_q** | 448,797,359 | 100.75 ms | 425,198,490 | 3677 | 65,891,213 | | **409600_q** | 1,824,311,621 | 649.45 ms | 2,562,835,035 | 14477 | 270,998,617 | | **1024_l** | 5,504,024 | 1.76 ms | 3,135,236 | 112 | 797,843 | | **2048_l** | 9,005,109 | 2.62 ms | 10,824,548 | 148 | 1,229,047 | | **4096_l** | 16,449,283 | 6.55 ms | 12,042,236 | 221 | 2,164,575 | | **10240_l** | 38,676,693 | 18.08 ms | 18,684,006 | 437 | 4,955,814 | | **102400_l** | 372,969,154 | 46.82 ms | 197,921,647 | 3677 | 46,956,545 | | **409600_l** | 1,488,046,316 | 177.62 ms | 725,989,221 | 14477 | 187,069,862 | 這邊取的是平均值的數值,實際的時間差在正負 10ms 左右。可以看到在 instruction, cycle, branch 上 list sort 都比自己寫的 sort 還有好。由於時間差落在正負 10ms ,因此在數量級小的情況下兩個演算法難分軒輊,但若是數量級大則可以明顯看到 list sort 的表現比自己寫的還要消耗更少時間。綜合以上數據可以發現 list sort 在各方面都優於自己實作的 merge sort 。 ### 利用 Valgrind 排除 qtest 實作的記憶體錯誤 執行`make valgrind`可以得到以下結果(節錄): ``` --- trace-01-ops 0/5 --- trace-02-ops 0/6 --- trace-03-ops 0/6 --- trace-04-ops 0/6 --- trace-05-ops 0/6 --- trace-06-ops 0/6 --- trace-07-string 0/6 --- trace-08-robust 6/6 --- trace-09-robust 0/6 --- trace-10-robust 6/6 --- trace-11-malloc 6/6 --- trace-12-malloc 6/6 --- trace-13-malloc 0/6 --- trace-14-perf 6/6 --- trace-15-perf 6/6 --- trace-16-perf 6/6 ``` 這邊取`trace-01-ops`的問題進行細部觀察,可以看到大致由三種問題組成: :::spoiler ``` gaberil0903@host:~/linux2023/lab0-c$ valgrind ./qtest -f traces/trace-01-ops.cmd # Test of insert_head and remove_head l = [] l = [gerbil] l = [bear gerbil] l = [dolphin bear gerbil] ==12508== Invalid read of size 8 ==12508== at 0x4852AF8: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f3d8 is 8 bytes before a block of size 2,049 alloc'd ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10C0ED: do_remove (qtest.c:371) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== ==12508== Invalid read of size 8 ==12508== at 0x4852B00: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f3d0 is 16 bytes before a block of size 2,049 alloc'd ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10C0ED: do_remove (qtest.c:371) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== ==12508== Invalid read of size 8 ==12508== at 0x4852AE4: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f3c8 is 24 bytes before a block of size 2,049 alloc'd ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10C0ED: do_remove (qtest.c:371) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== ==12508== Invalid read of size 8 ==12508== at 0x4852AF0: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f3c0 is 32 bytes before a block of size 2,064 in arena "client" ==12508== Removed dolphin from queue l = [bear gerbil] ==12508== Invalid read of size 8 ==12508== at 0x4852947: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f030 is 3 bytes after a block of size 45 alloc'd ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F29C: test_malloc (harness.c:133) ==12508== by 0x10F765: q_insert_head (queue.c:48) ==12508== by 0x10CAA5: do_ih (qtest.c:224) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== ==12508== Invalid read of size 8 ==12508== at 0x485294F: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f038 is 11 bytes after a block of size 45 alloc'd ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F29C: test_malloc (harness.c:133) ==12508== by 0x10F765: q_insert_head (queue.c:48) ==12508== by 0x10CAA5: do_ih (qtest.c:224) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== ==12508== Invalid read of size 8 ==12508== at 0x4852934: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f040 is 19 bytes after a block of size 45 alloc'd ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F29C: test_malloc (harness.c:133) ==12508== by 0x10F765: q_insert_head (queue.c:48) ==12508== by 0x10CAA5: do_ih (qtest.c:224) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== ==12508== Invalid read of size 8 ==12508== at 0x485293F: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f048 is 24 bytes after a block of size 48 in arena "client" ==12508== ==12508== Invalid read of size 1 ==12508== at 0x4852A10: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F89C: memcpy (string_fortified.h:29) ==12508== by 0x10F89C: q_remove_head (queue.c:96) ==12508== by 0x10C333: do_remove (qtest.c:404) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Address 0x4b7f420 is 64 bytes inside a block of size 2,049 free'd ==12508== at 0x484B27F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10C3B3: do_remove (qtest.c:454) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Block was alloc'd at ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10C0ED: do_remove (qtest.c:371) ==12508== by 0x10C4DB: do_rh (qtest.c:461) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== Removed bear from queue l = [gerbil] Removed gerbil from queue l = [] ==12508== 32 bytes in 1 blocks are still reachable in loss record 1 of 2 ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10CD3D: do_new (qtest.c:146) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== ==12508== 56 bytes in 1 blocks are still reachable in loss record 2 of 2 ==12508== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12508== by 0x10F29C: test_malloc (harness.c:133) ==12508== by 0x10F6A1: q_new (queue.c:20) ==12508== by 0x10CD76: do_new (qtest.c:150) ==12508== by 0x10DF86: interpret_cmda (console.c:181) ==12508== by 0x10E53B: interpret_cmd (console.c:201) ==12508== by 0x10E93C: cmd_select (console.c:610) ==12508== by 0x10F228: run_console (console.c:705) ==12508== by 0x10D378: main (qtest.c:1270) ==12508== ``` ::: ``` ==12311== Invalid read of size 8 ==12311== at 0x4852AF8: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12311== by 0x10F89C: memcpy (string_fortified.h:29) ==12311== by 0x10F89C: q_remove_head (queue.c:96) ==12311== by 0x10C333: do_remove (qtest.c:404) ==12311== by 0x10C4DB: do_rh (qtest.c:461) ==12311== by 0x10DF86: interpret_cmda (console.c:181) ==12311== by 0x10E53B: interpret_cmd (console.c:201) ==12311== by 0x10E93C: cmd_select (console.c:610) ==12311== by 0x10F228: run_console (console.c:705) ==12311== by 0x10D378: main (qtest.c:1270) ==12311== Address 0x4b7f3d8 is 8 bytes before a block of size 2,049 alloc'd ==12311== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12311== by 0x10C0ED: do_remove (qtest.c:371) ==12311== by 0x10C4DB: do_rh (qtest.c:461) ==12311== by 0x10DF86: interpret_cmda (console.c:181) ==12311== by 0x10E53B: interpret_cmd (console.c:201) ==12311== by 0x10E93C: cmd_select (console.c:610) ==12311== by 0x10F228: run_console (console.c:705) ==12311== by 0x10D378: main (qtest.c:1270) ==12311== 32 bytes in 1 blocks are still reachable in loss record 1 of 2 ==12311== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==12311== by 0x10CD3D: do_new (qtest.c:146) ==12311== by 0x10DF86: interpret_cmda (console.c:181) ==12311== by 0x10E53B: interpret_cmd (console.c:201) ==12311== by 0x10E93C: cmd_select (console.c:610) ==12311== by 0x10F228: run_console (console.c:705) ==12311== by 0x10D378: main (qtest.c:1270) ``` 第一、二個問題源自 bufsize 大小為 1025,然而測試字串 dolphin 的大小長度只有 8 ,所以在 `memcpy(sp, remove->value, bufsize);` 中會造成記憶體空間錯誤。所以只要將 `bufsize` 更改為`bufsize > strlen(remove->value) + 1 ? strlen(remove->value) : bufsize` 就好。 ```c if (sp && bufsize) { // sp != NULL and bufsize != 0 memcpy(sp, remove->value, bufsize > strlen(remove->value) + 1 ? strlen(remove->value) + 1 : bufsize); sp[bufsize - 1] = '\0'; } ``` 根據執行順序,判斷第三個訊息來自於把佇列清空後沒有正常釋放記憶體所導致,根據`q_quit`函式可以看到有個不正常的`return true`在程式第3行被執行,導致後續沒有進行 free 佇列。將不正常的程式碼移除後即可解決這個錯誤。 :::spoiler ```c= static bool q_quit(int argc, char *argv[]) { return true; report(3, "Freeing queue"); if (current && current->size > BIG_LIST_SIZE) set_cautious_mode(false); if (exception_setup(true)) { struct list_head *cur = chain.head.next; while (chain.size > 0) { queue_contex_t *qctx, *tmp; tmp = qctx = list_entry(cur, queue_contex_t, chain); cur = cur->next; q_free(qctx->q); free(tmp); chain.size--; } } exception_cancel(); set_cautious_mode(true); size_t bcnt = allocation_check(); if (bcnt > 0) { report(1, "ERROR: Freed queue, but %lu blocks are still allocated", bcnt); return false; } return true; } ``` ::: 再次執行`make valgrind`可以看到問題皆以修復。 ### 利用 massif 視覺化 simulation 這邊設計一個實驗組與一個對照組,採用的是前章節出問題的 removehead <s>指令</s> ---,可以得出兩種結果,前者為<s>優化前,後者為優化後</s>: :::warning 去查詢 optimize 的意思,留意 optimal 的定義。不要濫用詞彙。 :notes: jserv ::: 利用`valgrind --tool=massif ./qtest -f traces/sim-rh.cmd`與 Valgrind User Manual 9.3 的 massif-visualizer 來顯示結果: ```shell massif-visualizer massif.out.14944 ``` ![](https://i.imgur.com/qR3GWPO.png) ```shell massif-visualizer massif.out.14972 ``` ![](https://i.imgur.com/7fV1SaD.png) 可以看到在針對 q_quit 函式進行修復後,有改善記憶體的釋出,但程式執行最後仍未完全釋放記憶體。 ### TODO: 利用 Address Sanitizer 修正 qtest 執行過程中的錯誤 使用 Address Sanitizer 後進行 `make test` 後發現編號第 14 的測試案例因超時無法順利通過,而編號第 17 的測試案例中,常數時間測試在 remove_head/tail 時為常數時間時非常數時間。<s>比對過其他人程式碼後還沒有找到問題在哪裡,預計會再進行深入研究。</s> :::warning 不要說「其他人」,明確列出你參照的帳號名稱,還有你如何實驗。 :notes: jserv ::: ### 在 q_test 添加<s>指令</s> 命令 shuffle :::warning command 的翻譯是「命令」,instruction 的翻譯是「指令」,二者語境落差大。 :notes: jserv ::: 參考 [Fisher-Yates Shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法的<s>偽代碼</s> 虛擬碼進行實作。 :::warning 台灣的慣用翻譯風格中,少用「偽」,反過來說,中共的刊物就常見。 :notes: jserv ::: ```c void q_shuffle(struct list_head *head, size_t len) { if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *tail = head->prev; for (int i = len - 1; i > 0; i--) { struct list_head *cur = head; int j = rand() % (i + 1); for (int tmp = 0; tmp <= j; tmp++) cur = cur->next; shuffle_swap(cur, tail); tail = cur->prev; } } ``` 實作完成後利用範例的[測試程式](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-d#%E6%B8%AC%E8%A9%A6%E7%A8%8B%E5%BC%8F)進行卡方($X^2$)的計算,得出約等於 22.9521 。$[1, 2, 3, 4]$ 的排序中共有 24 種排列組合,故自由度(P)為 23 。根據卡方分布圖查詢可知 P-value 介於 0.25~0.5 之間,大於常見的顯著水準 alpha (0.05) 。統計結果鑑定不拒絕虛無假說。 ![](https://i.imgur.com/Rn4e2Zn.png) :::spoiler ``` Expectation: 41666 Observation: {'1234': 41341, '1243': 41787, '1324': 41601, '1342': 41794, '1423': 41864, '1432': 41914, '2134': 41506, '2143': 41670, '2314': 41623, '2341': 41579, '2413': 41635, '2431': 41982, '3124': 41732, '3142': 41633, '3214': 41552, '3241': 41843, '3412': 41806, '3421': 41728, '4123': 41406, '4132': 41222, '4213': 41999, '4231': 41325, '4312': 41783, '4321': 41675} chi square sum: 22.952143234291746 ``` :::