# 2025q1 Homework1 (lab0) contributed by < `TING0419` > ### Reviewed by `nyraa` > 注意科技詞彙翻譯 * 鍊表->鏈結串列 * 創建->建立 > 注意排版上英文與中文之間需要有一個空白 ### Reviewed by `liangchingyun` > 需要進行 rebase ## 開發環境 ``` $ uname -a Linux ting-ASUS-TUF-Gaming-A15-FA506IU-FA506IU 6.8.0-52-generic #53~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Jan 15 19:18:46 UTC 2 x86_64 x86_64 x86_64 GNU/Linux $ ldd --version ldd (Ubuntu GLIBC 2.35-0ubuntu3.9) 2.35 $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 44 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 16 On-line CPU(s) list: 0-15 Vendor ID: AuthenticAMD Model name: AMD Ryzen 7 4800H with Radeon Graphics CPU family: 23 Model: 96 Thread(s) per core: 2 Core(s) per socket: 8 Socket(s): 1 Stepping: 1 Frequency boost: enabled CPU max MHz: 2900.0000 CPU min MHz: 1400.0000 BogoMIPS: 5789.20 ``` ## 開發指定的佇列操作 ### `q_new` 原版本: > [Commit b453915](https://github.com/sysprog21/lab0-c/commit/b4539159f768755d5c31e7a2ac9b7e842085ef45) 使用記憶體分配來建立一個新的雙向鏈結的`head`。 該函數分配記憶體並初始化鏈表,將 `head->next` 和 `head->prev` 設置為指向自身。 如果記憶體分配失敗,返回 `NULL` 以防止空指標存取。 ```c struct list_head *q_new() { struct list_head *head = (struct list_head *) malloc(sizeof(struct list_head)); if (!head) return NULL; head->next = head; head->prev = head; return head; } ``` 改進: >[Commit e17b5df](https://github.com/sysprog21/lab0-c/commit/e17b5df9b256cc7c143b37b05a4bc9c2cc24cc4c) 使用 [list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h)中提供的API改寫,使程式更為簡潔。 ```c struct list_head *q_new() { struct list_head *head = (struct list_head *) malloc(sizeof(struct list_head)); if (!head) return NULL; INIT_LIST_HEAD(head); return head; } ``` ### `q_free` >[Commit 52fd5d0](https://github.com/sysprog21/lab0-c/commit/52fd5d075f9cd59082b4e8b3b4052439532351a2) 在佇列的實現中,鏈結串列是通過在 `element_t` 中定義的 `list_head` 結構來連接的。因此,可以使用在 [list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h)中定義的 `list_for_each_entry_safe` 遍歷佇列中的所有 element_t,並獲取每個 `element_t` 的位址以釋放其記憶體。此外,由於 `element_t `中的字串所指向的記憶體區塊也需要釋放,最後,還需要釋放 `head` 的 `list_head` 所指向的記憶體區塊。 ```c void q_free(struct list_head *head) { if (!head) return; if (list_empty(head)) { free(head); return; } struct list_head *node, *safe; list_for_each_safe (node, safe, head) { element_t *current = list_entry(node, element_t, list); q_release_element(current); } free(head); } ``` ### `q_insert_head` 原版本: > [Commit a5405a3](https://github.com/sysprog21/lab0-c/commit/a5405a3cbd9782371bb6a600581997b950a02b81) 使用 `list_add` 實現 `q_insert_head()`,將新節點插入佇列的 `head` 。確保當 `element_t` 的值指向字串時,使用 `strdup()` 來分配並複製字串,以避免在測試過程中發生錯誤。 ```c bool q_insert_head(struct list_head *head, char *s) { element_t *new_node = (element_t *) malloc(sizeof(element_t)); if (!new_node) { return false; } new_node->value = strdup(s); list_add(&new_node->list, head); return true; } ``` 改進: >[Commit 4af6ea3](https://github.com/sysprog21/lab0-c/commit/e79a800a7b0e6e57230efe06d2db7273fab7fb30) 這段改進優化了記憶體管理和錯誤處理。首先,新增了對 `head` 和 `s` 是否為空指標的檢查,防止空指標錯誤。接著,將字串的記憶體分配方式改為手動分配並使用 `strncpy` 進行字串複製,以避免緩衝區溢出。最後,對記憶體分配失敗的情況做了處理,確保在失敗時釋放已分配的記憶體,防止記憶體洩漏。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head || !s) return false; element_t *element = malloc(sizeof(element_t)); if (!element) return false; int s_len = strlen(s); element->value = (char *) malloc((s_len + 1) * sizeof(char)); if (!element->value) { free(element); return false; } strncpy(element->value, s, (s_len + 1)); list_add(&element->list, head); return true; } ``` ### `q_insert_tail` 原版本: > [Commit 5c91773](https://github.com/sysprog21/lab0-c/compare/master...TING0419:lab0-c:master) 實現 `q_insert_tail` 函數,將元素插入佇列的尾部。 為新節點分配記憶體並使用 `strdup` 函數複製字串。 使用 [list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 的API `list_add_tail()` 將新節點插入佇列的末尾。 ```c bool q_insert_tail(struct list_head *head, char *s) { element_t *new_node = (element_t *) malloc(sizeof(element_t)); if (!new_node) { return false; } list_add_tail(&new_node->list, head); new_node->value = strdup(s); return true; } ``` 改進: >[Commit b81c0ad](https://github.com/sysprog21/lab0-c/commit/b81c0ad42e8a3c884e62ba2e154d093cb5bae53e) 通過利用雙向鏈結串列的特性使用 `q_insert_head`。將新節點插入到 `head->prev` 位置,等同於將節點添加至尾部。 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head || !s) { return false; } return q_insert_head(head->prev, s); } ``` ### `q_remove_head` >[Commit 4cc1204](https://github.com/sysprog21/lab0-c/commit/4cc1204e9cdabeff015ac0e0866db0f80a9b63f4) 實作 `q_remove_head` 以從佇列的頭部移除元素。 如果提供了有效的緩衝區 `buffer`,則將該元素的字串值複製到該緩衝區中,並確保不會超過緩衝區的大小。 使用 `memcpy` 進行字串複製,並正確處理緩衝區溢位,必要時會對字串進行截斷。 移除該元素並將其返回。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *tmp = list_entry(head->next, element_t, list); if (sp && bufsize > 0) { size_t len = strlen(tmp->value); if (len < bufsize) { memcpy(sp, tmp->value, len + 1); } else { memcpy(sp, tmp->value, bufsize - 1); sp[bufsize - 1] = '\0'; } } list_del(&tmp->list); return tmp; } ``` ### `q_remove_tail` >[Commit 45413a7](https://github.com/sysprog21/lab0-c/commit/45413a7c191700bbf5f48095dd4b731f28544b8d) 將 `queue` 中最後一個元素移除 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) { return NULL; } return q_remove_head(head->prev->prev, sp, bufsize); } ``` ### `q_size` > [Commit 35bb7b7](https://github.com/sysprog21/lab0-c/commit/35bb7b78124e1afda5410a65082c8599063d91db) 使用 [list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 中的 `list_for_each` 來計算佇列的長度。 如果佇列為空,返回 0;否則,返回元素的數量。 ```c int q_size(struct list_head *head) { if (!head) return 0; int len = 0; struct list_head *li; list_for_each (li, head) len++; return len; } ``` ### `q_delete_mid` > [Commit 1b2ad88](https://github.com/sysprog21/lab0-c/commit/1b2ad8830f5647fc6366713ad72696ae02779cb7) 使用兩個指標來刪除佇列中的中間節點。 一個指標每次移動一步,另一個指標則移動兩步。 當較快的指標再次回到佇列的頭部時,較慢的指標將會指向中間的節點。將這個中間節點從佇列中移除。 ```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 *slow = head->next, *fast = head->next; while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } list_del(slow); element_t *element = list_entry(slow, element_t, list); q_release_element(element); return true; } ``` ### `q_delete_dup` >[Commit dbd101d](https://github.com/sysprog21/lab0-c/commit/dbd101d95a7a330a32b9356f4083771eecad8d0a) 移除佇列中所有具有重複字串值的節點。 使用 `list_for_each_entry_safe` 來遍歷佇列。 如果連續的節點具有相同的值,刪除第一個節點並追蹤重複的尾端。如果在重複節點之後出現唯一值,則也移除最後一個重複節點。 使用`list_del` 來解除節點鏈結,並使用 `q_release_element` 來釋放記憶體。 處理邊界情況,例如佇列為空或僅有一個元素的情況。 ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head || list_empty(head) || list_is_singular(head)) return false; element_t *node = NULL, *safe = NULL, *dup_tail = NULL; list_for_each_entry_safe (node, safe, head, list) { if (&safe->list != head && !strcmp(node->value, safe->value)) { list_del(&node->list); q_release_element(node); dup_tail = safe; } else { if (dup_tail) { list_del(&dup_tail->list); q_release_element(dup_tail); } dup_tail = NULL; } } return true; } ``` ### `q_swap` >[Commit 1b3ebb9](https://github.com/sysprog21/lab0-c/commit/1b3ebb975d95d4e57789236d353f93d3cf038ee5) 使用 `list_move` 來交換佇列中每兩個相鄰的節點。 如果佇列為空或僅包含一個元素,則不進行交換。 使用兩個指標遍歷佇列: 將 `first` 指向第一個節點,`second` 指向下一個節點。 將 `second` 移動到 `first` 之前,然後更新指標以繼續交換下一對節點。 ```c 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 *first = head->next, *second = head->next->next; while (second != head && first != head) { list_move(second, first->prev); first = first->next; second = first->next; } } ``` ### `q_reverse` >[Commit cd3fd5a](https://github.com/sysprog21/lab0-c/commit/cd3fd5abcc199c851eef9dac70ed9342f741a46f) 使用 `list_move` 來反轉佇列中元素的順序。 如果佇列為空或僅包含一個元素,則不進行任何變更。使用 `list_for_each_safe` 遍歷佇列, 將每個節點移動到佇列的頭部,以達成反轉效果。 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) { return; } struct list_head *node = NULL, *safe = NULL; list_for_each_safe (node, safe, head) { list_move(node, head); } } ``` ### `q_reverseK` >[Commit 24d364c](https://chatgpt.com/g/g-p-67bf03cb29b08191a64a5a418eb6ad31-linux-kermel/c/67bf03d1-db30-8012-9c43-8f15e6e2089d) 使用 `list_move` 來將佇列中的節點按每組 k 個進行反轉。 如果 k 為 1 或者佇列長度小於 k,則不進行任何變更。遍歷佇列,每次移動 k 個節點來進行反轉。 每次反轉完一組後,更新下一組的參考點。當剩餘的節點少於 k 個時,過程停止。 ```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) || k == 1) return; int len = q_size(head), cnt = 0; struct list_head *node = NULL, *safe = NULL, *reverse_head = head; if (len < k) return; list_for_each_safe (node, safe, head) { list_move(node, reverse_head); if (++cnt == k) { if ((len -= k) < k) return; cnt = 0; reverse_head = safe->prev; } } } ``` ### `q_sort` >[Commit 70d5497](https://github.com/sysprog21/lab0-c/commit/70d54976dbca94e72b591ad037ee1c01f57cd97d) 使用合併排序演算法來對佇列進行排序。 該演算法首先將圓形雙向鏈結串列拆分成單向鏈結串列,然後使用輔助函式(`merge_sort_list` 和 `merge_sorted_lists`)遞迴進行排序,最後重建雙向鏈結串列的指標以恢復圓形結構。 此實作支援根據 `descend` 標誌進行升序或降序排序。 ```c void q_sort(struct list_head *head, bool descend) { if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *data_head = head->next, *node = NULL, *safe = NULL; head->prev->next = NULL; head->next = merge_sort_list(data_head, descend); for (node = head, safe = head->next; safe->next; node = safe, safe = node->next) { safe->prev = node; } safe->next = head; head->prev = safe; } ``` 此函式 `merge_sorted_lists` 負責以遞迴方式對單向鏈結串列進行合併排序。 採用快慢指標切割串列為兩半,遞迴排序後再透過 `merge_sorted_lists` 合併結果。排序方向由 descend 參數控制。 ```c static struct list_head *merge_sort_list(struct list_head *head, bool descend) { if (!head || !head->next) return head; struct list_head *slow = head, *fast = head->next; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } struct list_head *second = slow->next; slow->next = NULL; head = merge_sort_list(head, descend); second = merge_sort_list(second, descend); return merge_sorted_lists(head, second, descend); } ``` 此函式 `merge_sorted_lists` 會合併兩個已排序的單向鏈結串列。 使用 `dummy` 節點與尾端指標 `tail`,以尾插法串接節點,並依 `descend` 參數決定是否採用升冪或降冪排序。此函式是整個 `Merge Sort`合併階段的核心。 ```c static struct list_head *merge_sorted_lists(struct list_head *left, struct list_head *right, bool descend) { LIST_HEAD(dummy); struct list_head *tail = &dummy; while (left && right) { const char *lval = list_entry(left, element_t, list)->value; const char *rval = list_entry(right, element_t, list)->value; if ((!descend && strcmp(lval, rval) <= 0) || (descend && strcmp(lval, rval) >= 0)) { tail->next = left; left = left->next; } else { tail->next = right; right = right->next; } tail = tail->next; } tail->next = left ? left : right; return dummy.next; } ``` ### `q_ascend & q_descend` >[Commit 0aa4a3b](https://github.com/sysprog21/lab0-c/commit/0aa4a3b6c3a34aada690434c5cd3b46f5dd087a6) 由於這兩個函式邏輯高度相似,因此引入共用的輔助函式 `purge`,透過布林參數 `descend` 區分升冪與降冪兩種情境。 刪除判斷依賴當前節點後方的元素,因此實作了反向的 `list_for_each_safe`,並在遍歷過程中追蹤目前的「極值」(最大或最小)作為基準,判斷每個節點是否應被移除。 儘管函式的主要目標為刪除節點,但在測試期間發現若未釋放記憶體,會導致錯誤發生。因此在移除節點的同時,明確執行記憶體釋放以避免記憶體洩漏。 ```c int purge(struct list_head *head, bool descend) { if (!head || list_empty(head)) return 0; if (list_is_singular(head)) return 1; int cnt = 1; struct list_head *node = NULL, *safe = NULL, *peak = head->prev; for (node = head->prev->prev, safe = node->prev; node != head; node = safe, safe = node->prev) { const char *s1 = list_entry(peak, element_t, list)->value; const char *s2 = list_entry(node, element_t, list)->value; if ((!descend && strcmp(s1, s2) <= 0) || (descend && strcmp(s1, s2) >= 0)) { list_del(node); q_release_element(list_entry(node, element_t, list)); } else { peak = node; cnt += 1; } } return cnt; } ``` 呼叫 `purge` 並傳入 `descend = false` ,表示執行升冪保留。 ```c int q_ascend(struct list_head *head) { return purge(head, false); } ``` 呼叫 `purge` 並傳入 `descend = true` ,表示執行降冪保留。 ```c int q_descend(struct list_head *head) { return purge(head, true); } ``` ### `q_merge` >[Commit 1cf7897](https://github.com/sysprog21/lab0-c/commit/1cf78977df128588bdd1d0701a29652b9d91d867) 檢查輸入的佇列是否為空或為 `NULL`,再進行後續操作。 遍歷鏈結串列中的每個佇列,並將其合併為單一排序後的佇列。 初始化一個 `dummy` 節點以進行合併操作,並正確設置節點的 `next` 與 `prev` 指標。 若 `descend` 旗標為 `true`,則選擇性地對合併後的佇列進行反轉。 最後將排序完成的佇列合併回原始的 list 中。 ```c int q_merge(struct list_head *head, bool descend) { if (!head || list_empty(head)) return 0; int size = 0; struct list_head *merge_queue = NULL, *iter = head->next; while (iter != head) { queue_contex_t *curr_queue = list_entry(iter, queue_contex_t, chain); size += curr_queue->size; curr_queue->q->prev->next = NULL; merge_queue = merge_sorted_lists(merge_queue, curr_queue->q->next, descend); iter = iter->next; INIT_LIST_HEAD(curr_queue->q); } LIST_HEAD(dummy_head); dummy_head.next = merge_queue; struct list_head *node = NULL, *safe = NULL; for (node = &dummy_head, safe = dummy_head.next; safe->next; node = safe, safe = node->next) { safe->prev = node; } safe->next = &dummy_head; dummy_head.prev = safe; list_splice(&dummy_head, list_first_entry(head, queue_contex_t, chain)->q); return size; } ``` ### `make test` ```c +++ TESTING trace trace-17-complexity: # Test if time complexity of 'q_insert_tail', 'q_insert_head', 'q_remove_tail', and 'q_remove_head' is constant Probably constant time Probably constant time Probably constant time Probably constant time --- trace-17-complexity 5/5 --- TOTAL 100/100 ``` ## Valgrind + 自動測試程式 ### [Valgrind ](https://en.wikipedia.org/wiki/Valgrind) >參考 [Eric chou]((https://hackmd.io/@charliechiu/linux2025-homework1#Valgrind--%E8%87%AA%E5%8B%95%E6%B8%AC%E8%A9%A6%E7%A8%8B%E5%BC%8F)) `Valgrind` 是一套運行於使用者層級(`user space`)的動態分析工具框架,可用來檢測程式執行期間的記憶體使用狀況與潛在錯誤。使用者層與核心層(`kernel space`)的主要差異,在於可存取的記憶體範圍受限於虛擬記憶體機制,從而隔離應用程式與作業系統,避免一般應用程式誤修改系統核心的資料。而當 `user space` 中的應用需要存取系統資源時,則必須透過 `system call` 呼叫進入 `kernel space`。 `Valgrind` 本身並非直接執行原始機器碼,而是運用 `just-in-time (JIT)` 編譯技術,將機器指令轉換為中介語言 `(Intermediate Representation, IR)`,並在特定區段插入對應分析工具的檢查邏輯,再重新轉譯成機器碼執行。這使得 `Valgrind` 可以在不中斷執行流程的情況下動態觀察記憶體使用狀況,但也導致效能下降,通常會比原程式慢上數倍。 ### 觀察記憶體分配 為了分析程式在執行過程中對堆記憶體(heap memory)的使用狀況,我們使用 `Valgrind`工具套件中的子工具 `massif`。 `massif` 可提供程式在各個時間點的記憶體分配情況,並協助找出記憶體使用量高峰點或潛在的記憶體浪費問題。 這個工具可以載入 `massif` 產生的 `massif.out.<pid>` 檔案,讓你清楚看到記憶體分配的變化趨勢圖。 ``` # 安裝可視化工具 # 我們可以使用 massif-visualizer 這個圖形介面工具來視覺化分析結果 $ sudo apt install massif-visualizer -y ``` 在 `Makefile` 中新增以下目標 `check-massif`,目的是對 `qtest` 使用 `Valgrind` 的 `massif` 工具進行記憶體用量分析,並使用指定的輸入測資檔 `trace-06-ops.cmd` 執行測試。 ``` check-massif: qtest valgrind --tool=massif ./$< -v 3 -f traces/trace-06-ops.cmd ``` 執行以下指令會產生一個檔案,例如:`massif.out.12345` ``` $ make check-massif ``` 你可以使用以下指令以文字方式檢視記憶體使用歷程: ``` $ ms_print massif.out.12345 ``` 會得到像是以下的輸出 ``` KB 15.24^ ## | @ : # : ::::: | @ ::: # : :: :: :: | @ ::: # :::: :: :: | :@:@:: @::::: : # :::: @::::: | @:@@:@ :: ::::@:::::::::@:@# :::: @::::: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | ::::@ @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | : @ @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | : @ @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | : @ @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: | : @ @:@:@@:@ ::::::::@:::::::::@:@# :::: @:::@: 0 +----------------------------------------------------------------------->ki 0 835.4 ``` 也可以使用 GUI 工具: ``` $ massif-visualizer massif.out.12345 ``` 會得到像是以下的輸出 ![圖片](https://hackmd.io/_uploads/BJxk1Pm-ex.png) #### 分析上圖記憶體用量 ```c new // 建立新 queue,尚未分配記憶體 ih RAND 4 // 插入 4 筆隨機資料(insert head),記憶體用量小幅上升 it gerbil 3 // 插入 'gerbil' 到 tail,記憶體上升 it lion 2 // 插入 'lion' 到 tail,記憶體再次上升 it zebra 2 // 插入 'zebra' 到 tail,記憶體達到小波峰,堆區開始出現 malloc_or_fail 分配 sort // 對 queue 做排序,記憶體使用幾乎不變(應為 in-place 排序) dedup // 刪除重複節點,觀察到部分記憶體釋放(queue_remove 出現,堆下降) free // 釋放 queue,堆區記憶體幾乎歸零 new // 再次建立 queue,重新進入分配階段 ih a // 插入 a,記憶體開始重新配置 ih b ih c ih d ih a // 插入重複節點 'a' ih c // 插入重複節點 'c',記憶體繼續增加(test_malloc 占主體) descend // 降冪排列,記憶體維持原狀 rh d // 移除 head 'd',記憶體出現 queue_remove,記憶體下降 rh c // 繼續移除 rh b rh a // 每次移除都釋放一部分記憶體 free // 最後 queue 全部移除與釋放,回到初始狀態 new // 第三次建立 queue ih a 3 // 插入 a (weight 3),記憶體再次配置 ih b ih c ih d ih e 2 // 插入 e (weight 2) reverseK 3 // 每 3 個反轉一次,記憶體使用無明顯變化 rh d // 開始移除節點,queue_remove 出現,再次造成釋放 rh e rh e // 嘗試移除已刪除的節點,記憶體未變 rh a rh b rh c // 持續釋放 queue 節點 rh a // 試圖移除空 queue 節點 rh a // 試圖移除空 queue 節點,最終記憶體幾乎歸零 ``` 0 ~ 約 310,000: - 使用 `new`, `ih`, `it` 指令建立 queue 並插入多筆資料。 - `ih RAND 4` 插入隨機值,`it gerbil 3`, `it lion 2`, `it zebra 2` 插入具權重項目。 - 記憶體逐步增加,來源包括: - `malloc_or_fail` 分配節點記憶體。 - `_IO_file_doallocate` 分配輸出緩衝區。 --- 約 310,000: - 執行 `sort` 與 `dedup`: - 重複元素(如 zebra)被移除。 - `queue_remove` 開始釋放部分記憶體。 - 圖中出現第一次明顯的記憶體下滑。 --- 約 310,000 ~ 350,000: - 執行 `free` 釋放整個 queue,記憶體歸零。 - 接著再次執行 `new` 與多次 `ih` 指令插入: - 插入 a、b、c、d 及重複的 a、c。 - 記憶體快速跳升,反映節點重新配置。 --- 約 350,000 ~ 700,000: - 執行 `descend` 排序後,連續呼叫 `rh`(remove head): - 順序移除 d, c, b, a。 - 每次 `rh` 對應圖中鋸齒狀記憶體下滑。 --- 約 700,000 ~ 800,000: - 第三次 `new` 建立 queue,插入 a、b、c、d、e。 - `reverseK 3` 對前三個元素做 group reverse: - 造成短暫記憶體峰值(圖中達到 14.4 KiB)。 - 接著多次 `rh` 將所有元素移除。 --- 約 850,000: - 所有資料移除後,記憶體歸零。 - 程式正常結束,無記憶體殘留。 --- 小結 - 圖中明顯的記憶體上升段落對應插入與 `reverseK` 操作。 - 鋸齒式下降對應 `remove` / `remove head` 操作。 - `free` 對應一次性大量釋放記憶體。 - 此圖可幫助開發者檢查記憶體分配策略與潛在洩漏點。 ```c bool q_insert_tail(struct list_head *head, const char *s, int order) { int len = strlen(s); char *value_buf = malloc(len + 1); if (!value_buf) return false; memcpy(value_buf, s, len + 1); // 拷貝 '\0' element_t *elem = malloc(sizeof(element_t)); if (!elem) { free(value_buf); return false; } elem->value = value_buf; elem->order = order; list_add_tail(&elem->list, head); return true; } ``` ![圖片](https://hackmd.io/_uploads/Hy-4IgL-lg.png) ``` user@ting:~/linux2025/lab0-c$ ./test_sort_standalone Generating 10000000 random strings... [Top-down] Sorting... [Kernel bottom-up] Sorting... [Top-down] Sorted: Correct, Time: 6.733542 sec [Kernel bottom-up] Sorted: Correct, Time: 5.658984 sec Are the sorted lists identical? Yes First 10 elements after Top-down sort: 00(10954) 00(34809) 00(48758) 00(57181) 00(73406) 00(121297) 00(146560) 00(157486) 00(179901) 00(187963) First 10 elements after Kernel bottom-up sort: 00(10954) 00(34809) 00(48758) 00(57181) 00(73406) 00(121297) 00(146560) 00(157486) 00(179901) 00(187963) [Top-down] Average sorting time: 1.149401 sec [Kernel bottom-up] Average sorting time: 0.591669 sec ```