# 2025q1 Homework1 (lab0) contributed by < [otischung](https://github.com/otischung/) > {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 開發環境 ```bash ❯ uname -a Linux scream-Ubuntu-24 6.11.0-17-generic #17~24.04.2-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 20 22:48:29 UTC 2 x86_64 x86_64 x86_64 GNU/Linux ❯ gcc -v Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-fG75Ri/gcc-13-13.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-fG75Ri/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 Thread model: posix Supported LTO compression algorithms: zlib zstd gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04) ❯ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 46 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 24 On-line CPU(s) list: 0-23 Vendor ID: GenuineIntel Model name: 13th Gen Intel(R) Core(TM) i7-13700 CPU family: 6 Model: 183 Thread(s) per core: 2 Core(s) per socket: 16 Socket(s): 1 Stepping: 1 CPU(s) scaling MHz: 21% CPU max MHz: 5200.0000 CPU min MHz: 800.0000 BogoMIPS: 4224.00 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse3 6 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb r dtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopolog y nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 mon itor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm sse4_1 sse4_ 2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lah f_lm abm 3dnowprefetch cpuid_fault ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb intel_pt sha_ni xsaveopt xsavec xgetbv1 xsaves split_lock_detect user_shstk avx_vnni dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp hwp_pkg_ req hfi vnmi umip pku ospke waitpkg gfni vaes vpclmulqdq rdpid movdiri movdir64b fsrm md_clear serialize pconfig arch_lbr ibt flush_l1d arch _capabilities Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 640 KiB (16 instances) L1i: 768 KiB (16 instances) L2: 24 MiB (10 instances) L3: 30 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-23 Vulnerabilities: Gather data sampling: Not affected Itlb multihit: Not affected L1tf: Not affected Mds: Not affected Meltdown: Not affected Mmio stale data: Not affected Reg file data sampling: Mitigation; Clear Register File Retbleed: Not affected Spec rstack overflow: Not affected Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization Spectre v2: Mitigation; Enhanced / Automatic IBRS; IBPB conditional; RSB filling; PBRSB-eIBRS SW sequence; BHI BHI_DIS_S Srbds: Not affected Tsx async abort: Not affected ``` ## 整理程式碼現有架構設計 參考 [Dennis Liu](https://hackmd.io/@dennis40816/linux2025-homework1) 的解說圖片 https://hackmd.io/@dennis40816/linux2025-homework1?stext=7939%3A146%3A0%3A1741066532%3Aiu6nSC 我們將 `list.h`, `queue.h` 現有的 API 定義與實作做討論 ```cpp struct list_head { struct list_head *prev; struct list_head *next; }; ``` 可以發現,這個 `list_head` 只有雙向鏈結串列的部份,並沒有定義存放資料的欄位。 > Both node and head share the same struct type. 因此,我們接者看單一節點 (node/element) 是如何定義的 ```cpp typedef struct { char *value; struct list_head list; } element_t; ``` 現在,我們再回頭看 `struct list_head` 的定義 > The `prev` pointer of the list head points to the last list node of the list. > The `next` points to the first list node of the list. > For an empty list, both member variables point to the head. 對於一個空的 queue,我們已經有以下此函式可呼叫 ```cpp static inline void INIT_LIST_HEAD(struct list_head *head) { head->next = head; head->prev = head; } ``` 所以,對於一個 queue,我們有 - 一個 `struct list_head *head` - 若干個 `element_t e_0, e_1, ...` - `head->next` 指向第一個 element `e0` 的 `struct list_head list` - `head->prev` 指向最後一個 element `e_{N-1}` 的 `struct list_head list` - N 是 element 數量。 因此,我們會需要 [container of](https://hackmd.io/@sysprog/linux-macro-containerof) 經由 `struct list_head list` 的位址,回推該 node `element_t` 的位址。 現在,我們已經了解 [container of](https://hackmd.io/@sysprog/linux-macro-containerof) 如何應用於單一 queue,現在我們可以來看如何建立起多個 queues,這些 queues 也是由雙向鏈結串列所組成。 ```cpp typedef struct { struct list_head *q; struct list_head chain; int size; int id; } queue_contex_t; ``` 透過 `qtest.c`,我們可以知道如何操作以上所有的 struct。 ```cpp 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; } ... } ``` - `q` 是此 queue 的 `list_head *`,根據 `list_head` 定義 - 如果該 queue 有 node, - 走訪 `next` 會得到第一個元素 - 走訪 `prev` 會得到最後一個元素。 - 如果該 queue 沒有有 node,則 `next` 與 `prev` 均指向 `list_head` 自己。 - `chain` 是 queue 自身的雙向鏈結串列,由此走訪上一個或下一個 queue,同樣用 [container of](https://hackmd.io/@sysprog/linux-macro-containerof) 回推 `queue_contex_t`。 - `size` 是此 queue 有幾個 node。 - `id` 是此 queue 的編號。 ## 針對佇列操作的程式碼實作 ### `q_new` commit: [cc22da8](https://github.com/otischung/lab0-c/commit/cc22da8c88af3ba42b9a2cdf0d49e8d1dff95fe4) 用途就如同 `qtest.c` 內的 `q_new()`,根據 `list_head` 定義,透過 `INIT_LIST_HEAD()` 初始化。 ### `q_free` commit: [ae04274](https://github.com/otischung/lab0-c/commit/ae0427472a0d4a67b4ad0497708732ffe4223459) 首先,我們需要 `free` `element_t`,而我們發現在 `queue.h` 內就有實作 `q_release_element()` ```cpp static inline void q_release_element(element_t *e) { test_free(e->value); test_free(e); } ``` 接下來,我們需要走訪每一個 `element_t`,這需要以下操作 - 對於每個 `list_head` 經由 [container of](https://hackmd.io/@sysprog/linux-macro-containerof) 回推 `element_t` - 由於我們要做刪除,所以我們要額外定義一個 `element_t *` 指向下一個物件,才能安全將目前 iterator 指向的物件刪除 - iterator 指向下一個 `list_head`,直到回到 head 本身 這些複雜的迴圈設定,在 `list.h` 已經有定義 macro ```cpp #define list_for_each_entry_safe(entry, safe, head, member) \ for (entry = list_entry((head)->next, __typeof__(*entry), member), \ safe = list_entry(entry->member.next, __typeof__(*entry), member); \ &entry->member != (head); entry = safe, \ safe = list_entry(safe->member.next, __typeof__(*entry), member)) ``` 因此,檢查完 `struct list_head *head` 不是 `NULL` 之後,定義 iterator 與 safe 指標,代入 macro 展開迴圈,移除 element,最後移除 `head` 即可。 ### `q_insert_head` commit: [52745fd](https://github.com/otischung/lab0-c/commit/52745fd70989f6afe1e601eea397dfe3c242b0a7) 在這裡,我們需要建立一個 `element_t` 的記憶體空間做存取,由於我們對於所有 node 都是透過 `list_head` 間接操作,因此不需要特別儲存每一個`element_t` 的記憶體位址。 又因為 `element_t` 的記憶體空間完成配置後需要被保留以供後續操作,因此,我們需要使用指標,透過 `malloc()` 的方式配置,這樣離開此函式之後,該記憶體位置不會被刪除。 配置 `element_t` 的記憶體空間後,我們使用 `strdup()` 來複製字串到指定記憶體位置,詳見 [Manual Page](https://man7.org/linux/man-pages/man3/strdup.3.html) 剩下的 linked-list 部份,在 `list.h` 已經有實作 `list_add()` ```cpp static inline void list_add(struct list_head *node, struct list_head *head) { struct list_head *next = head->next; next->prev = node; node->next = next; node->prev = head; head->next = node; } ``` **注意**:因為我們是透過 [container of](https://hackmd.io/@sysprog/linux-macro-containerof) 操作 node,直接看此函式必定會有 memory leak 的疑慮,因此<font color=FF0000>**我們在此關閉 `pre-commit.hook`**</font>。 ### `q_insert_tail` commit: [52745fd](https://github.com/otischung/lab0-c/commit/52745fd70989f6afe1e601eea397dfe3c242b0a7) 觀察 `list_add()` 的實作,他會將 node 插入到 `head->next` 之前。 而 `q_insert_tail()` 應當要將 node 插入到 `head` 之前,這兩者之間差一個 `next`。 因此,將 `head` 代入 `head->prev` 即可重用 `q_insert_head()`。 :::info **注意**:在 `make test` 中,我的 `q_insert_head` 與 `q_insert_tail` 一直回報可能不是 constant time ::: > ERROR: Probably not constant time or wrong implementation :::success 在 merge 了[源頭 upstream ](https://github.com/sysprog21/lab0-c/tree/599de0f32ae8ec9541e7de8053e7549272220999)的新的 [commit](https://github.com/otischung/lab0-c/tree/12872c8f9282d15eec0168de90ee4970a7bb2465) 之後,此問題已解決 ::: 可以經由以下指令得到目前合併了 upstream 的哪個 commit: ```bash git remote add upstream https://github.com/sysprog21/lab0-c.git git fetch upstream git merge-base HEAD upstream/master ``` ### `q_remove_head` commit: [fa8ff8b](https://github.com/otischung/lab0-c/commit/fa8ff8b06d6f8e435d9b5da4ccbd7914bd339aab) 檢查 queue 是否有至少一個 node,這包含 2 種情況 - `head == NULL` - `head` 剛被初始化 - 特性是 `prev` 與 `next` 會等於 `head` - `list.h` 已經有 `list_empty()` 的實作 ```cpp static inline int list_empty(const struct list_head *head) { return (head->next == head); } ``` 接下來,我們需要找到第一個 node,`list.h` 已經有 `list_first_entry()` 的 macro ```cpp #define list_entry(node, type, member) container_of(node, type, member) #define list_first_entry(head, type, member) \ list_entry((head)->next, type, member) ``` 找到之後,予以刪除,`list.h` 已經有 `list_del()` 的實作 ```cpp static inline void list_del(struct list_head *node) { struct list_head *next = node->next; struct list_head *prev = node->prev; next->prev = prev; prev->next = next; } ``` 最後,使用 `strncpy()` 將欲 remove 的 node 的字串複製出來,最後回傳欲 remove 的 node 的記憶體位址。 注意:remove 與 delete 不同,remove 僅有移出 queue,並未完全刪除該 node 的記憶體空間 ### `q_remove_tail` commit: [fa8ff8b](https://github.com/otischung/lab0-c/commit/fa8ff8b06d6f8e435d9b5da4ccbd7914bd339aab) 與 `q_insert_tail` 相似,將 `head` 代入 `head->prev` 即可重用 `q_remove_head()`。 ### `q_size` commit: [463c4c4](https://github.com/otischung/lab0-c/commit/463c4c415a6c56781f12af92694b767fa4236a07) `list.h` 已經有 `list_for_each()` 的 macro ```cpp #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` 設定一個計數器,走訪完畢即可。 ### `q_delete_mid` commit: [2fd563d](https://github.com/otischung/lab0-c/commit/2fd563d5be944d1202b20d5276abace83dd6c834) 此函式與此 [leetcode](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) 題目相似。 我們可以利用[快慢指標](https://hackmd.io/@sysprog/ry8NwAMvT)來找到中間點,簡單來說就是,一開始慢指標與快指標皆指向第一個 node,也就是 `head->next`,之後每次慢指標走一步,且快指標的下一個不會回到開頭,則快指標就走兩步,如此一來,我們的慢指標必定會指到第 $\lfloor n / 2 \rfloor$ 個 node,n 為總 node 數量。 ### `q_delete_dup` commit: [a91339f](https://github.com/otischung/lab0-c/commit/a91339fa1bcb382ecc2d971e14b3cebe438165e7) 此函式與此 [leetcode](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/) 題目相似。 需留意此函式要求對於一個已排序的 linked-list,刪除**所有**有重複的 node,即只要有重複,皆不予保留,例如: `[1, 1, 1, 2, 3, 4, 5, 5]` 執行過 `q_delete_dup`,預計變成 `[2, 3, 4]` `1` 將被完全刪除。 我們需要一個 traverse pointer,還有一個慢一個 node 的 previous pointer。 一開始,`prev == NULL` ``` 1 1 1 2 3 4 5 5 p c ``` 當 `prev != NULL` 時,開始判斷 node 是否重複 - 若重複,則 delete `prev` node。 ``` 1 1 1 2 3 4 5 5 p c ``` - 若不重複,則 - 若前一次為重複,則需刪除位於前一個該種最後一個 node ``` 1 1 1 2 3 4 5 5 p c ``` - 若前一次不重複,則不處理 ``` 1 1 1 2 3 4 5 5 p c ``` - 最後一個需要特別判斷 - 若前一次為重複,則需刪除位於前一個該種最後一個 node ``` 1 1 1 2 3 4 5 5 p c ``` - 若前一次不重複,則不處理 ``` 1 1 1 2 3 4 5 5 6 p c ``` ### `q_swap` commit: [2e91e6f](https://github.com/otischung/lab0-c/commit/2e91e6fecd20defc9bbcaa6d63655cb0829e27e1) 此函式與此 [leetcode](https://leetcode.com/problems/swap-nodes-in-pairs/) 題目相似。 我們先從如何交換 2 個 node 開始,原本我的想法過於複雜,需要同時考量前一個與後一個 node,總共 4 個 node。但是其實這樣看似複雜的操作,其實等價於把前一個 node 移除 (remove, not delete),然後再將其插入至後一個 node 之後,`list.h` 已經有 `list_for_each()` 的函式 ```cpp static inline void list_move(struct list_head *node, struct list_head *head) { list_del(node); list_add(node, head); } ``` 接著,我們再看 `list_for_each` ```cpp #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` 雖然這個函式的註解有說明 > The current node (iterator) is allowed to be removed from the list. Any other modifications to the the list will cause undefined behavior. 不過,考量以下程式的意義 ```cpp for (node = (head)->next; node != (head); node = node->next) { if (node->next == head) break; list_move(node, node->next); } ``` 我們就可以達到以下結果 ``` Beginning 1 2 3 4 n list_move 2 1 3 4 n Next Iteration 2 1 3 4 n list_move 2 1 4 3 n ``` ### `q_reverse` commit: [d4ff6ca](https://github.com/otischung/lab0-c/commit/d4ff6ca77420cddea14594c45cdd5fc399d7301d) 我們要反轉一個 queue,只需要逐一將所有 node 重新插入至 queue 的開頭即可 我們看一下 `list.h` 裡面的 `list_for_each_safe` ```cpp #define list_for_each_safe(node, safe, head) \ for (node = (head)->next, safe = node->next; node != (head); \ node = safe, safe = node->next) ``` 這裡由於 `node = safe`,所以更改 node 並不影響 traverse 的順序 ### `q_reverseK` commit: [68b430e](https://github.com/otischung/lab0-c/commit/68b430e5ddfb6df012fd88ea7a5dcdd9d71afd0e) 此函式與此 [leetcode](https://leetcode.com/problems/reverse-nodes-in-k-group/) 題目相似。 首先確認目前指向的 node 後面還有沒有 k 個 nodes (包含自身),若有,則對於這些 k 個 nodes 做反轉,方法與 `q_reverse` 類似,只是由於只走訪 k 個,因此沒有 macro 可以套用,要自己寫。 ### `q_sort` commit: [1156c9c](https://github.com/otischung/lab0-c/commit/1156c9c1ac0e7eb527950c175155680fd13ab47a) 我們使用 Merge sort 來實作此函式。 Merge sort 有以下需要考量 1. 將目前的 list 切成一半 2. 遞迴解左右兩邊的 list,預計返回後會得到左右兩個各自排序完成的兩個 list 3. 將兩個已排序的 list 合併成一個排序的 list #### 將目前的 list 切成一半 (divide) 我們需要分割 list,在 `list.h` 已經有實作 `list_cut_position()` ```cpp static inline void list_cut_position(struct list_head *head_to, struct list_head *head_from, struct list_head *node) ``` 這會將 `*head_from` 從頭到 `*node` (含 `*node` 本身),都移到 `*head_to`。 切一半的事情,可以使用**快慢指標**找到中間的 node。 因此,我們定義一個新的 `list_head left`,將原本的 list 從頭到中間的 node 都搬移到 `left`,即可完成第一項任務。 #### 遞迴解 現在,我們有新的 `left` 與原本的 `head`,分別遞迴求解即可。 #### 合併 list (conquer) 我們建立一個新的函式 `q_merge_two()` 來完成此項目。 首先,建立一個暫存 list `temp_head`。 接著,對於左右兩陣列皆有元素的時候,比較雙方的開頭所儲存的值的大小。 假設定義由小到大排序 (ascending),那麼就是將小的那一方移到暫存 list。 ``` left: 1 3 5 7 9 11 head: 2 4 6 8 10 temp: NULL ------------------- left: 3 5 7 9 11 head: 2 4 6 8 10 temp: 1 ------------------- ... ``` 此操作完成之後,若其中一方還有 node,那麼代表這些 node 所存的值都比暫存還大,所以直接搬移即可。 ``` left: 11 head: NULL temp: 1 2 3 4 5 6 7 8 9 10 ----------------------------- left: NULL head: NULL temp: 1 2 3 4 5 6 7 8 9 10 11 ----------------------------- ... ``` 最後,將 temp 搬移到 `left` 就完成操作。 `list.h` 已經有 `list_splice[_tail][_init]` 函式,專門複製整個 list 到另一個 list。 ### `q_ascend` 和 `q_descend` commit: [bfda729](https://github.com/otischung/lab0-c/commit/bfda729bd8f7b78fa11a19280a6e7307e5a8c47f) `q_descend`與此 [leetcode](https://leetcode.com/problems/remove-nodes-from-linked-list) 題目相似。 移除每個 node,若其右側的任何位置存在值更大的 node。 因此,我們需要走訪所有的 node,從每一個 node 中,往回刪除所有左側比當前走訪的 node 還要小的數字。 以下做示範,p 代表 prev,c 代表 current ``` [head, 5, 2, 13, 3, 8] p c ``` 現在發現 $13 > 2$,所以將 2 刪除,並繼續左移,直到 `prev == head`。 ``` [head, 13, 3, 8] p c ``` 以此類推,最後此 list 將剩下 `[13, 8]`。 若是解 `q_ascend`,則移除每個 node,若其右側的任何位置存在值更**小**的 node。 以此類推,最後此 list 將剩下 `[2, 3, 8]`。 ### `q_merge` commit: [c54469e](https://github.com/otischung/lab0-c/commit/c54469e41720507cd8bea565d58e00f84ed75e2c) 此函式與此 [leetcode](https://leetcode.com/problems/merge-k-sorted-lists/) 題目相似。 我們先看一下 `q_merge` 在 `qtest.c` 是如何被使用的 ```cpp typedef struct { struct list_head head; int size; } queue_chain_t; static queue_chain_t chain = {.size = 0}; static bool do_merge(int argc, char *argv[]) { ... if (current && exception_setup(true)) len = q_merge(&chain.head, descend); ... } ``` 接著,我們看一下要求 > There is no need to free the 'queue_contex_t' and its member 'q' since they will be released externally. However, q_merge() is responsible for making the queues to be NULL-queue, except the first one. 這個 merge 的要求與之前實作 Merge sort 的 `q_merge_two()` 相同,因此我們重用此函式。 我們首先使用 `list_for_each_entry_safe` 這個 macro 走訪每一個 queue,特別處理第一個 queue 並記錄其位址,之後依序使用 `q_merge_two()` 將後續每一個 queue 都 merge 到第一個 queue,將 merge 完成的 NULL queue 搬移到開頭,這樣就不會影響後續走訪的進行。 完成走訪後,first queue 會位於最後一個 queue,其前面都是 NULL queue (如果原本有 2 個 queues 以上),因此,再把 first queue 搬回第一個即可。 ## 其他疑問 ### 同步 repository 同步來源:[commit](https://github.com/sysprog21/lab0-c/commit/3c7c7b3251e50475a317e73f4517a181b60dcc2d) 完成同步到我的 repo:[commit](https://github.com/otischung/lab0-c/commit/5aef7d12ed988b21b8ecc9674735d76571354a29) 我在同步 repository 之後,發現 `make test` 一直跑不過,仔細檢查後發現 `scripts/check_repo.sh` 在抓取 upstream hash 時,會有 rate limited 的問題,因此發了 [issue](https://github.com/sysprog21/lab0-c/issues/243)。