# 2023q1 Homework1 (lab0) contributed by < [willow11235811](https://github.com/willow11235811) > ## 開發環境 ```shell $ paleofetch OS: Arch Linux x86_64 Host: X570 AORUS ELITE -CF Kernel: 6.1.12-arch1-1 Uptime: 3 hours, 41 mins Battery: Not found Packages: 1791 (pacman) Shell: zsh Resolution: 1920x1080 Terminal: st-256color CPU: AMD Ryzen 7 5700X 8- Processor (16) @ 4.661GHz Memory: 2850MiB / 32023MiB (8%) $ gcc --version gcc (GCC) 12.2.1 20230201 ``` ## :notebook: 開發過程 - [x] 實作 `queue.c` - [ ] 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),修正 `qtest` 執行過程中的錯誤 - [ ] 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗 - [ ] 精簡 ```q_delete_dup``` - [ ] 思考其他 ```q_merge``` 實作方式 - [ ] 研讀 Linux 核心原始程式碼的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試引入到 `lab0-c` 專案 :::spoiler 比較你自己實作的 merge sort 和 Linux 核心程式碼之間效能落差 ::: - [ ] 確保 `qtest` 提供的 `web` 命令得以搭配上述佇列實作使用 :::spoiler 目前一旦網頁伺服器啟動後,原本處理命令列操作的 `linenoise` 套件就無法使用,請提出改善機制 * 提示: 使用 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫 ::: - [ ] 在 `qtest` 提供新的命令 `shuffle` :::spoiler 允許藉由 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作,需要以統計的原理來分析你的實作,探究洗牌的「亂度」 ::: - [ ] 在 `qtest` 中執行 `option entropy 1` 並搭配 `ih RAND 10` 一類的命令 :::spoiler 觀察亂數字串的分布,並運用「資訊與熵互補,資訊就是負熵」的觀念,設計一組新的命令或選項,得以在 `qtest` 切換不同的 PRNG 的實作 (可選定 [Xorshift](https://en.wikipedia.org/wiki/Xorshift)),搭配統計分析工具,比較亂數產生器 (如 Xorshift vs. 內建) 的品質 * 參見: [由大數法則理解訊息熵 (entropy) 的數學意義](https://hackmd.io/@8dSak6oVTweMeAe9fXWCPA/ryoegPgw_) * 參見: [The Linux Pseudorandom Number Generator Revisited](https://eprint.iacr.org/2012/251.pdf) ::: - [ ] 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉 :::spoiler 解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 [Student's t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 及程式實作的原理。 * 注意:現有實作存在若干致命缺陷,請討論並提出解決方案 * 在 [oreparaz/dudect](https://github.com/oreparaz/dudect) 的程式碼具備 percentile 的處理,但在 [lab0-c](https://github.com/sysprog21/lab0-c) 則無。 ::: - [ ] 指出現有程式的缺陷或者可改進之處 (例如更全面地使用 Linux 核心風格的鏈結串列 API),嘗試實作並提交 pull request - [ ] 詳閱第 1 週所有教材及作業描述 [(一)](/@sysprog/linux2023-lab0-a), [(二)](/@sysprog/linux2023-lab0-b), [(三)](/@sysprog/linux2023-lab0-c), [(四)](/@sysprog/linux2023-lab0-d), [(五)](/@sysprog/linux2023-lab0-e),記錄你的啟發和疑惑 - [ ] 解釋 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作 :::spoiler 說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。可對照參考 [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/H1TtmVTTz) ::: ## 實作 queue.c > Score: 95 / 100 ### q_new 利用 `malloc` 配置記憶體。 若配置失敗先釋放記憶體空間,並回傳 ```NULL``` 值。 若成功配置,呼叫 ```list.h``` 的 ```INIT_LIST_HEAD``` 進行初始化。 > commit: [85b8cd1](https://github.com/willow11235811/lab0-c/commit/85b8cd13dfcf449faa1f8fe457bbdfded310c423) :::danger 不用張貼完整程式碼。 :notes: jserv > 已修改 ::: ### q_free 先檢查是否為 ```NULL``` 佇列或是空的佇列, 利用 ```list.h``` 提供的 ```list_for_each_safe``` 走訪所有節點, 再使用 ```q_release_element``` 釋放節點內的所有 element。 commit: [325c1fe](https://github.com/willow11235811/lab0-c/commit/325c1fe16620b797587000de97805c7af608a008) :::spoiler ```c void q_free(struct list_head *l) { if (!l) return; if (list_empty(l)) { free(l); return; } struct list_head *node, *save; list_for_each_safe(node, save, l) q_release_element(container_of(node, element_t, list)); free(l); } ``` ::: ### q_insert_head 利用 ```list.h``` 提供的鏈結串列巨集和函式,可管理鏈結串列, 重點在於實作出新的節點和管理指標與記憶體。 藉由 ```malloc``` 和 ```strncpy``` 來獲取新的記憶體位置。 commit: [aae2f1c](https://github.com/willow11235811/lab0-c/commit/aae2f1c1f3bea14bde7b4419063f4ff1e6f735b0) :::spoiler ```c bool q_insert_head(struct list_head *head, char *s) { if (!head || !s) return false; element_t *new_ele = malloc(sizeof(element_t)); if (!new_ele) { free(new_ele); return false; } size_t len = strlen(s) + 1; char *new_str = malloc(len * sizeof(char)); if (!new_str) { free(new_str); free(new_ele); return false; } strncpy(new_str, s, head); new_ele->value = new_str; list_add(&new_ele->list, head); return true; } ``` ::: ### q_insert_tail 與 ```q_insert_head``` 作法相同,改使用 ```list_add_tail``` 實作。 commit: [f04edd1](https://github.com/willow11235811/lab0-c/commit/f04edd1f3cc38f2996ce8701273df36da51b5f50) :::spoiler ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head || !s) return false; element_t *new_ele = malloc(sizeof(element_t)); if (!new_ele) { free(new_ele); return false; } size_t len = strlen(s) + 1; char *new_str = malloc(len * sizeof(char)); if (!new_str) { free(new_str); free(new_ele); return false; } strncpy(new_str, s, len); new_ele->value = new_str; list_add_tail(&new_ele->list, head); return true; } ``` ::: ### q_remove_head 要移除佇列的頭部且回傳被移除的元素,同時要將元素的字串資料儲存。 利用 ```list.h``` 提供的 ```container_of``` 可以找到頭部元素,並使用 ```list_del``` 移除元素,使用 ```strncpy``` 複製字串資料,並在最後加上 null terminator 。 commit: [7d4331c](https://github.com/willow11235811/lab0-c/commit/7d4331c7878ec2ac89dbde4dcbbaaa51d9767f8a), [ca27194](https://github.com/willow11235811/lab0-c/commit/ca27194a831fcd0f553c587dba21ad41ff295a0e) :::spoiler ```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 = container_of(head->next, element_t, list); if (sp && bufsize) { strncpy(sp, tmp->value, bufsize - 1); *(sp + bufsize - 1) = '\0'; } list_del(head->next); return tmp; } ``` ::: ### q_remove_tail 與 q_remove_head 作法相同,改尋找尾部。 commit: [7aad974](https://github.com/willow11235811/lab0-c/commit/7aad974e1b4ca3cc0010ea2ff66d182bc1a9a3db), [ca27194](https://github.com/willow11235811/lab0-c/commit/ca27194a831fcd0f553c587dba21ad41ff295a0e) :::spoiler ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *tmp = container_of(head->prev, element_t, list); if (sp && bufsize) { strncpy(sp, tmp->value, bufsize - 1); *(sp + bufsize - 1) = '\0'; } list_del(head->prev); return tmp; } ``` ::: ### q_size 利用 ```list.h``` 提供的 ```list_for_each``` 走訪所有節點,這個實作的時間複雜度是 $O(N)$ 。 commit: [693b886](https://github.com/willow11235811/lab0-c/commit/693b886ca63fdffcd209fe2242fcdbfdab3f172b) :::spoiler ```c int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; int size = 0; struct list_head *count = NULL; list_for_each (count, head) size++; return size; } ``` ::: ### q_delete_mid 利用快慢指標演算法 (Tortoise and Hare Algorithm) ,藉由快指標:慢指標移動速度2:1,當快指標走完整個 linked list 時,慢指標會到達中點。 commit: [17573d1](https://github.com/willow11235811/lab0-c/commit/17573d14933fb79049c6c29cbea39916b7ca7e0a) :::spoiler ```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 *fast = head->next, *slow = head->next; while (fast != head && fast->next != head) slow = slow->next, fast = fast->next->next; list_del(slow); q_release_element(list_entry(slow, element_t, list)); return true; } ``` ::: ### q_delete_dup 一開始想利用 ```list_for_each_safe``` 走訪所有節點,但根據定義只允許刪除 ```node``` 節點,其他操作都可能造成 undefined behavior 。 所以利用 ```tmp``` 走訪 ```save``` 到尾段的節點,只要發現 ```node``` 與 ```tmp``` 有相同的 ```value``` ,就刪除 ```node``` 這個節點。 再利用 ```break``` 跳脫至最外層的 ```list_for_each_safe``` 繼續走訪剩下節點。 commit: [930e5db](https://github.com/willow11235811/lab0-c/commit/930e5db42ec4fe94345cbcc709a6f90f3491a50f) :::spoiler Version 1 ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head) return false; struct list_head *node, *save, *tmp; list_for_each_safe (node, save, head) { for (tmp = save; tmp != head; tmp = tmp->next) if (!strcmp(list_entry(node, element_t, list)->value, list_entry(tmp, element_t, list)->value)) { list_del(node); q_release_element(list_entry(node, element_t, list)); break; } } return true; } ``` ::: :::danger :bug: Bugs 隨後仔細閱讀 ```queue.h``` 發現 ```q_delete_dup``` 的要求是刪除「所有」重複的節點, Version 1 會留下1個節點。 ::: #### Debug 加入 ```is_dup``` 判斷是否有連續的節點重複,但程式碼應該還能再精簡。 commit: [03d9191](https://github.com/willow11235811/lab0-c/commit/03d91915a6151531ec3aa3532ea86d60622e33f2) :::info :notebook_with_decorative_cover: ToDo 精簡 ```q_delete_dup``` ::: :::spoiler Version 2 ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head) return false; struct list_head *node, *save; bool is_dup = false; list_for_each_safe (node, save, head) { if (node->next != head && !strcmp(list_entry(node, element_t, list)->value, list_entry(save, element_t, list)->value)) { is_dup = true; list_del(node); q_release_element(list_entry(node, element_t, list)); } else if (is_dup) { is_dup = false; list_del(node); q_release_element(list_entry(node, element_t, list)); } } return true; } ``` ::: ### q_swap 利用 indirect pointer 的技巧,直接操作要互換節點的指標。 commit: [25817e5](https://github.com/willow11235811/lab0-c/commit/25817e521436b7196bafcc8c5a7abe768220b7bf) :::spoiler Version 1 ```c void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ if (!head) return; for (struct list_head **l = &head->next; *l != head && (*l)->next != head; l = &(*l)->next) { struct list_head *first = head, *second = head->next; first->prev = second; first->next = second->next; second->prev = first->prev; second->next = first; *l = first; } } ``` ::: :::danger :bug: Bugs 1. 出現無限迴圈 2. 指標操作應可用 ```list.h``` 提供的程式精簡 ::: #### Debug 使用 ```list_move``` 增加可讀性,並避免無限迴圈。 commit: [b9be753](https://github.com/willow11235811/lab0-c/commit/b9be753f88c2f9287de409f06061cf869b2aa8ea) :::spoiler Version 2 ```c void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ if (!head) return; struct list_head *node, *save; for (node = head->next, save = node->next; node != head && save != head; node = node->next, save = node->next) { list_move(node, save); } } ``` ::: ### q_reverse ```list.h``` 提供的 ```list_move``` 可將節點移到頭部,但操作時會刪除節點, 因此使用 ```list_for_each_safe``` 走訪所有節點。 commit: [df3a1e7](https://github.com/willow11235811/lab0-c/commit/df3a1e7c66323a697abfdff7195883236756657e) :::spoiler ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *node, *save; list_for_each_safe (node, save, head) list_move(node, head); } ``` ::: ### q_reverseK 要反轉 $K$ 個元素,會使用到 ```list.h``` 提供的 ```list_cut_position``` 和 ```list_splice``` 來切斷與接合反轉後的佇列。 先讓 ```node``` 前進 $K-1$ 個節點,再利用 ```tmp``` 作為儲存切斷後佇列的 buffer ,並反覆迭代直到尾部。 commit: [9070c81](https://github.com/willow11235811/lab0-c/commit/9070c817252848a832b72c20c6227eba072cfedc) :::spoiler Version 1 ```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 i = k; struct list_head *node, *save, *cut = head; list_for_each_safe (node, save, head) { if (--i) continue; i = k; LIST_HEAD(tmp); list_cut_position(&tmp, cut, head); q_reverse(&tmp); list_splice(&tmp, cut); cut = save->prev; } } ``` ::: :::danger :bug: Bugs 雖可執行,但反轉後的佇列順序不對。 > 提交 `qtest` 檢查機制的強化改進? > :notes: jserv ::: #### Debug ```list_cut_position``` 應使用 ```node``` 帶入,否則切點不對。 commit: [c2b25d1](https://github.com/willow11235811/lab0-c/commit/c2b25d1c229d9ec2700924b4f54e0f612cbe7a35) :::spoiler Version 2 ```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 i = k; struct list_head *node, *save, *cut = head; list_for_each_safe (node, save, head) { if (--i) continue; i = k; LIST_HEAD(tmp); list_cut_position(&tmp, cut, node); q_reverse(&tmp); list_splice(&tmp, cut); cut = save->prev; } } ``` ::: ### q_sort 使用 Merge Sort 演算法加上遞迴的形式進行實作。 利用快慢指標演算法 (Tortoise and Hare Algorithm) ,將佇列切成兩段。 在實作 ```merge_list``` 用以合併佇列,參考[@ItisCaleb](https://hackmd.io/@ItisCaleb/S1v9IQpai#q_sort)所實作的函數。 commit: [252b5b7](https://github.com/willow11235811/lab0-c/commit/252b5b7d335b7a1cde4f44f929752fe61a449a17) :::spoiler Version 1 ```c static inline void merge_list(struct list_head *head, struct list_head *left, struct list_head *right) { while (!list_empty(left) && !list_empty(right)) { if (strcmp(list_entry(left->next, element_t, list)->value, list_entry(right->next, element_t, list)->value) < 0) { list_move_tail(left->next, head); } else { list_move_tail(right->next, head); } } if (!list_empty(left)) { list_splice_tail_init(left, head); } else { list_splice_tail_init(right, head); } } void q_sort(struct list_head *head) { /* Using merge sort */ if (!head || list_empty(head) || list_is_singular(head)) return; /* Find the middle point */ struct list_head *fast, *slow; for (fast = head, slow = head; fast != head && fast->next != head;) { fast = fast->next->next; slow = slow->next; } /* Divide into left and right parts */ LIST_HEAD(left); LIST_HEAD(right); list_splice_tail_init(head, &right); list_cut_position(&left, &right, slow); /* Recursion */ q_sort(&left); q_sort(&right); merge_list(head, &left, &right); } ``` ::: :::danger :bug: Bugs 出現無限迴圈 ::: #### Debug 呼叫 ```list_splice_tail_init``` 時會使 header 被初始化,其 ```*prev``` 和 ```*next``` 不會指向第一個節點,破壞 doubly-linked list 的結構。 改使用兩個 ```list_cut_position``` 。 commit: [522d558](https://github.com/willow11235811/lab0-c/commit/522d5584c371eaec91385c820f8abe637bc6c5ee) :::spoiler Version2 ```c static inline void merge_list(struct list_head *head, struct list_head *left, struct list_head *right) { while (!list_empty(left) && !list_empty(right)) { if (strcmp(list_entry(left->next, element_t, list)->value, list_entry(right->next, element_t, list)->value) < 0) { list_move_tail(left->next, head); } else { list_move_tail(right->next, head); } } if (!list_empty(left)) { list_splice_tail_init(left, head); } else { list_splice_tail_init(right, head); } } void q_sort(struct list_head *head) { /* Using merge sort */ if (!head || list_empty(head) || list_is_singular(head)) return; /* Find the middle point */ struct list_head *fast, *slow; fast = head, slow = head; do { fast = fast->next->next; slow = slow->next; } while (fast != head && fast->next != head); /* Divide into left and right parts */ LIST_HEAD(left); LIST_HEAD(right); list_cut_position(&left, head, slow); list_cut_position(&right, head, head->prev); /* Recursion */ q_sort(&left); q_sort(&right); merge_list(head, &left, &right); } ``` ::: ### q_descend 要刪除所有節點,只要該節點右側有比自己更大的 value 。 策略是假設尾部有佇列中的最大值 ```best``` ,從尾部往頭部依序搜尋,只要找到更小的值就刪除該節點。 過程中若遇到比現有最大值更大的 value ,就更新佇列的最大值 ```best``` 。 commit: [7379088](https://github.com/willow11235811/lab0-c/commit/73790880e9da6314a54ab24400e9d21607d0c1fa) :::spoiler ```c int q_descend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || list_empty(head)) return 0; element_t *elem = list_entry(head->prev, element_t, list), *save = list_entry(elem->list.prev, element_t, list); char *best = elem->value; while (&elem->list != head) { if (strcmp(elem->value, best) < 0) { list_del(&elem->list); q_release_element(elem); } else { best = elem->value; } elem = save; save = list_entry(elem->list.prev, element_t, list); } return q_size(head); } ``` ::: ### q_merge 一開始策略是想利用 Merge sort 演算法,由佇列頭部往尾部兩兩合併佇列。 但實作時一直無法實現程式碼,最後參考 [@paul90317](https://hackmd.io/@paul90317/linux2023-spring-lab0#q_merge) 的 ```q_merge``` 實作。 :::info :notebook_with_decorative_cover: ToDo 思考其他 ```q_merge``` 實作方式 ::: commit: [d082344](https://github.com/willow11235811/lab0-c/commit/d08234460d077569ab6f1851db561cc4cd3cd57d) :::spoiler ```c int q_merge(struct list_head *head) { // https://leetcode.com/problems/merge-k-sorted-lists/ if (!head || list_empty(head)) return 0; int n = q_size(head); while (n > 1) { struct list_head *fast = head->next, *slow = head->next; for (int i = 0; i < n / 2; ++i) { LIST_HEAD(temp); merge_list(&temp, container_of(fast, queue_contex_t, chain)->q, container_of(fast->next, queue_contex_t, chain)->q); list_splice_tail(&temp, container_of(slow, queue_contex_t, chain)->q); fast = fast->next->next; slow = slow->next; } if (n & 1) list_splice_tail_init(container_of(fast, queue_contex_t, chain)->q, container_of(slow, queue_contex_t, chain)->q); n = (n + 1) / 2; } return q_size(container_of(head->next, queue_contex_t, chain)->q); } ``` :::