--- tags: linux2022 --- # 2022q1 Homework1 (lab0) contributed by < `eric88525` > ## 實驗環境 ```shell! $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 Copyright (C) 2019 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 48 bits physical, 48 bits virtual CPU(s): 16 On-line CPU(s) list: 0-15 Thread(s) per core: 2 Core(s) per socket: 8 Socket(s): 1 NUMA node(s): 1 Vendor ID: AuthenticAMD CPU family: 25 Model: 33 Model name: AMD Ryzen 7 5800X 8-Core Processor Stepping: 0 Frequency boost: enabled CPU MHz: 2200.000 CPU max MHz: 4850.1948 CPU min MHz: 2200.0000 BogoMIPS: 7585.34 Virtualization: AMD-V L1d cache: 256 KiB L1i cache: 256 KiB L2 cache: 4 MiB L3 cache: 32 MiB NUMA node0 CPU(s): 0-15 ``` ## [作業要求](https://hackmd.io/@sysprog/linux2022-lab0) * 在 GitHub 上 fork [lab0-c](https://github.com/sysprog21/lab0-c) * 參閱 [Git 教學和 GitHub 設定指引](https://hackmd.io/@sysprog/git-with-github) ^附教學影片^ * 依據上述指示著手修改 `queue.[ch]` 和連帶的檔案,測試後用 Git 管理各項修改,要能滿足 `$ make test` 自動評分系統的所有項目。 * 在提交程式變更前,務必詳閱 [如何寫好 Git Commit Message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/) * 詳閱「[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)」並應用裡頭提及的手法,例如 pointer to pointer (指向某個指標的指標,或說 indirect pointer),讓程式碼更精簡且有效 * 研讀 Linux 核心原始程式碼的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試引入到 `lab0-c` 專案,比較你自己實作的 merge sort 和 Linux 核心程式碼之間效能落差 * 在 `qtest` 提供新的命令 `shuffle`,允許藉由 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作 * 在 `qtest` 提供新的命令 `web`,提供 web 伺服器功能,注意: web 伺服器運作過程中,`qtest` 仍可接受其他命令 * 可依據前述說明,嘗試整合 [tiny-web-server](https://github.com/7890/tiny-web-server) * 除了修改程式,也要編輯「[作業區](https://hackmd.io/@sysprog/linux2022-homework1)」,增添開發紀錄和 GitHub 連結,除了提及你如何逐步達到自動評分程式的要求外,共筆也要涵蓋以下: * 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),修正 `qtest` 執行過程中的錯誤 * 先執行 `qtest` 再於命令提示列輸入 `help` 命令,會使 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer) 觸發錯誤,應予以排除 * 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗 * 解釋 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作,說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。可對照參考 [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/H1TtmVTTz) * 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉,解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 [Student's t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 及程式實作的原理。注意:現有實作存在若干致命缺陷,請討論並提出解決方案; * 指出現有程式的缺陷 (提示: 和 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 有關) 或者可改進之處 (例如更全面地使用 Linux 核心風格的鏈結串列 API),嘗試實作並提交 pull request * 截止日期: * Mar 1, 2022 (含) 之前 * 越早在 GitHub 上有動態、越早接受 code review 並持續改進者,評分越高 指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改,記得也該實作 q_sort 函式。 [queue.c](https://github.com/sysprog21/lab0-c/blob/master/queue.c) 僅提供介面 ([interface](https://en.wikipedia.org/wiki/Interface-based_programming)) 但尚未有完整程式實作,需要由學員補完並逐步精進,包含以下: - [x] `q_new`: 建立新的「空」佇列; - [x] `q_free`: 釋放佇列所佔用的記憶體; - [x] `q_insert_head`: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則); - [x] `q_insert_tail`: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則); - [x] `q_ _head`: 在佇列開頭 (head) 移去 (remove) 給定的節點; - [x] `q_release_element`: 釋放特定節點的記憶體空間; - [x] `q_size`: 計算目前佇列的節點總量; - [x] `q_delete_mid`: 移走佇列中間的節點,詳見 [LeetCode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) - [x] `q_delete_dup`: 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/) - [x] `q_swap`: 交換佇列中鄰近的節點,詳見 [LeetCode 24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/) - [x] `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點; - [x] `q_sort`: 以==遞增順序==來排序鏈結串列的節點,可參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 得知實作手法; # 開發過程 ## q_new 建立空的list_head,並閱讀linux kernel api和list.h來簡化相關程式 ```c struct list_head *q_new() { struct list_head *p = malloc(sizeof(struct list_head)); // allocate space fail, return null if (p == NULL) return NULL; // init pointer INIT_LIST_HEAD(p); return p; } ``` ## q_free + 參考 kernel api,list_for_each_entry_safe 會用暫存n來記錄下一個位置,就可以安全刪除當前節點 + [link](https://www.cnblogs.com/yangguang-it/p/11667772.html) ```c void q_free(struct list_head *l) { if (l == NULL) return; element_t *pos = NULL, *n = NULL; list_for_each_entry_safe (pos, n, l, list) q_release_element(pos); free(l); } ``` ## q_insert_head + kernel api 已經提供相關實作,只需要處理記憶體管理和字串拷貝 > list_add: Insert a new entry after the specified head. This is good for implementing stacks. ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; // create new node and assign value to it element_t *new_node = malloc(sizeof(element_t)); if (!new_node) return false; new_node->value = strdup(s); if (!new_node->value) { q_release_element(new_node); return false; } list_add(&new_node->list, head); return true; } ``` ## q_insert_tail + kernel api 已經提供相關實作,只需要處理記憶體管理和字串拷貝 > list_add_tail: Insert a new entry before the specified head. This is useful for implementing queues. ```c bool q_insert_tail(struct list_head *head, char *s) { if (!s || !head) return false; // create new node and assign value to it element_t *new_node = malloc(sizeof(element_t)); if (!new_node) return false; new_node->value = strdup(s); if (!new_node->value) { q_release_element(new_node); return false; } list_add_tail(&new_node->list, head); return true; } ``` ## q_remove_head + 呼叫 list_first_entry 來取得第一個entry + 用 list_del 來確保前後節點在移除 entry 後仍然互相連接。 + 複製字串到sp ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *p = list_first_entry(head, element_t, list); list_del_init(&(p->list)); if (sp) { strncpy(sp, p->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return p; } ``` ## q_release_element + 無變動 ```c void q_release_element(element_t *e) { free(e->value); free(e); } ``` ## q_size + 遍歷整個 list 來計算size ```c int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; int size = 0; struct list_head *p = NULL; list_for_each (p, head) size++; return size; } ``` ## q_delete_mid + 運用快/慢指標,找出中心點 + 找出中心後,用 list_del_init 確保 linked list 串接正常 + 釋放記憶體 ```c bool q_delete_mid(struct list_head *head) { if (head == NULL || list_empty(head)) return false; struct list_head **p = &head->next; for (struct list_head *fast = head->next; fast != head && fast->next != head; fast = fast->next->next) p = &(*p)->next; struct list_head *to_delete = (*p); list_del_init(to_delete); q_release_element(container_of(to_delete, element_t, list)); return true; } ``` ## q_delete_dup + 首先讓 double pointer p 指向 head 的 next 指標 + 如果遇到重複的節點,透過prev來暫存即將被刪除的節點,並讓curr往下移動,並刪除prev + 把 p 移動到下一個節點的next指標上 + 如果 curr 或是 curr->next 為 head 就結束 ```c bool q_delete_dup(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head **p = &(head->next); struct list_head *curr = head->next, *prev = NULL; while (curr != head && curr->next != head) { if (strcmp(container_of(curr, element_t, list)->value, container_of(curr->next, element_t, list)->value) == 0) { do { prev = curr; curr = curr->next; list_del(prev); q_release_element(container_of(prev, element_t, list)); } while (curr->next != head && strcmp(container_of(curr, element_t, list)->value, container_of(curr->next, element_t, list)->value) == 0); } p = &((*p)->next); curr = curr->next; } return true; } ``` ## q_swap + 兩兩交換節點 + 如果有a和b節點需要交換,首先把b抽出來,接著加入a的前面 + 原本是使用 list_add_tail 而不是 list_add,會需要額外的指標來記錄 a 前面的節點,如果用 list_add_tail就可以省下 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *curr = head->next; struct list_head *next = NULL; while (curr && curr->next && curr != head && curr->next != head) { next = curr->next; list_del(next); list_add_tail(next, curr); curr = curr->next; } } ``` ## q_reverse + 運用prev和next來紀錄前後位置,逐一反轉節點 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *prev = head->prev, *curr = head, *next = NULL; while (next != head) { next = curr->next; curr->next = prev; curr->prev = next; prev = curr; curr = next; } } ``` ## q_sort + 參考老師提供的merge sort,改成有prev的版本 + 一開始要先把 head 拆出來,變成單向的 linking list ,最後串回來 + merge sort相較於quick sort能有更穩定的速度(都是 nlogn) + 更新: 在進入q_sort後要先用 list_empty來檢查一次,不然 test17 會出問題 ```c struct list_head *merge(struct list_head *L1, struct list_head *L2) { struct list_head *head = NULL, **ptr = &head, **node = NULL; struct list_head *prev_node = NULL; for (node = NULL; L1 && L2; prev_node = *node, *node = (*node)->next) { // let node point to smaller node pointer(L1/L2) node = strcmp(container_of(L1, element_t, list)->value, container_of(L2, element_t, list)->value) > 0 ? &L2 : &L1; *ptr = *node; (*node)->prev = prev_node; ptr = &(*ptr)->next; } *node = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2); *ptr = *node; (*node)->prev = prev_node; return head; } struct list_head *merge_sort(struct list_head *head) { if (!head || !head->next) return head; struct list_head *fast = head->next; struct list_head *slow = head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; struct list_head *L1 = merge_sort(head); struct list_head *L2 = merge_sort(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(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *last_node = head->prev; last_node->next = NULL; head->next->prev = NULL; struct list_head *sorted_list = merge_sort(head->next); INIT_LIST_HEAD(head); head->next = sorted_list; sorted_list->prev = head; // move last_node pointer to for (last_node = sorted_list; last_node->next != NULL; last_node = last_node->next) { } head->prev = last_node; last_node->next = head; } ``` ## 最後一個測試的 bug + 在本地有時會顯示 ERROR: Probably not constant time ,有時則會通過,這似乎只是本機上的問題,丟到遠端還是會通過 ## shuffle 命令 > 參考[作業說明](https://hackmd.io/@sysprog/linux2022-lab0#qtest-%E5%91%BD%E4%BB%A4%E7%9B%B4%E8%AD%AF%E5%99%A8%E7%9A%84%E5%AF%A6%E4%BD%9C) 的 qtest 命令直譯器介紹來實作 在 qtest.c 的 `console_init()`內加入這行,可以新增命令、function、訊息顯示 ```c ADD_COMMAND(shuffle, " | Shuffle every nodes in queue"); ``` `ADD_COMMAND` 是把 `add_cmd()` 再包裝一次,由於巨集展開後會把 `cmd` 和 do_`cmd` 的 function 相連,因此要連結的 function名稱應該以 do_開頭 ```c // console.h void add_cmd(char *name, cmd_function operation, char *documentation); #define ADD_COMMAND(cmd, msg) add_cmd(#cmd, do_##cmd, msg) ``` 在 qtest.c 加入 `do_shuffle` 函式,有做 arguments 和 null 的檢查 ```c // qtest.c static bool do_shuffle(int argc, char *argv[]) { if (argc != 1) { report(1, "%s takes no arguments", argv[0]); return false; } if (!l_meta.l) report(3, "Warning: Calling sort on null queue"); error_check(); if (exception_setup(true)) q_shuffle(l_meta.l); show_queue(3); return !error_check(); } ``` 在 qtest.c 內實作 shuffle,這邊會檢查 head 是否為null或empty,接著每次從還沒被 shuffle 的節點中,選一個來移動到尾端 ```c void q_shuffle(struct list_head *head) { if (!head || list_empty(head)) return; int n = q_size(head); if (n == 1) return; int i; for (i = n; i > 1; i--) { int randNum = rand() % i; struct list_head *tmp = head->next; while (randNum--) tmp = tmp->next; list_move_tail(tmp, head); } } ``` # 引入 lib/list_sort.c + [linux 的 list_sort ](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 定義如下 + priv 是要傳入給 cmp 的資料 + cmp 為用來比較的 function ```c void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp) ``` cmp argument 的格式 ```c // list_cmp_func_t is a function pointer, it takes (void *, const struct list_head *, const struct list_head *) as argument and return int typedef int (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *) ``` 要套用到本專案的話,單獨把 `list_sort.h` `list_sort.c` 抽出並放入專案目錄,並做以下修改 ```diff // list_sort.h /* SPDX-License-Identifier: GPL-2.0 */ #ifndef _LINUX_LIST_SORT_H #define _LINUX_LIST_SORT_H #include <linux/types.h> // 從 linux/compiler.h 抽出 likely(x) 和 unlikely(x) + #define likely(x) __builtin_expect(!!(x), 1) + #define unlikely(x) __builtin_expect(!!(x), 0) struct list_head; typedef int __attribute__((nonnull(2, 3))) (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *); // 測試程式中,會嘗試傳入 NULL 到 sort 內 // 因此刪除 __attribute__((nonnull(2,3))) 的 function attribute - __attribute__((nonnull(2,3))) void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp); #endif ``` + 簡化標頭檔的使用 ```diff // list_sort.c - #include <linux/kernel.h> - #include <linux/bug.h> - #include <linux/compiler.h> - #include <linux/export.h> - #include <linux/string.h> - #include <linux/list_sort.h> - #include <linux/list.h> + #include "list_sort.h" + #include "list.h" ``` + 刪除 function attribute並加入 null 判斷 ```diff // list_sort.c - __attribute__((nonnull(2,3))) void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp) + if (head == NULL) + return; ``` + 加入 compare function 在 q_test.c , 也要 include 標頭檔 ```diff // q_test.c + #include "list_sort.h" // the compare function of element_t compare + int compare_element_t(void *priv, + const struct list_head *l, + const struct list_head *r) + { + return strcmp(container_of(l, element_t, list)->value, + container_of(r, element_t, list)->value); + } ``` + 修改 `qtest.c` 的 do_sort() ,讓執行 sort 時後面加入 `linux` 參數可切換為 linux 版本的 sort ```diff bool do_sort(int argc, char *argv[]) { - if (argc != 1) { - report(1, "%s takes no arguments", argv[0]); + if (argc > 2) + report(1, "%s takes <=2 arguments", argv[0]); if (!l_meta.l) report(3, "Warning: Calling sort on null queue"); error_check(); int cnt = q_size(l_meta.l); if (cnt < 2) report(3, "Warning: Calling sort on single node"); error_check(); set_noallocate_mode(true); if (exception_setup(true)) { - q_sort(l_meta.l); + if (argc == 2 && !strcmp(argv[1], "linux")) { + list_sort(NULL, l_meta.l, compare_element_t); + } else { + q_sort(l_meta.l); + } } ``` + 在 makefile 的 `OBJS` 加入 `list_sort.o` ```diff OBJS := qtest.o report.o console.o harness.o queue.o \ random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \ - linenoise.o + linenoise.o list_sort.o ``` ### 比較效能 + 執行命令檔來比較效能 ```c # Comapre performance of my merge sort with linux sort option fail 0 option malloc 0 # 30k sample new ih a 10000 ih b 10000 ih c 10000 time sort reverse time sort linux free # 300k sample new ih a 100000 ih b 100000 ih c 100000 time sort reverse time sort linux free # 3M sample new ih a 1000000 ih b 1000000 ih c 1000000 time sort reverse time sort linux free ``` + 在不同 sample 數量下的執行時間比較 | | my merge sort | linux sort | | -------- | -------- | -------- | | 30k samples | 0.003 | 0.001 | | 300k samples | 0.044 | 0.023 | | 3M samples | 0.619 | 0.350 | # linux 的 sort 分析 篇幅過長,另外開筆記 [link](https://hackmd.io/@eric88525/list_sort) # tiny web server # 運用 Valgrind 排除 qtest 實作的記憶體錯誤 1. 用 valgrind 檢查記憶體,每種測試都有以下問題,都跟 `linenoiseHistoryAdd` 或 `linenoiseHistoryLoad` 有關,而這兩個 function 都在 linenoise.h 被定義。 問題點的呼叫順序為 main -> linenoiseHistoryLoad -> linenoiseHistoryAdd -> linenoiseHistoryAdd -> strdup ``` $ make valgrind # Test of insert_head and remove_head ==2070491== 10 bytes in 1 blocks are still reachable in loss record 1 of 3 ==2070491== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==2070491== by 0x4A5850E: strdup (strdup.c:42) ==2070491== by 0x110A27: linenoiseHistoryAdd (linenoise.c:1236) ==2070491== by 0x1115BA: linenoiseHistoryLoad (linenoise.c:1325) ==2070491== by 0x10C82A: main (qtest.c:1010) ``` :::spoiler linenoiseHistoryAdd 的實做 (linenoise.c) ```c= /* This is the API call to add a new entry in the linenoise history. * It uses a fixed array of char pointers that are shifted (memmoved) * when the history max length is reached in order to remove the older * entry and make room for the new one, so it is not exactly suitable for huge * histories, but will work well for a few hundred of entries. * * Using a circular buffer is smarter, but a bit more complex to handle. */ int linenoiseHistoryAdd(const char *line) { char *linecopy; if (history_max_len == 0) return 0; /* Initialization on first call. */ if (history == NULL) { history = malloc(sizeof(char *) * history_max_len); if (history == NULL) return 0; memset(history, 0, (sizeof(char *) * history_max_len)); } /* Don't add duplicated lines. */ if (history_len && !strcmp(history[history_len - 1], line)) return 0; /* Add an heap allocated copy of the line in the history. * If we reached the max length, remove the older line. */ linecopy = strdup(line); if (!linecopy) return 0; if (history_len == history_max_len) { free(history[0]); memmove(history, history + 1, sizeof(char *) * (history_max_len - 1)); history_len--; } history[history_len] = linecopy; history_len++; return 1; } ``` ::: 2. 根據 linenoiseHistory 的描述,可以推斷 history 為一個 pointer array,裡面最多紀錄 history_max_len 個 char pointer。 valrind 的報告指出問題會出在 line29: strdup 這裡,當新加入了 line 後,程式有確實的把被移出的 line 給釋放掉,那問題或許會出在 history 在程式執行完成後沒全部釋放 在 linenoise.c 有宣告history ```c=132 static char **history = NULL; ``` :::spoiler main at qtest.c ```c= #define BUFSIZE 256 int main(int argc, char *argv[]) { /* sanity check for git hook integration */ if (!sanity_check()) return -1; /* To hold input file name */ char buf[BUFSIZE]; char *infile_name = NULL; char lbuf[BUFSIZE]; char *logfile_name = NULL; int level = 4; int c; while ((c = getopt(argc, argv, "hv:f:l:")) != -1) { switch (c) { case 'h': usage(argv[0]); break; case 'f': strncpy(buf, optarg, BUFSIZE); buf[BUFSIZE - 1] = '\0'; infile_name = buf; break; case 'v': { char *endptr; errno = 0; level = strtol(optarg, &endptr, 10); if (errno != 0 || endptr == optarg) { fprintf(stderr, "Invalid verbosity level\n"); exit(EXIT_FAILURE); } break; } case 'l': strncpy(lbuf, optarg, BUFSIZE); buf[BUFSIZE - 1] = '\0'; logfile_name = lbuf; break; default: printf("Unknown option '%c'\n", c); usage(argv[0]); break; } } srand((unsigned int) (time(NULL))); queue_init(); init_cmd(); console_init(); /* Trigger call back function(auto completion) */ linenoiseSetCompletionCallback(completion); linenoiseHistorySetMaxLen(HISTORY_LEN); linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ set_verblevel(level); if (level > 1) { set_echo(true); } if (logfile_name) set_logfile(logfile_name); add_quit_helper(queue_quit); bool ok = true; ok = ok && run_console(infile_name); ok = ok && finish_cmd(); return ok ? 0 : 1; } ``` ::: 3. 回到 qtest.c 觀察 main ,在呼叫 `linenoiseHistoryLoad` 後,似乎沒有做任何事來讓 history 被釋放,因此第二點的推論或許是正確的 4. 從 linenoice.h 中看到以下宣告,猜測這跟釋放 history 或 line 有關 ```c void linenoiseFree(void *ptr); ``` 5. 回去 linenoice.c 中查找實做,發現了沒被 header file 宣告的函式,此函式可以把 history 釋放 ```c /* Free the history, but does not reset it. Only used when we have to * exit() to avoid memory leaks are reported by valgrind & co. */ static void freeHistory(void) { if (history) { int j; for (j = 0; j < history_len; j++) free(history[j]); free(history); } } ``` 6. 而在 linenoise.c ,只有另一個函式呼叫 freeHistory ```c /* At exit we'll try to fix the terminal to the initial conditions. */ static void linenoiseAtExit(void) { disableRawMode(STDIN_FILENO); freeHistory(); } ``` 7. 嘗試找出 `linenoiseAtExit` 和 `freeHistory` 在專案有哪些地方會被執行,結果完全沒有,證實了第 2 點的猜想,根本沒有去釋放 history ![](https://i.imgur.com/n7Yxlzs.png) ![](https://i.imgur.com/wxYlYoS.png) 8. 因此要修復記憶體問題,就只有手動呼叫 `linenoiseAtExit` 或是 `freeHistory` 了 + 把 `linenoiseAtExit` 從 static void 改成 void 來改變可視範圍,並移動宣告到 header file ```diff=174 // linenoise.c - static void linenoiseAtExit(void); ``` ```diff=73 // linenoise.h + void linenoiseAtExit(void); ``` + 由於 qtest.c 內並沒有 include `linenoise.h` ,因此嘗試加在`finish_cmd()` 內,會選擇加在這是因為: qtest.c 在 main 結束前有呼叫 `finish_cmd()` 來檢驗程式有沒有正確關閉 ```diff=589 // console.c bool finish_cmd() { bool ok = true; if (!quit_flag) ok = ok && do_quit(0, NULL); has_infile = false; + linenoiseAtExit(); return ok && err_cnt == 0; } ``` 9. 重新執行 make valgrind 後問題解決,沒有任何記憶體洩漏