# 2023q1 Homework1 (lab0) contributed by < `dYu0y` > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 Copyright (C) 2021 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 Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 20 On-line CPU(s) list: 0-19 Vendor ID: GenuineIntel Model name: 12th Gen Intel(R) Core(TM) i7-12700H CPU family: 6 Model: 154 Thread(s) per core: 2 Core(s) per socket: 14 Socket(s): 1 Stepping: 3 CPU max MHz: 4700.0000 CPU min MHz: 400.0000 BogoMIPS: 5376.00 ``` ## 開發過程 ### 對結構串接方式誤會 一開始誤會了結構串接的方式,畢竟從作業的描述與老師在上課時的操作可以看出,這裡的佇列實際上有著雙層的結構。 :::warning 不要濫用詞彙,請回去翻閱你的數學課本,理解「二維」的定義。 :notes: jserv ::: 使用者可以透過命令 `new` 來建立一個一維的佇列,這個佇列內可以存放多個字串。如果使用者多次呼叫 new 命令,外層會有另外一個佇列,將這些使用者建立的所有一維佇列串接起來,並且可以透過 `prev` 和 `next` 命令在這些佇列間切換。 於是在一開始看到 `queue_contex_t` 的定義時: ```c typedef struct { struct list_head *q; struct list_head chain; int size; int id; } queue_contex_t; ``` 誤以為 `q` 是用來指向用來存放字串的佇列 `list_head` 的指標,而 `chain` 則是用來串接所有透過使用者呼叫 `new` 命令建立的佇列。 所以 `q_new` 寫成了下面這樣: ```c // in queue.c struct list_head *q_new() { queue_contex_t *queue = (queue_contex_t *) malloc(sizeof(queue_contex_t)); if (!queue) { return NULL; } queue->q = (struct list_head *) malloc(sizeof(struct list_head)); if (!queue->q) { free(queue); return NULL; } INIT_LIST_HEAD(&queue->chain); INIT_LIST_HEAD(queue->q); return &queue->chain; } ``` 後續在完成諸如對 `q_insert` 與 `q_free` 等函式的實作並開始進行測試時,發現總是有記憶體沒有被釋放,即便我看不出來我的 `q_free` 哪裡有寫錯。 當時 `q_free` 的實作: ```c // in queue.c void q_free(struct list_head *l) { queue_contex_t *queue = container_of(l, queue_contex_t, chain); element_t *iter, *safe; list_for_each_entry_safe (iter, safe, queue->q, list) { q_release_element(iter); } free(queue->q); free(queue); } ``` 執行的結果: ```shell $ ./qtest cmd> new l = [] cmd> ih RAND 3 l = [sfxup fhjvevv dmeht] cmd> free l = NULL ERROR: There is no queue, but 6 blocks are still allocated ``` 經過幾輪測試,發現沒釋放的記憶體區塊數量正好是插入節點數量的兩倍,因此推測應該是 `insert` 過程中新增的節點與複製的字串這兩塊記憶體沒有被釋放,才會產生這個現象。 `insert` 的部分程式碼節錄: ```c bool q_insert_head(struct list_head *head, char *s) { element_t *new = (element_t *) malloc(sizeof(element_t)); if (!new) { return false; } new->value = strdup(s); if (!new->value) { free(new); return false; } // ...後續部分 } ``` 於是我感到很疑惑,因為這就代表了我的 `q_free` 實作中,`list_foreach_entry_safe` 的迴圈部分並沒有被執行,也就是說下方程式碼的第 6 ~ 8 行的迴圈,因為不明的原因,沒有正確的將佇列的每個節點進行正確的釋放。 ```c= // in queue.c void q_free(struct list_head *l) { queue_contex_t *queue = container_of(l, queue_contex_t, chain); element_t *iter, *safe; list_for_each_entry_safe (iter, safe, queue->q, list) { q_release_element(iter); } free(queue->q); free(queue); } ``` 為了了解到底發生了什麼事,我決定使用 gdb 的逐行執行功能來進行除錯。 首先檢查了 Makefile 檔案確定了編譯的參數有加入 `-g`,使得產生的可執行檔是可以被 gdb 讀取的。 之後使用 gdb 除錯的過程如下節錄: ```shell $ gdb ./qtest (gdb) b q_free (gdb) r cmd> new l = [] cmd> ih RAND 3 l = [rnjclmi rijzhmx uogrpnpel] cmd> free (gdb) layout src (gdb) n ``` 這邊由於很明確出問題的地方就在 `q_free` 之內,於是將斷點設置在其上,奇怪的是當我在 gdb 內使用 n 命令要執行下一行時,卻會直接跳至 `harness.c` 的 `exception_setup` 函式內。 在大略的看過 `harness.c` 的檔案內容後,初步的解決方式是將 `time_limit` 的初始值設定得大一點,避免在使用 gdb 的過程中因為超時而難以除錯。 但是這樣的缺點是每次在 commit 之前還需要將 `time_limit` 的初始值調回來,需要 debug 時又需要將初始值調大。 這邊作個簡單的思考就可以知道,要除錯的過程不太可能這麼麻煩,應該是我漏掉了什麼東西。最後也如我所料,在專案的 `READMD.md` 中,我看到了這一段: --- ## Debugging Facilities Before using GDB debug `qtest`, there are some routine instructions need to do. The script `scripts/debug.py` covers these instructions and provides basic debug function. ```shell $ scripts/debug.py -h usage: debug.py [-h] [-d | -a] optional arguments: -h, --help show this help message and exit -d, --debug Enter gdb shell -a, --analyze Analyze the core dump file ``` * Enter GDB without interruption by **SIGALRM**. ``` $ scripts/debug.py -d Reading symbols from lab0-c/qtest...done. Signal Stop Print Pass to program Description SIGALRM No No No Alarm clock Starting program: lab0-c/qtest cmd> ``` --- 原來透過呼叫 `$ scripts/debug.py -d` 進入 gdb 就可以避免觸發 SIGALRM 的中斷。 後來在 gdb 的幫助下找到了 `qtest.c` 中使用者呼叫 `new` 命令時會執行的函式 `do_new`,以下是程式碼節錄: ```c static bool do_new(int argc, char *argv[]) { // ... if (exception_setup(true)) { queue_contex_t *qctx = malloc(sizeof(queue_contex_t)); list_add_tail(&qctx->chain, &chain.head); qctx->size = 0; qctx->q = q_new(); qctx->id = chain.size++; current = qctx; } // ... } ``` 這下才發現原來我對於結構串接方式的認知是有誤的,於是對已經實作的函式進行了修改,終於不會在執行 `./qtest` 時呼叫 `free` 函式之後,看到仍然有尚未釋放的記憶體區塊了。 現階段的程式碼,至少能先通過 `$ make check` > commit [e89243d](https://github.com/dYu0y/lab0-c/commit/e89243d) 目前這個 commit 裡面含有的功能太多,之後會使用 patch 的方式拆分為多個只包含單一功能的 commit。 ### delete duplicate 我的想法是使用一個迴圈將佇列從頭到尾走訪一遍,檢查是否有含有重複元素的節點,並將那些節點移至另一個待處理佇列中。 等到迴圈結束後,再一次性釋放待處理佇列中所有節點所持有的資源。 這邊比較要注意的是在 `queue.h` 中的註解: ```c /** * q_delete_dup() - Delete all nodes that have duplicate string, 148 * leaving only distinct strings from the original queue. * @head: header of queue * * Reference: * https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ * * Return: true for success, false if list is NULL. */ bool q_delete_dup(struct list_head *head); ``` 提到了函式的返回值,只有當傳入的佇列是 NULL 時才回傳 false。 > commit [5d975e1](https://github.com/dyu0y/lab0-c/commit/5d975e1) --- 以下部分還尚未完成 ### reverse k 先建立一個計數器 `len` 並將初始值設為 0,透過 linked list api 中的 `list_for_each_safe` 臨時連結: q_swap: > commit [7879380](https://github.com/sysprog21/lab0-c/commit/7879380) q_reverseK: > commit [3bd2bf6](https://github.com/sysprog21/lab0-c/commit/3bd2bf6) q_reverse: > commit [f4770dc3](https://github.com/sysprog21/lab0-c/commit/f4770dc3)