--- tags: linux2022 --- # 2022q1 Homework1 (lab0) contributed by < [`ShawnLu31`](https://github.com/ShawnLu31) > ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 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: GenuineIntel CPU family: 6 Model: 165 Model name: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz Stepping: 5 CPU MHz: 2900.000 CPU max MHz: 4800.0000 CPU min MHz: 800.0000 BogoMIPS: 5799.77 Virtualization: VT-x L1d cache: 256 KiB L1i cache: 256 KiB L2 cache: 2 MiB L3 cache: 16 MiB NUMA node0 CPU(s): 0-15 ``` ## 作業要求 > [lab0](https://hackmd.io/@sysprog/2020-lab0) - [x] 開發環境設定 - [x] Fork lab0 on github - [x] 佇列實作 - [x] `q_new`: 建立新的「空」佇列 - [x] `q_free`: 釋放佇列所佔用的記憶體 - [x] `q_insert_head`: 在 head 插入 (insert) 給定的新節點 (LIFO) - [x] `q_insert_tail`: 在 tail 插入 (insert) 給定的新節點 (FIFO) - [x] `q_remove_head`: 從 head 移去 (remove) 節點,回傳該節點 - [x] `q_remove_tail`: 從 tail 移去 (remove) 節點,回傳該節點 - [x] `q_release_element`: 釋放特定節點的記憶體空間 - [x] `q_size`: 計算目前佇列的節點總量 - [x] `q_delete_mid`: 移走佇列中間的節點 - [x] `q_delete_dup`: 在已經排序的狀況,移走佇列中具備重複內容的節點 - [x] `q_swap`: 交換佇列中鄰近的節點 - [x] `q_reverse`: 以反向順序重新排列鏈結串列,不配置或釋放任何節點(重排已經存在的節點) - [x] `q_sort`: 以遞增順序來排序鏈結串列的節點 - [ ] 用 Graphviz 在 HackMD 筆記上視覺化展現 - [x] 引入 Git pre-commit hook - [x] 透過 clang-format 確保一致的程式風格 - [x] 詳閱 - [x] 使用 - [ ] 透過 GNU Aspell 進行拼字檢查 - [ ] 開啟 Cppcheck 靜態程式碼檢查功能 - [ ] 以 Valgrind 分析記憶體問題 ## 開發過程 ### 佇列架構 ### lish.h 巨集參考 - `INIT_LIST_HEAD()` - Initialize empty list head - Add new node - `list_add()` - Add a list node to the beginning of the list - `list_add_tail()` - Add a list node to the end of the list - `list_splice()` - Add list nodes from a list to beginning of another list - `list_splice_tail()` - Add list nodes from a list to end of another list - `list_splice_init()` - Move list nodes from a list to beginning of another list - `list_splice_tail_init()` - Move list nodes from a list to end of another list - Remove node - `list_del()` - Remove a list node from the list - `list_del_init()` - Remove a list node from the list and reinitialize it - Check - `list_empty()` - Check if list head has no nodes attached - `list_is_singular()` - Check if list head has exactly one node attached - Move - `list_cut_position()` - Move beginning of a list to another list - `list_move()` - Move a list node to the beginning of the list - `list_move_tail()` - Move a list node to the end of the list - Entry - `list_first_entry()` - get first entry of the list - `list_last_entry()` - get last entry of the list - iteraton - `list_for_each` - iterate over list nodes - `list_for_each_entry` - iterate over list entries ### 針對佇列的實作 #### `q_new` 用 `malloc` 取得 `struct list_head` 大小的記憶體。 - 如果失敗回傳 `NULL` 。 - 成功則用 `INIT_LIST_HEAD()` 做初始化。 ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (!head) return NULL; INIT_LIST_HEAD(head); return head; } ``` :::danger 注意共筆書寫規範:中英文間用一個半形空白區隔 :notes: jserv ::: #### `q_free` 此函式須釋放佇列所有空間,所以須釋放 `element` 和 `l` 。 用 `list_for_each_entry_safe()` 走訪節點。 - entry ,對應 element 物件。 - safe ,允許移除目前節點。 用 `list_del()` 移出佇列, `q_release_element()` 釋放節點空間。 釋放佇列 `l` 。 ```c void q_free(struct list_head *l) { if (!l) return; /* queue is NULL */ element_t *del, *next; /* del, the element to be free. next, the next element of del */ list_for_each_entry_safe (del, next, l, list) { list_del(&del->list); q_release_element(del); } free(l); return; } ``` #### `q_insert_head` 若佇列是`NULL` 立刻回傳 false 。 用 `malloc` 取得 `element_t` 大小的記憶體。失敗回傳 false 。 用 `malloc` 取得 s 字串大小的記憶體。失敗釋放 e 的空間並回傳 false 。 用 `memcpy` 複製字串和 `INIT_LIST_HEAD()` 初始化新節點,再用 `list_add` 加入佇列最前端。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; /* queue is NULL */ element_t *e = malloc(sizeof(element_t)); if (!e) return NULL; /* could not allocate space */ int len = strlen(s) + 1; e->value = malloc(sizeof(char) * len); if (!e->value) { free(e); return NULL; /* could not allocate space */ } memcpy(e->value, s, len); INIT_LIST_HEAD(&e->list); list_add(&e->list, head); return true; } ``` ##### 更新: 用 `gen_element` 取代與 `q_insert_tail` 相同的程式。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head || !s) return false; /* queue is NULL or s is NULL*/ element_t *e = gen_element(s); if (!e) return false; list_add(&e->list, head); return true; } ``` #### `q_insert_tail` 大致與 `q_insert_head()` 相同。用 `list_add_tail()` 加入佇列尾端。 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; /* queue is NULL */ element_t *e = malloc(sizeof(element_t)); if (!e) return NULL; /* could not allocate space */ int len = strlen(s) + 1; e->value = malloc(sizeof(char) * len); if (!e->value) { free(e); return NULL; /* could not allocate space */ } memcpy(e->value, s, len); INIT_LIST_HEAD(&e->list); list_add_tail(&e->list, head); return true; } ``` ##### 更新: ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head || !s) return false; /* queue is NULL or s is NULL*/ element_t *e = gen_element(s); if (!e) return false; list_add_tail(&e->list, head); return true; } ``` ##### `gen_element` ```c element_t *gen_element(char *s) { element_t *e = malloc(sizeof(element_t)); if (!e) return NULL; /* could not allocate space */ int len = strlen(s) + 1; e->value = malloc(sizeof(char) * len); if (!e->value) { free(e); return NULL; /* could not allocate space */ } memcpy(e->value, s, len); INIT_LIST_HEAD(&e->list); return e; } ``` #### `q_remove_head` 若佇列為 `NULL` 或空,回傳 `NULL` 。 `list_first_entry()` 取得第一個節點位置, `list_del_init()` 移除該節點。 若 `sp` 不為 `NULL` ,複製節點的 `value` 到 `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 *e = list_first_entry(head, element_t, list); list_del_init(head->next); if (sp) memcpy(sp, e->value, strlen(e->value) + 1); return e; } ``` #### `q_remove_tail` 用 `q_remove_head()` 實作,傳入 `head->prev->prev` ,其取得的節點便是 `head->prev` 。 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { return q_remove_head(head->prev->prev, sp, bufsize); } ``` #### `q_size` 一開始檢查佇列是否為 `NULL` 或空。 使用 `list_for_each()` 走訪佇列,計算節點數量,並回傳。 ```c int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; struct list_head *node; int count = 0; list_for_each (node, head) ++count; return count; } ``` #### `q_delete_mid` > 根據[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#circular-linked-list)中的範例實作 一開始檢查佇列是否為 `NULL` 或空。 使用快慢指標找到中間的節點。 - `indir` 指標一個迴圈走到下個節點(一個節點), `fast` 指標一個迴圈走到下下個節點(兩個節點)。 - 當 `fast = head` (q_size is even) 或 `fast->next = head` (q_size is odd) 時,fast 已走 n 個節點, `*indir` 指向第 $n/2$ 節點,即為中間的節點 (0-based indexing)。 - 若 q_size = 6, `fast` 停在 5th 節點,`*indir` 指向 3th 節點 。 - 若 q_size = 5, `fast` 停在 3th 節點,`*indir` 指向 2th 節點 。 - `list_del()` 移除要刪除的節點,用 `list_entry()` 找到節點位置,再釋放空間。 ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head **indir = &head->next, *fast = head->next; for (; fast != head && fast->next != head; fast = fast->next->next) indir = &(*indir)->next; struct list_head *del_node = *indir; list_del_init(del_node); element_t *del_e = list_entry(del_node, element_t, list); q_release_element(del_e); return true; } ``` #### `q_delete_dup` 若佇列為 `NULL` 回傳 `false` 。 用 `list_for_each_entry_safe()` ,因為它允許在走訪佇列時刪除節點。 - 若是 `str` 與 `node->value` 字串相同。移除 `node` 並釋放空間。否則將 `node->vale` 複製給 `str` ,成為下一個要比較的字串。 - 因為使用此函式的佇列須為已排序好的狀態,有相同字串的節點一定相鄰。一旦發現下個節點字串不同,目前節點的字串便不會再後續出現,不須再比較。 - 經由迴圈,會將 n 個有相同字串的節點的後 n-1 個節點刪除,因為第一次偵測到存在相同字串 `str` 時,同時至少有兩個節點,前一個是更新 `str` 時走訪,目前的節點是發現重複字串且將被刪除的節點,後續重複節點也被走訪後刪除。 為了刪除第一個節點,再刪除目前節點後檢查 `str` 和 `next->value`,若是**不同**,表示重複的 n-1 個節點已被刪除完畢,此時 `next` 的前一個節點就是重複的第一個節點(兩節點之前的其他節點都被移除),用 `list_entry()` 取得該節點 `first_dup` ,移除並釋放空間。 - 變數 - `str` 存目前比要對的字串。 - `node` 目前的節點,可被 delete 。 - `next` , `node` 的下一個節點。 - `first_dup` , n 個有相同字串的節點中的第一個節點。 ```c bool q_delete_dup(struct list_head *head) { if (!head) return false; element_t *first_dup, *node, *next; char *str = malloc(sizeof(char) * 1024); list_for_each_entry_safe (node, next, head, list) { if (strcmp(str, node->value) == 0) { list_del_init(&node->list); q_release_element(node); if (strcmp(str, next->value) != 0) { /* delete the frist node that have duplicate string */ first_dup = list_entry(next->list.prev, element_t, list); list_del_init(&first_dup->list); q_release_element(first_dup); } } else { memcpy(str, node->value, strlen(node->value) + 1); } } free(str); return true; } ``` 改進: 將 `str` 指標指向目前比對的節點的 `value` ,取代原本的 `malloc` 和 `memcpy` ,減省記憶體空間。初始化則改為指向 `\0` 。 ```c bool q_delete_dup(struct list_head *head) { if (!head) return false; element_t *node, *next; char *str = "\0"; list_for_each_entry_safe (node, next, head, list) { if (strcmp(str, node->value) == 0) { ... if (strcmp(str, next->value) != 0) { /* delete the frist node that have duplicate string */ element_t *first_dup = list_entry(next->list.prev, element_t, list); list_del_init(&first_dup->list); q_release_element(first_dup); str = "\0"; } } else { str = node->value; } } return true; } ``` #### `q_swap` 若佇列為 `NULL` 、空,或只有一個節點,則回傳,不須 swap 。 根據 `list_for_each_safe` 迴圈,加上 `safe != head` 的條件,用來在節點為奇數時,剩下最後一個時停止迴圈。 交換節點位置: - `safe` 後移一個節點,原本 `safe` 的位置為 `node->next` , 交換 `node` 和 `node->next` 兩節點。 - 更新連接 `node->prev` 和 `node->next` 。 - 更新連接 `node` 和 `node->next` 。 - 更新連接 `node` 和 `safe` 。 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *node, *safe; for (node = head->next, safe = node->next; node != head && safe != head; node = safe, safe = node->next) { safe = safe->next; node->prev->next = node->next; node->next->prev = node->prev; node->prev = node->next; node->next->next = node; safe->prev = node; node->next = safe; } } ``` #### `q_reverse` 確認佇列是否為 `NULL` 或空。 用 `list_for_each_safe` 走訪節點,交換 `node` 的兩個指標 `node->prev` `node-。next` 指向的位址。因為 `list_for_each_safe` 是從第一個節點開始,迴圈結束後交換 `head` 的兩個指標 `prev` `next` 指向的位址(此時 `node == head`)。 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *node, *next; list_for_each_safe(node, next, head) { node->next = node->prev; node->prev = next; } node->next = node->prev; node->prev = next; return; } ``` #### `q_sort` 若佇列為 `NULL` 、空或只有一個節點,則不做排序。 根據 Quick sort 實作排序,將傳入的第一個節點 `head` 當作比較的基準值,數值小的節點移動到 `head` 前,數值相同或大於則維持原位。再用遞迴繼續排序 `head` 的左右子佇列。 用 `prev` 和 `next` 兩個指標的指標操作。 - 立即回傳,不做迴圈 - `tail->next == head` 表示上一層遞迴後左子佇列或右佇列為空。 - `head == tail` 表示上一層遞迴後左子佇列或右佇列只有移的節點。 - 迴圈 - 初始 `prev` `next` 都指向 `head` 。 - 若 `*next->next` `value` 小於 `head` `value` ,移到 `*prev` 節點的前面,然後 `prev` 指標指向該節點。 - 否則(`*next->next` `value` 大於等於 `head` `value`), `next` 指向下一個節點。 - 若 `*next == tail` (`*prev == tail`) 表示此子佇列已比較完所有節點且最後一個節點大於等於(小於)`head` 。 - 遞迴呼叫 - `prev` 和 `next` 再比較後都會指向比較後的節點,因此分別作為左子佇列的 `head` 和右子佇列 `tail` 。 ```c void sort(struct list_head *head, struct list_head *tail) { if (tail->next == head || head == tail) return; struct list_head **prev = &head, **next = &head; char *head_str = list_entry(head, element_t, list)->value; for (; (*next) != tail && (*prev) != tail;) { char *str = list_entry((*next)->next, element_t, list)->value; if (strcmp(str, head_str) < 0) { (*prev)->prev->next = (*next)->next; (*next)->next->prev = (*prev)->prev; (*prev)->prev = (*next)->next; (*next)->next = (*next)->next->next; (*next)->next->prev = *next; (*prev)->prev->next = *prev; prev = &(*prev)->prev; } else { next = &(*next)->next; } } sort(*prev, head->prev); sort(head->next, *next); return; } void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; sort(head->next, head->prev); } ``` ## 參考資訊 - 共筆格式參考:[共筆示範](https://hackmd.io/@cccccs100203/linux2020-lab0)