# 2023q1 Homework1 (lab0) contributed by < `paulpeng-popo` > ## 開發環境 ```shell $ 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 Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 12 On-line CPU(s) list: 0-11 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz Stepping: 10 CPU max MHz: 4600.0000 CPU min MHz: 800.0000 BogoMIPS: 6399.96 Virtualization: VT-x L1d: 192 KiB (6 instances) L1i: 192 KiB (6 instances) L2: 1.5 MiB (6 instances) L3: 12 MiB (1 instance) NUMA node0 CPU(s): 0-11 ``` ## 開發紀錄 ### `q_new()` 確認 queue.h 中對 `q_new()` 行為的定義,可利用 `INIT_LIST_HEAD` 簡化程式碼 ```c struct list_head *head = malloc(sizeof(struct list_head)); if (head) { INIT_LIST_HEAD(head); } return head; ``` 好奇 `LIST_HEAD` 跟 `INIT_LIST_HEAD` 到底有什麼差,所以試著用 LIST_HEAD 改寫,結果遇到 function returns address of local variable,猜測是 variable 在 heap 空間被釋放後便跟著消失了,回傳的 `struct list_head *` 會變成 dangling pointer,因此編譯器回報錯誤 ```c LIST_HEAD(head); return &head; ``` 目前想到的解決辦法是宣告成 static,但副作用就是在 `q_free()` 的時候不能加 `free(l)` 這行,否則會 Undefined behavior,另外,此寫法在 q_test 沒問題,但 make test 會卡住,原因不明 ```c static LIST_HEAD(head); return &head; ``` :::warning 上方的 `1.`, `2.`, `3.`, `4.` 沒有存在的必要,儘量以簡潔清晰的方式展現。 :notes: jserv > [name=paulpeng] 收到 ::: ### `q_free()` 可以利用 `list_entry` 找到 `struct list_head` 外層的 `element_t` 指標 :::info - `list_entry` / `container_of` 利用 `offsetof` 算出每個 `element_t` 中 member 與 `element_t` 起始位址的 offset,後續就能用傳入的 member 位址往前算 offset 個 bytes,得到 `element_t` 的起始位址 - `list_for_each` 跟 `list_for_each_safe` 可以搭配 `list_entry` 的使用達成相似的功能 ; `list_for_each_safe` 跟 `list_for_each_entry_safe` 也是如此 ::: ```c struct list_head *node, *safe; list_for_each_safe (node, safe, l) { q_release_element(list_entry(node, element_t, list)); } free(l); ``` 發現 `list_for_each_entry_safe`,會去抓下一個 entry 存到 safe,但如果下一個 entry,也就是 `safe->member.next` 本身就是 head node,其外層並沒有 `element_t` pointer 指向它,好奇究竟為什麼不會報錯 ```c safe = list_entry(safe->member.next, __typeof__(*entry), member)) ``` 做個小實驗,結果 first 與 amz 的確指到同一個 object,或是說,`tmp->list` 是可以執行通過的,代表魔法藏在 `list_entry` 裡面 ```c element_t *tmp = list_entry(head, element_t, list); element_t *first = list_first_entry(head, element_t, list); element_t *amz = list_entry(tmp->list.next, element_t, list); ``` 猜測,head node 經過 `list_entry` 的計算後,返回了一個暫時的 `element_t` pointer,而裡面只會包含 list member,但若要嘗試 `printf` 出 `tmp->value` 便會 Segmentation fault ### `q_insert_[head|tail]` 使用 List API 的 `list_add` 和 `list_add_tail` 在字串複製方面想到可以用 4 種方式實作 ```c void * memcpy ( void * destination, const void * source, size_t num ); void * memmove ( void * destination, const void * source, size_t num ); char * strcpy ( char * destination, const char * source ); char * strncpy ( char * destination, const char * source, size_t num ); ``` - `memmove` 可以用來保證 src 跟 dst 不重疊 - 後來查到有 `strdup` 的方法可以 malloc 跟 copy 一起做 [strncpy(3p)](https://man7.org/linux/man-pages/man3/strncpy.3p.html) > The stpncpy() and strncpy() functions shall copy not more than n bytes (bytes that follow a NUL character are not copied) from the array pointed to by s2 to the array pointed to by s1. > > If the array pointed to by s2 is a string that is shorter than n bytes, NUL characters shall be appended to the copy in the array pointed to by s1, until n bytes in all are written. > > If copying takes place between objects that overlap, the behavior is undefined. [strdup(3)](https://man7.org/linux/man-pages/man3/strdup.3.html) > The strdup() function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3). ```c /* q_insert_head() */ element_t *entry = malloc(sizeof(element_t)); if (!entry) { return false; } size_t len = strlen(s) + 1; entry->value = malloc(sizeof(char) * len); strncpy(entry->value, s, len - 1); (entry->value)[len - 1] = '\0'; list_add(&entry->list, head); ``` ```c /* q_insert_tail() */ element_t *entry = malloc(sizeof(element_t)); if (!entry) { return false; } size_t len = strlen(s) + 1; entry->value = malloc(sizeof(char) * len); strncpy(entry->value, s, len - 1); (entry->value)[len - 1] = '\0'; list_add_tail(&entry->list, head); ``` [terry23304](https://hackmd.io/@terry23304/linux2023-lab0) 跟 [Paintako](https://hackmd.io/@Paintako/SyaRaHGCi) 提醒,`entry->value` malloc 的部分需要對回傳值做檢查,因此改成如下 ```c size_t len = strlen(s) + 1; entry->value = malloc(sizeof(char) * len); if (!entry->value) { free(entry); return false; } ``` 同時增加對參數 `s` 的檢查 ```c if (!head || !s) { return false; } ``` 後來參考到 [25077667](https://hackmd.io/@25077667/lab0-2023#q_insert_head) 的寫法,將兩個 functions 相似的功能寫在一起,使函式容易維護,而需要不同操作的地方利用 function pointer 的技巧來達成 ```c static bool q_insert(struct list_head *head, char *s, void (*op)(struct list_head *, struct list_head *)) { ... } bool q_insert_head(struct list_head *head, char *s) { return q_insert(head, s, list_add); } bool q_insert_tail(struct list_head *head, char *s) { return q_insert(head, s, list_add_tail); } ``` :::warning `q_insert` 沒有存在的必要,應在 `q_insert_tail` 中重用 `q_insert_head`。 :notes: jserv ::: ### `q_remove_[head|tail]()` 注意到除了 unlink node 外,還會將 element 內的 value 複製到 sp 中,猜測此做法是為了方便讓呼叫端確認移除的 value 內容 ```c /* q_remove_head() */ element_t *entry = list_first_entry(head, element_t, list); list_del_init(&entry->list); if (sp) { strncpy(sp, entry->value, bufsize); sp[bufsize - 1] = '\0'; } ``` ```c /* q_remove_tail() */ element_t *entry = list_last_entry(head, element_t, list); list_del_init(&entry->list); if (sp) { strncpy(sp, entry->value, bufsize); sp[bufsize - 1] = '\0'; } ``` 參考來自 [Shiritai](https://hackmd.io/@Eroiko/lab0-impl#q_insert_head-%E5%92%8C-q_insert_tail-%E7%9A%84%E5%AF%A6%E4%BD%9C%E8%88%87%E9%87%8D%E6%A7%8B) 的作法,使用前處理器的技巧,對程式碼做簡化 ```c #define q_remove(suffix, list_api) \ element_t *q_remove_##suffix(struct list_head *head, char *sp, \ size_t bufsize) \ { \ if (!head || list_empty(head)) \ return NULL; \ element_t *entry = list_api(head, element_t, list); \ list_del_init(&entry->list); \ if (sp) { \ strncpy(sp, entry->value, bufsize); \ sp[bufsize - 1] = '\0'; \ } \ return entry; \ } /* q_remove_head() */ q_remove(head, list_first_entry); /* q_remove_tail() */ q_remove(tail, list_last_entry); ``` :::warning 撰寫更精簡的程式碼。 :notes: jserv ::: ### `q_size()` ```c int len = 0; struct list_head *node; list_for_each (node, head) { len++; } ``` ### `q_delete_mid()` 第一時間想到的方法是,從兩端向中間走訪,雖然效果與快慢指標法相同,但缺點就是只在 circular doubly-linked list 的資料結構上有效 ```c struct list_head *front = head->next; struct list_head *back = head->prev; while (front != back && front->next != back) { front = front->next; back = back->prev; } // delete the node which is pointed by front list_del_init(front); element_t *entry = list_entry(front, element_t, list); q_release_element(entry); return true; ``` 所以最後還是改回快慢指標,並獨立成一子函式,方便後續實作使用 :::warning 這個「所以」的依據為何? :notes: jserv ::: ```c static void _find_mid(struct list_head **mid, struct list_head *head) { *mid = head->next; struct list_head *fast = head->next->next; while (fast != head && fast && fast->next != head && fast->next) { *mid = (*mid)->next; fast = fast->next->next; } } ``` ### `q_delete_dup()` 以 sorted list 為基礎,刪除具重複值的 node,只需要判斷相鄰兩節點是否值相同即可 ```c bool dup = false; struct list_head *del_list = q_new(); element_t *entry, *safe; list_for_each_entry_safe (entry, safe, head, list) { if (&safe->list != head && !strcmp(entry->value, safe->value)) { list_move(&entry->list, del_list); dup = true; } else if (dup) { list_move(&entry->list, del_list); dup = false; } } q_free(del_list); } ``` 後來想想,這個作法把欲刪除的節點都移到一條新的 `list_head` 上,不但多用記憶體空間,又在 `q_free` 的時候多跑一層迴圈,有點多此一舉的感覺 因此搭配後面的 `cmpstr()` 的函式,可改寫簡化 ```c bool dup = false; struct list_head *node, *safe; list_for_each_safe (node, safe, head) { if (safe != head && !cmpstr(&node, &safe)) { list_del(node); q_release_element(list_entry(node, element_t, list)); dup = true; } else if (dup) { list_del(node); q_release_element(list_entry(node, element_t, list)); dup = false; } } ``` ### `q_reverse[K]` and `q_swap()` 這邊參考 [Shiritai](https://hackmd.io/@Eroiko/lab0-impl#q_swap) 的思路,考慮 q_swap, q_reverse, q_reverseK 性質相似,可以把 `q_swap` 當 `reverseK` 的特例處理 `list_move` 裡面有呼叫到 `list_del` 做 unlink 的動作,因此使用 `list_for_each_safe` ```c /* q_reverse() */ LIST_HEAD(reverse_list); struct list_head *node, *safe; list_for_each_safe (node, safe, head) { list_move(node, &reverse_list); } list_splice_init(&reverse_list, head); ``` reverse 延伸,但是多了 counter 計算是否到達 K group 的最後一個 node 參考 [KHLee529](https://hackmd.io/@KHLee529/linux2023-lab0#%E5%8F%8D%E5%90%91%E9%87%8D%E6%8E%92) 的程式架構 ```c /* q_reverseK() */ int count = 0; LIST_HEAD(rlist); LIST_HEAD(tmp); struct list_head *node, *safe; struct list_head *start = head; list_for_each_safe (node, safe, head) { count++; if (count == k) { list_cut_position(&rlist, start, node); q_reverse(&rlist); list_splice_tail_init(&rlist, &tmp); start = safe->prev; count = 0; } } list_splice_init(&tmp, head); ``` 如前所述,swap 是 K=2 的 reverseK ```c /* q_swap() */ q_reverseK(head, 2); ``` ### `q_sort()` 使用 [strcmp(3)](https://man7.org/linux/man-pages/man3/strcmp.3.html) 做字串比大小 > The strcmp() and strncmp() functions return an integer less than, equal to, or greater than zero if s1 (or the first n bytes thereof) is found, respectively, to be less than, to match, or be greater than s2. 發現<s>蠻</s> --- 許多部分需要用到 strcmp,索性寫一個 cmpstr,簡化一下程式碼篇幅 - return value $\lt$ 0 代表 len(str1) < len(str2) - return value $=$ 0 代表 len(str1) == len(str2) - return value $\gt$ 0 代表 len(str1) > len(str2) :::warning 查閱教育部國語辭典「[蠻](https://dict.revised.moe.edu.tw/dictView.jsp?ID=1235)」: * 強橫、不通情理 * 落後的、未開化的 在這裡無助於溝通,儘量使用簡潔且清晰的詞彙。 :notes: jserv ::: ```c static inline int cmpstr(const void *p1, const void *p2) { element_t *first = list_entry(*(const struct list_head **) p1, element_t, list); element_t *second = list_entry(*(const struct list_head **) p2, element_t, list); return strcmp(first->value, second->value); } ``` 參考 [sysprog21/linux-list](https://github.com/sysprog21/linux-list/blob/master/examples/quick-sort.c) 裡面的 `q_sort.c` 並改寫 ```c /* stable quick sort */ // pick head first as pivot pivot = head->next; list_del(pivot); list_for_each_safe (node, safe, head) { if (cmpstr(&node, &pivot) < 0) list_move_tail(node, &list_less); else list_move_tail(node, &list_greater); } q_sort(&list_less); q_sort(&list_greater); list_add(pivot, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); ``` 實作完,執行 qtest 測試 sort 功能沒問題,但 make test 卻過不了,研究後發現是題目有要求 $O(n\log n)$ 的時間複雜度,而 quick sort 有可能發生 $O(n^2)$ 的 worst case 最後換成 merge sort,參考 [你所不知道的 C 語言: linked list 和非連續記憶體](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) 中 LeetCode 21 的講解範例 ```c /* merge sort 分割 */ if (!list || !list->next) { return list; } LIST_HEAD(head); head.next = list; struct list_head *slow = NULL, *mid = NULL; _find_mid(&slow, &head); mid = slow->next; slow->next = NULL; struct list_head *left = merge_sort(list); struct list_head *right = merge_sort(mid); return merge_two_lists(left, right); ``` ```c /* merge sort 合併 */ struct list_head *head = NULL, **ptr = &head, **node; for (node = NULL; L1 && L2; *node = (*node)->next) { if (cmpstr(&L1, &L2) < 0) node = &L1; else node = &L2; *ptr = *node; ptr = &(*ptr)->next; } *ptr = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2); return head; ``` ### `q_descend()` ```c struct list_head *node, *safe; list_for_each_safe (node, safe, head) { element_t *pentry = list_entry(node->prev, element_t, list); element_t *entry = list_entry(node, element_t, list); if (&pentry->list != head && strcmp(pentry->value, entry->value) < 0) { list_del(node); q_release_element(entry); } } return q_size(head); ``` 這邊我誤解題目的意思,原意是 **對任一節點 n,若其右邊有比它大的值,則刪除 n**,而我實作成 **對任一節點 n,刪除 n 右邊值大於 n 的所有節點** 參考 [terry23304](https://hackmd.io/@terry23304/linux2023-lab0#q_descend) 的作法後,使用反向迭代,改寫如下 ```c struct list_head *node = head->prev; struct list_head *pnode = node->prev; char *max = NULL; for (; node != head; node = pnode) { element_t *entry = list_entry(node, element_t, list); pnode = node->prev; if (!max || strcmp(entry->value, max) > 0) { max = entry->value; } else { list_del(node); q_release_element(entry); } } return q_size(head); ``` ### `q_merge()` 把所有 lists 都連接在一起,最後對整個 list 做 sort ```c queue_contex_t *qhead = list_first_entry(head, queue_contex_t, chain); list_del_init(&qhead->chain); queue_contex_t *cur = NULL; list_for_each_entry (cur, head, chain) { list_splice_init(cur->q, qhead->q); qhead->size += cur->size; } list_add(&qhead->chain, head); q_sort(qhead->q); ``` 猜測或許此函式是對 sorted list 進行 merge,這樣只要在 merge 的時候同時處理排序問題就好,而不用在最後進行大型的排序工作,未來可以嘗試改進 ## Address Sanitizer Makefile 中有一段,可以判斷 SANITIZER 是否有被打開,接著會把 -fsanitize=address 加到 compile flag 中 ```cmake # Enable sanitizer(s) or not ifeq ("$(SANITIZER)","1") # https://github.com/google/sanitizers/wiki/AddressSanitizerFlags CFLAGS += -fsanitize=address -fno-omit-frame-pointer -fno-common LDFLAGS += -fsanitize=address endif ``` 下命令的時候只要指定 `SANITIZER=1` 即可 ```shell $ make clean && make SANITIZER=1 test ``` 執行後分數未改變 ## Valgrind 與 Massif 視覺化 ## Paper Dude, is my code constant time? ## Web server