--- title: 2023q1 Homework1 (lab0) tags: Linux核心實作 --- # 2023q1 Homework1 (lab0) contributed by < [`lorian0738`](https://github.com/lorian0738) > ### Reviewed by `zeddyuu` * 開發過程中的編譯器不應該是 Visual Studio Code,應該是整合開發環境 * 有些函式只有部份程式碼,建議可以貼上整段的程式碼再開始說明會比較完整且可讀 * 快慢指標用圖片說明很好,但建議可以用 Graphviz * 完整說明改善程式碼前後的想法差異,很棒 * 一些部份尚未完成,像洗牌操作以及論文閱讀等等 ## 開發環境 ```bash $ 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): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz CPU family: 6 Model: 158 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 9 CPU max MHz: 4200.0000 CPU min MHz: 800.0000 BogoMIPS: 7200.00 ``` ## 開發過程 ### 前置作業 #### 檢查 Linux 版本 原本想用 `lsb_release` 檢查,但是我裝的版本沒有這個指令 因此改用 `cat /etc/*-release` 得 `VERSION_ID="22.04"` 確認符合作業需求 #### github 設定 參照 [Git 教學和 GitHub 設定指引](https://hackmd.io/@sysprog/gnu-linux-dev/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Fgit-with-github#Git-%E6%95%99%E5%AD%B8%E5%92%8C-GitHub-%E8%A8%AD%E5%AE%9A%E6%8C%87%E5%BC%95) 建立 GitHub 帳號與產生 SSH key 建立完公鑰下指令 `clip < ~/.ssh/id_rsa.pub` 時沒有這個指令 於是用 `sudo apt install geomview` 下載 然而還是無法取得公鑰 最後輸入 `cat ~/.ssh/id_rsa.pub` 再複製 (註: `git commit -a` 將所有改過的 ~~push 到 GitHub 上~~ commit 出去建立一個新的 git 版本 `git log` 看曾經 ~~push~~ commit 過的情況 也可以用 `tig` 看更詳細的 最後用 `git push -u origin master` push 到 Github 上) #### 檢查 cppcheck 版本 指令:`cppcheck --version` 確認 Cppcheck 2.7 符合規定 #### 取得程式碼 至 [lab0-c](https://github.com/sysprog21/lab0-c) fork 到自己的GitHub 再照要求 clone 下來 `git clone git@github.com:lorian0738/lab0-c` #### 編譯器 使用 Visual studio Code ### q_new > 建立新的「空」佇列 先要 list_head 的空間,如果沒有要到就 return 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; } ``` ### q_free > 釋放佇列所佔用的記憶體 判斷若為 NULL 則直接 return,若為 empty則釋放 list_head 佔用的空間 否則利用 `list_for_each_entry_safe` 和 `q_release_element` 逐一釋放每個 node 佔用的空間 最後要記得把 head 的空間也釋放掉 ```c void q_free(struct list_head *l) { if (!l) return; if (list_empty(l)) { free(l); return; } element_t *entry = NULL, *safe = NULL; list_for_each_entry_safe (entry, safe, l, list) q_release_element(entry); free(l); } ``` ### q_insert_head > 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則) 原本的寫法: ```c bool q_insert_head(struct list_head *head, char *s) { element_t *entry = (element_t *) malloc(sizeof(element_t)); if (!entry) return false; entry->value = s; list_add(&entry->list, head); return true; } ``` 但發現忘記應該要先複製要加入的字串,否則原本的資料更動的時候會動到entry中儲存的資料,改寫成: ```c bool q_insert_head(struct list_head *head, char *s) { element_t *element = (element_t *) malloc(sizeof(element_t)); char *tmp = (char *) malloc(sizeof(char) * (strlen(s) + 1)); if (!element || !tmp) return false; strncpy(tmp, s, strlen(s) + 1); element->value = tmp; list_add(&element->list, head); return true; } ``` 其中直接用 `strncpy` 複製可確保複製到 buffer 可容納大小且自動在結尾補上 '\0' 後來發現應該要在一開始檢查 queue 是否為 NULL ,因此在最前面補上 ```c if (head == NULL) return false; ``` 且在 make test 時發現有 malloc 的錯誤,檢查後發現目前的程式有可能在要 element_t 的空間時有要到,但是在要字串的空間時失敗回傳false,前面要的空間應該要被釋放掉,應分開判斷是否有要到空間改為如下: ```c if (!element) return false; if (!tmp) { free(element); return false; } ``` 此步將原本 `7 blocks are still allocated` 降為 4 ( insert_tail 的部份從 9 降到 4 ),但還是有空間沒有釋放,才想到也有可能是前面 element_t 就沒有要到空間了,但繼續做下去才檢查的話有可能後面的字串有要到空間,這時候就會沒有釋放到字串的空間,因此應該換個順序: ```c element_t *element = (element_t *) malloc(sizeof(element_t)); if (!element) return false; char *tmp = (char *) malloc(sizeof(char) * (strlen(s) + 1)); if (!tmp) { free(element); return false; } ``` 才解決這個問題 :::info 在 commit 的時候原本想寫 Fix malloc failure on insert_head and insert_tail 但 malloc 不合法,必須寫完整的 memory allocation 才可以 改完後超過上限 50 個 characters 因此最後提交 Fix memory allocation failure on insert ::: ### q_insert_tail > 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則) 和 q_insert_head 差不多,差別在於最後以 `list_add_tail` 加到最後 ```c list_add_tail(&element->list, head); ``` ### q_remove_head > 在佇列開頭 (head) 移去 (remove) 給定的節點 在 queue.h 的檔案中: ``` * @head: header of queue * @sp: string would be inserted * @bufsize: size of the string * * If sp is non-NULL and an element is removed, copy the removed string to *sp * (up to a maximum of bufsize-1 characters, plus a null terminator.) ``` 第二行有點看不懂 sp 是什麼,註解和前面做 insert 時的 s 一樣,但這裡應該沒有要將字串插入,所以有點困惑 但第五行的意思推測是要將移除的字串複製過去 ``` Return: the pointer to element, %NULL if queue is NULL or empty ``` 首先判斷 queue 是否為 NULL 或空 ```c if (!head || list_empty(head)) return NULL; ``` 接著用 `list_first_entry` 取出開頭的 entry, 並用 `list_del` 移走該點 ```c element_t *element = (element_t *) malloc(sizeof(element_t)); element = list_first_entry(head, element_t, list); list_del(head->next); ``` 最後判斷 sp 是否不是 NULL 以做複製,再回傳 element ```c if (sp != NULL) { strncpy(sp, element->value, bufsize -1); sp[bufsize - 1] = '\0'; } return element; ``` 但後來發現其實 `list_del` 不會釋放空間,取出開頭時不需要要一個新的空間,應該直接寫成以下就好 ```c element_t *element = list_first_entry(head, element_t, list); ``` ### q_remove_tail > 在佇列尾端 (tail) 移去 (remove) 給定的節點 作法和 `remove_head` 差不多,僅改用 `list_last_entry` 取出最後一個entry,且刪除節點時刪的是 head->prev 也就是最後一點 ### q_size > 計算目前佇列的節點總量 利用 `list_for_each` 跑過 list 且每跑一個 node 長度就加一 ```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 > 移走佇列中間的節點,詳見 [LeetCode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) 確定 list 不是 NULL 或 empty 後,用前面寫過的 q_size 取得 list 的長度 ```c int mid = q_size(head) / 2; ``` 接著以 list_for_each_entry_safe 跑 entry 到中間的時候做 delete 和 release ```c element_t *entry = NULL, *safe = NULL; list_for_each_entry_safe (entry, safe, head, list) { if (mid == 0) { list_del(&entry->list); q_release_element(entry); return true; } mid = mid - 1; } return false; ``` ### q_delete_dup > 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/) 首先判斷 list 若為 NULL 則回傳 false 而若 list 為 empty 或只有一點,就直接回傳true ```c if (!head) return false; if (list_empty(head) || list_is_singular(head)) return true; ``` 接著利用 `list_for_each_entry_safe` 走訪 list,將下一個 entry 的 value 位址紀錄在 tmp,若現在的值和下一個一樣表示需要 release,且紀錄 delete 為 1,這樣走到有重複的最後一個 entry 時才會記得需要 release 而寫到這裡的時候有點混亂,有許多語法上的錯誤,以下紀錄。 原本的寫法 part 1: ``` element_t *entry = NULL, *safe = NULL; char *tmp = NULL; bool delete = 0; list_for_each_entry_safe (entry, safe, head, list) { if (&entry->list.next == head) return true; ``` 當下一個為 head 就直接 return 了,忽略了最後一個 entry 可能跟前面重疊到所以也需要刪除,應該確認 delete 是否為 0 再 return 修正: ```c if (entry->list.next == head) { if (delete) { list_del(&entry->list); q_release_element(entry); return true; } else { return true; } } ``` 原本的寫法 part 2: ``` *tmp = container_of(&entry->list.next, element_t, list)->value; if (strcmp(&entry->value, tmp) == 0) { delete = 1; list_del(&entry->list); q_release_element(entry); } else if (delete) { delete = 0; list_del(&entry->list); q_release_element(entry); } } return true; ``` value 本身就是一個指向字串的指標了 tmp 也是指標,直接用 = 就好 在做字串比較的時候也不需要再取址 修正: ```c tmp = container_of(entry->list.next, element_t, list)->value; ``` ```c if (strcmp(entry->value, tmp) == 0) { ``` ### q_swap > 交換佇列中鄰近的節點,詳見 [LeetCode 24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/) 利用 list_for_each_entry_safe 每跑兩個 node 就將該 node 移到前一個的前面 ```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; element_t *entry = NULL, *safe = NULL; bool even = 0; list_for_each_entry_safe (entry, safe, head, list) { if (even) { list_move(&entry->list, entry->list.prev); } even = !even; } return; } ``` 修正:`list_move` 是把 node 移到指向 node 的後面,所以這樣什麼都不會改動,應該要用 `list_move_tail` ### q_reverse > 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點 在 queue.h 中: ``` * This function should not allocate or free any list elements * (e.g., by calling q_insert_head, q_insert_tail, or q_remove_head). ``` 但 q_remove_head 也沒有 allocate 或 free 所以覺得有點困惑 但總之是要將 list 整個顛倒過來 因此需要將第一個和最後一個對調、第二個和倒數第二個對調......以此類推。 首先判斷 list 若為 NULL 或 empty 或只有一個節點就直接 return 接著用 left 和 right 的指標分別指向第一點、最後一點,並往右、左跑 list 以while迴圈持續交換 left 和 right 指到的點,直到指到同一個點或是相鄰的點 ```c while (left != right) { if (left->next == right) { list_move(left,right); break; } tmp = left->prev; list_move(left,right); list_move(right,tmp); tmp = left->prev; left = right->next; right = tmp; } ``` ### q_reverseK > 類似 q_reverse,但指定每 k 個節點為一組,進行反向順序排列,詳見 [LeetCode 25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/) 若 head 為 NULL 或 empty 或只有一個節點或 k 為一就直接 return 接著跑迴圈並紀錄當跑了 k 個節點就需要做 reverse  ```c struct list_head *node = NULL, *safe = NULL; struct list_head *left = head->next; struct list_head *right; struct list_head *tmp; int count = 0; list_for_each_safe (node, safe, head) { count = count + 1; if (count == k) { count = 0; right = node; while (left != right) { if (left->next == right) { list_move(left, right); break; } tmp = left->prev; list_move(left, right); list_move(right, tmp); tmp = left->prev; left = right->next; right = tmp; } left = safe; } } ``` ~~跑完後有可能最後幾個加起來不到 k 個節點的沒有做 reverse 要再把它做完~~ **更新:重看一次題目發現剩餘的不用做,也就是以下不需要** ```c if (left != head) { right = head->prev; while (left != right) { if (left->next == right) { list_move(left, right); break; } tmp = left->prev; list_move(left, right); list_move(right, tmp); tmp = left->prev; left = right->next; right = tmp; } } ``` 目前的寫法過於冗長,需要再想想更精簡的作法 ### q_sort > 以遞增順序來排序鏈結串列的節點,可參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 得知實作手法 看過文章後選擇用複雜度為 O(nlogn) 的 merge sort 方法實作 #### 原先的想法(有許多錯誤): 一樣判斷可以直接 return 的狀況,並呼叫 mergesort_list 做 divide and conquer 中 divide 的部份 `mergesort_list` ```c void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) { return; } head = mergesort_list(head); } ``` 找出中間點並拆成兩個 list,遞迴下去直到拆不了,再呼叫 `mergeTwoLists` 將兩兩合併 ```c struct list_head *mergesort_list(struct list_head *head) { if (!head || !head->next) return head; // let right points to midle struct list_head *left = head->next; struct list_head *right = head->prev; while (left != right) { if (left->next == right) { break; } left = left->next; right = right->prev; } // cut the link of list left->next = NULL; head->prev->next = NULL; left = mergesort_list(head), right = mergesort_list(right); return mergeTwoLists(left, right); } ``` 參考 [案例探討: LeetCode 21. Merge Two Sorted Lists](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists) 使用指標的指標的方法 覺得這樣的寫法很不可思議,需要仔細思考並不搞混每個指標指在哪裡 而其中指標類型 uintptr_t 定義在 stdint.h 之中,因此加了標頭檔 `#include <stdint.h>` ```c struct list_head *mergeTwoLists(struct list_head *L1, struct list_head *L2) { struct list_head *head = NULL, **ptr = &head, **node; for (node = NULL; L1 && L2; *node = (*node)->next) { node = strcmp(list_entry(L1, element_t, list)->value, list_entry(L2, element_t, list)->value) < 0 ? &L1 : &L2; *ptr = *node; ptr = &(*ptr)->next; } *ptr = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2); return head; } ``` #### 修正: > 觀摩[同學的做法](https://hackmd.io/@wanghanchi/linux2023-lab0#q_sort)後發現想法錯誤的地方 1. 找中間點的方法錯誤:原本想和前面的做法一樣從尾端和開頭開始往中間找中間點,但是在 merge 的過程中只會將next指向正確的下一個節點,而下一個節點的 prev 不會更新到正確的上一個 node,所以不能用這種方法找中間節點,應該要用快慢指標找 快慢指標(極度陽春版,若有學會如何製作更精美的版本再更新): ![](https://i.imgur.com/y85fb0N.jpg) ![](https://i.imgur.com/EDDq5pR.jpg) ```c struct list_head *slow = head; for (struct list_head *fast = head; fast && fast->next; fast = fast->next->next) slow = slow->next; struct list_head *mid = slow; ``` 2. 沒有串好 circular linked-list:一樣因為過程的 prev 指標都是錯誤的,所以需要在 q_sort 做完 mergesort_list 後更新正確的 prev 指標 ```c struct list_head *p = head, *n = head->next; for(; n != NULL; n = n->next) { n -> prev = p; p = n; } p->next = head; head->prev = p; ``` 3. 為遞迴方便,mergesort_list 傳入的 head 應直接指向第一個 node ```c head->next = mergesort_list(head->next); ``` ### q_descend > 參閱 [LeetCode 2487. Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/) 首先判斷 NULL 或 empty 直接回傳長度為 0 而若只有一個 node 則直接回傳 1 ```c if (!head || list_empty(head)) { return 0; } else if (list_is_singular(head)) { return 1; } ``` 因為直接做不好做,可以將 list 做 reverse,接著紀錄沿途遇到的最大值,若在其後有較小的值則刪除,最後再做一次 reverse 即可 ```c q_reverse(head); char *max = list_first_entry(head, element_t, list)->value; element_t *entry = NULL, *safe = NULL; list_for_each_entry_safe (entry, safe, head, list) { if (strcmp(entry->value, max) < 0) { list_del(&entry->list); } else { max = entry->value; } } q_reverse(head); return q_size(head); ``` 但 make test 可以發現有錯,訊息: ``` ERROR: There is no queue, but 4 blocks are still allocated ERROR: Freed queue, but 4 blocks are still allocated ``` 看起來是需要釋放空間,雖然題目寫的是 remove,於是做完 list_del 之後加了: ```c free(entry); ``` 但這樣還是有沒釋放到的空間,發現 entry->value 沒有釋放到,改成以下: ```c q_release_element(entry); ``` ### q_merge > 合併所有已排序的佇列,並合而為一個新的已排序佇列,詳見 [LeetCode 23. Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/) 在 queue.h 中可見關於 q_merge: ``` * @head: header of chain ``` 而 chain 被用在 queue_contex_t 中,被用來將許多 queue 的 head 串在一起 ```c /** * queue_contex_t - The context managing a chain of queues * @q: pointer to the head of the queue * @chain: used by chaining the heads of queues * @size: the length of this queue * @id: the unique identification number */ typedef struct { struct list_head *q; struct list_head chain; int size; int id; } queue_contex_t; ``` 而先前有做過 q_sort,因此先將每個 queue 合在一起,再做 sort 即可 首先若 chain 為 NULL 或 empty 回傳 0 接著原本和上面一樣想著若為 singular 回傳 1,但這裡的 head 指的是 chain 的 head,就算是 singular 也有可能有一串 queue 所以不能這樣寫 ```c if (!head || list_empty(head)) { return 0; } ``` 接著以 list_for_each_entry 走訪 chain,將每個 queue 接上第一個 queue for 迴圈中也是從第一個 entry 開始走,一開始沒有注意到直接做 `list_splice_init` ,導致重複接了第一個 queue,應該要避免 ```c queue_contex_t *new_head = list_first_entry(head, queue_contex_t, chain); queue_contex_t *entry = NULL; list_for_each_entry (entry, head, chain) { if (entry == new_head) continue; list_splice_init(entry->q, new_head->q); } ``` 再進行 sort 並回傳大小 ```c q_sort(new_head->q); return q_size(new_head->q); ``` ### make test 沒有皮卡丘 目前分數:95/100 constant time 沒有過 ## 以 [Valgrind](https://valgrind.org/) 分析記憶體問題 ``` $ make valgrind ``` 這邊測出 `--- TOTAL 100/100` 最後兩行: ``` Test with specific case by running command: scripts/driver.py -p /tmp/qtest.6Sd6EX --valgrind -t <tid> ```