# 2018q3 Homework3 (list) contributed by < `ofAlpaca` > ###### tags: `CSIE5006` `HW` ## 自我檢查事項 #### 為何 Linux 採用 macro 來實作 linked list?一般的 function call 有何成本? * 使用 macro 可以省去多餘的 function call 且執行速度較快,因為 preprocessor 會將 macro 展開至呼叫 macro 的程式碼內。 * 使用 function call 最大的成本就是需要**呼叫堆疊** (Stack) ,所以在執行時期較花費時間。 * 使用 macro 不會呼叫堆疊,但會展開至程式碼內,因此執行速度較 function call 快,屬於空間換取時間。 * 不過 function call 的優點是會做 **type checking** ,而 macro 不會。 參考 : [Macro vs Function](https://www.geeksforgeeks.org/macros-vs-functions/) [Macro vs Function(二)](https://dotblogs.com.tw/ace_dream/2016/01/16/function_macro) --- #### Linux 應用 linked list 在哪些場合?舉三個案例並附上對應程式碼,需要解說,以及揣摩對應的考量 * linux kernel 中的 timer 使用了 linked list 來動態儲存計時器 * 計時器用於指定某段時間後才執行特定函式 * `timer_list` 計時器的結構,儲存著 timeout 與執行函式的 function pointer 等資訊 * 使用 doubly-linked list 來記錄 timer 除了能動態的增刪以外,還能根據 `expires` 的先後來排序 ```clike struct timer_list { struct list_head entry; unsigned long expires; struct tvec_base *base; void (*function)(unsigned long); unsigned long data; int slack; }; ``` 參考: [Timers](http://www.science.unitn.it/~fiorella/guidelinux/tlk/node121.html) --- #### GNU extension 的 [typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) 有何作用?在程式碼中扮演什麼角色? * `typeof` 是 operator ,用於回傳 object 的型別,是 gcc 的 extension。 * 常用於讓 macro 在傳遞參數時能動態接受多種型別,可以讓程式碼更加有彈性。例如 : ```clike #define max(a,b) \ ({ typeof (a) _a = (a); \ typeof (b) _b = (b); \ _a > _b ? _a : _b; }) ``` --- #### 解釋以下巨集的原理 ```Clike #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` 此 macro 的目的是為了從 struct 中的 member 推算出原本 struct 的位址。 以下逐一分析 : * 先透過 `__typeof__` 得到 type 中的成員 `member` 的型別,並產生一個 pointer to 給該型別的 `__pmember` 。 * 將 `ptr` assign 給 `__pmember` 。 * `__pmember` 目前指向的是 `member` 的位址。 * `offsetof(type, member)` 可以算出 `member` 在 `type` 這個 struct 中的位移量, offset 。 * 將絕對位址 `(char*) __pmember` 減去 `offsetof(type, member)` ,可以得到 struct 的起始位址。 * 最後 `(type*)` 再將起始位置轉型為 pointer to `type` 。 參考 : [你所不知道的 C 語言 : 指標篇](https://hackmd.io/s/HyBPr9WGl#C-%E8%AA%9E%E8%A8%80-offsetof-%E5%B7%A8%E9%9B%86%E7%9A%84%E5%AE%9A%E7%BE%A9) --- #### 除了你熟悉的 add 和 delete 操作,`list.h` 還定義一系列操作,為什麼呢?這些有什麼益處? * 主要的目的是為了提供 linked list 的 Linux Kernel API 。 * 在 `list.h` 中的各種操作都是只針對 linked list 本身而已,完全不影響資料的部份。 * 透過這種寫法,只要將 `list_head` 包含進 structure ,就可以讓它也能隨心所欲地放資料,同時又不用自己維護一套 doubly linked list 。 ![](https://i.imgur.com/d3bG8t6.png) 參考 : [Linux kernel api](https://kernel.readthedocs.io/en/sphinx-samples/kernel-api.html#doubly-linked-lists) [運作流程](https://myao0730.blogspot.com/2016/12/linux.html) --- #### `LIST_POISONING` 這樣的設計有何意義? * 根據 `list.h` 內的 `list_del()` 描述 : ``` * LIST_POISONING can be enabled during build-time to provoke an invalid memory * access when the memory behind the next/prev pointer is used after a list_del. * This only works on systems which prohibit access to the predefined memory * addresses. ``` 意思是當 next/prev pointer 所指向的記憶體空間被 `list_del()` 刪除後,為了避免 dangling pointer 的問題,所以讓 pointer 指向違法的記憶體位址,之後如果 pointer 被 reference 時就會觸發 page fault 。 * 將 pointer 指向會產生 page fault 的位址。`linux/poison.h` 有提到也可用 `0xdead000000000000` ,這樣就可以明顯地看出是 poisoning 。 ```clike #ifdef LIST_POISONING node->prev = (struct list_head *) (0x00100100); node->next = (struct list_head *) (0x00200200); #endif ``` :::info 位址 `0x00100100` 與 `0x00200200` 有什麼特別意思嗎? ::: - [x] 根據 [這裡](https://wiki.litmus-rt.org/litmus/KernelDebugging),這兩個位址都是 Well-known address ,在 linux kernel 中是常數位址,為了能讓人發現到有錯誤發生而故意挑選的位址。 參考 : [pointer poison](https://stackoverflow.com/questions/27801360/what-is-the-meaning-of-0xdead000000000000/27806246) [dangling pointer](https://en.wikipedia.org/wiki/Dangling_pointer) [poison.h](https://github.com/torvalds/linux/blob/master/include/linux/poison.h) --- #### linked list 採用環狀是基於哪些考量? 根據 Linux Device Driver Chapter 11 Linked lists 中講到 : > Since Linux lists are circular, the head of the list is not generally different from any other entry. 如果 linked list 是環狀結構,那麼每個節點的操作都是一樣的,因此不需要再特別去維護 head / tail pointer ,從任何節點都可以尋訪整個 linked list 。 參考 : [linux device driver 3rd-edition](https://bootlin.com/doc/books/ldd3.pdf) [Quora : Why are all the linked lists circular in the Linux Kernel?](https://www.quora.com/Why-are-all-the-linked-lists-circular-in-the-Linux-Kernel) --- #### `list_for_each_safe` 和 `list_for_each` 的差異在哪?"safe" 在執行時期的影響為何? * `list_for_each_safe` 和 `list_for_each` 的差異在於 `safe` 可以接受刪除節點。 * 先講結論,`safe` 主要是為了防止刪除當下的這個節點後會遺失其 next pointer 因而出現錯誤,所以多增加了一個 safe pointer 去保存它,這樣無論怎麼修改那個節點都不會影響到下一個節點。 ```clike #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) #define list_for_each_safe(node, safe, head) \ for (node = (head)->next, safe = node->next; node != (head); \ node = safe, safe = node->next) ``` --- #### for_each 風格的開發方式對程式開發者的影響為何? 提示:對照其他程式語言,如 Perl 和 Python * for_each 迴圈風格最大的特點在於沒有明顯的 counter ,基本上就是把集合中的所有元素都跑過一遍。 * 可以避免 off-by-one error (OBOE) ,指的是執行迴圈時的邊界條件判斷寫錯,導致迴圈多跑或少跑一次。 * for_each in python : ```python set = [1,3,7,5,11,13,17,23] s = 0 for item in set: s = s + item ``` 與 C 語言的 for 迴圈比起來精簡很多,增加了程式碼的易讀性,不用特別去宣告 iterator 或是 counter ,也不用去擔心條件判斷是否正確。 參考 : [Foreach_loop](https://en.wikipedia.org/wiki/Foreach_loop) --- #### 程式註解裡頭大量存在 `@` 符號,這有何意義?你能否應用在後續的程式開發呢? 提示: 對照看 Doxygen * Doxygen 是一套自動生成文件的程式,能夠解析原始碼的註解後產生文件,而 `@` 就是其所有指令的開頭。 > All commands in the documentation start with a backslash (\) or an at-sign (@). * 在 `list.h` 中的 `@` 用的指令是 `param` ,就是告訴 Doxygen 描述這個 function 參數的用途 。 > Starts a parameter description for a function parameter with name <parameter-name>, followed by a description of the parameter. * 如果之後要和他人進行協同開發作業,各函式的註解就是必要的了,一來可以幫助他人快速理解自己的程式碼內容,二來可以幫助未來的自己回想。 * 可以學習像 `kernel-doc` 風格註解 : ``` /** * function_name() - Brief description of function. * @arg1: Describe the first argument. * @arg2: Describe the second argument. * One can provide multiple line descriptions * for arguments. * * A longer description, with more discussion of the function function_name() * that might be useful to those using or modifying it. Begins with an * empty comment line, and may include additional embedded empty * comment lines. * * The longer description may have multiple paragraphs. * * Context: Describes whether the function can sleep, what locks it takes, * releases, or expects to be held. It can extend over multiple * lines. * Return: Describe the return value of foobar. * * The return value description can also have multiple paragraphs, and should * be placed at the end of the comment block. */ ``` 參考 : [Doxygen 筆記](https://blog.longwin.com.tw/2011/04/doxygen-document-generator-2011/) [Doxygen Manual - cmdparam](http://www.stack.nl/~dimitri/doxygen/manual/commands.html#cmdtparam) [Function documentation](https://www.kernel.org/doc/html/latest/doc-guide/kernel-doc.html#function-documentation) --- #### `tests/` 目錄底下的 unit test 的作用為何?就軟體工程來說的精神為何? * unit test 是用於檢驗程式的某一功能是否正確的測試方法。 * 在 `tests/` 底下的 `.c` 檔是用於檢驗 `list.h` 中的 linked list 操作是否正確的 unit test 。 * 在 make 時期會將 unit test 編譯後並執行,若是 linked list 的操作不符合預期,則會 **throw assertion** 並中止程式。 ```shell $ make CC tests/list_add_tail.o LD tests/list_add_tail *** Validating tests/list_add_tail *** list_add_tail: tests/list_add_tail.c:34: main: Assertion `i == 1' failed. Aborted (core dumped) Makefile:49: recipe for target 'tests/list_add_tail.ok' failed make: *** [tests/list_add_tail.ok] Error 134 ``` 參考 : [Unit testing](https://en.wikipedia.org/wiki/Unit_testing) --- #### `tests/` 目錄的 unit test 可如何持續精進和改善呢? ![](https://i.imgur.com/nBD17BY.png) --- ## Make `lab0-c` to `lab-list` ### 改寫目標 * 學習 linux-list 裡頭的技巧與 unit test 的設計 * 改寫為 doubly-linked list 且考慮到 circular * `qtest` 也需要修改至可支援 --- ### `queue.h` * 資料結構調整 * 增加 `list_head` 來作為 doubly-linked list 的主要部分,包含 `next` 與 `prev` 指標。 * 將原本 `list_ele_t` 的 `next` 指標改為 `list_head` 。 * 將原本 `queue_t` 內 `list_ele_t` 的 `head` 與 `tail` 指標移除,改為 `list_head` 。 * 結構如下圖 : ![](https://i.imgur.com/8ipW5oS.png) * 透過 linux-kernel 風格的 linked list 可以不用再特別處理 head 或 tail 指標,因為大家都是相同的。 ```clike= struct list_head { struct list_head *prev; struct list_head *next; }; typedef struct ELE { char *value; struct list_head list; } list_ele_t; typedef struct { size_t size; // size of the linked list struct list_head head; /* Doubly Linked list of elements */ } queue_t; ``` * 新增 macro * 為了能夠從 `list_head` 找到 `list_ele_t` 的 entry ,所以增加 `container_of` 巨集。 * 增加 `list_for_each_safe` 巨集來尋訪 linked list 。 ```clike= #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) #define list_for_each_safe(node, safe, head) \ for (node = (head)->next, safe = node->next; node != (head); \ node = safe, safe = node->next) ``` * 新增 linked list 的操作函式,源自於 [linux-list](https://github.com/sysprog21/linux-list) 的 `list.h` ```clike= static inline void list_add(struct list_head *node, struct list_head *entry) { struct list_head *next = entry->next; next->prev = node; node->next = next; node->prev = entry; entry->next = node; } static inline void list_add_tail(struct list_head *node, struct list_head *head) { struct list_head *prev = head->prev; prev->next = node; node->next = head; node->prev = prev; head->prev = node; } 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; } static inline void INIT_LIST_HEAD(struct list_head *head) { head->next = head; head->prev = head; } ``` --- ### `queue.c` * `q_new()` * 新增初始化 `list_head` ,使得 `next` 與 `prev` 都指向自己的 `list_head` 。 ```clike= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (q == NULL) return NULL; INIT_LIST_HEAD(&q->head); q->size = 0; return q; } ``` * `q_free()` * 透過 `list_for_each_safe` 尋訪各個 `list_head` 並處理好要刪除節點的指標。 * 再使用 `container_of` 來取得 `list_ele_t` 的 entry 並釋放其記憶體。 ```clike= void q_free(queue_t *q) { if (q != NULL) { struct list_head *li = NULL, *lis = NULL; list_ele_t *rm_node; list_for_each_safe(li, lis, &q->head) { list_del(li); rm_node = container_of(li, list_ele_t, list); free(rm_node); } free(q); } } ``` * `q_insert_head()` * 產生新的 `list_ele_t * newh` ,並初始化其 `list_head` 。 * 從 `q` 內的 `list_head head` 的 next 插入。 * 示意圖,此為 circular 只是沒畫出指標。 ![](https://i.imgur.com/g0DSZia.png) ```clike= bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; if (q != NULL) { newh = malloc(sizeof(list_ele_t)); if (newh != NULL) { newh->value = strdup(s); INIT_LIST_HEAD(&newh->list); list_add(&newh->list, &q->head); q->size++; return true; } } return false; } ``` * `q_insert_tail()` * 原理和 `q_insert_head` 相同,只差在是由 prev 那端插入。 * 示意圖,此為 circular 只是沒畫出指標。 ![](https://i.imgur.com/UXXIYau.png) ```clike= bool q_insert_tail(queue_t *q, char *s) { list_ele_t *newh; if (q != NULL) { newh = malloc(sizeof(list_ele_t)); if (newh != NULL) { newh->value = strdup(s); INIT_LIST_HEAD(&newh->list); list_add_tail(&newh->list, &q->head); q->size++; return true; } } return false; } ``` * `q_remove_head()` * 所要刪除的節點位於 `q` 的下個節點。 * 使用 `container_of` 後得到刪除節點的 entry ,並在 `list_del` 刪除它。 * 最後在釋放其記憶體。 * 在刪除之後, `queue_t` 的 next 與 prev 指標會指向自己的 `list_head` 。 ![](https://i.imgur.com/mVHrCGq.png) ```clike= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (q != NULL) { if (q->head.next != &q->head) { list_ele_t *rm_node = container_of(q->head.next, list_ele_t, list); if (sp != NULL) { memcpy(sp, rm_node->value, bufsize); // copy the removed string to sp sp[bufsize - 1] = '\0'; // add terminator at the end } list_del(q->head.next); free(rm_node); // free the removed node q->size--; return true; } } return false; } ``` * `q_reverse()` * 使用 `list_for_each_safe` 尋訪每個節點,並將其 next 與 prev 指標對調。 * `list_for_each_safe` 的 `head` 不會走訪到,因為判斷式 `node != (head)` 會在走回 `head` 時跳出迴圈。 ```clike #define list_for_each_safe(node, safe, head) \ for (node = (head)->next, safe = node->next; node != (head); \ node = safe, safe = node->next) ``` * 所以還需要做最後一次指標對調才得以完成反轉佇列。 ```clike= void q_reverse(queue_t *q) { if (q != NULL) { struct list_head *li = NULL, *lis = NULL, *tmp = NULL; list_for_each_safe(li, lis, &q->head) { tmp = li->next; li->next = li->prev; li->prev = tmp; } tmp = li->next; li->next = li->prev; li->prev = tmp; } ``` --- ### `qtest.c` * 原本的 `qtest.c` 可以直接透過 `list_ele_t` 來獲得 value 。 * 但是由於修改為 doubly-linked list , 鏈接的部分不再是 `list_ele_t` 而是 `list_head` 。 * 這也是大部分 `qtest.c` 需要修改的部分,以 `show_queue()` 下要印出 queue 內容的部分為例。 * 修改前 : (透過走訪 `list_ele_t` 來印出 value ) ```clike= list_ele_t *e = q->head; if (exception_setup(true)) { while (ok && e && cnt < qcnt) { if (cnt < big_queue_size) report_noreturn(vlevel, cnt == 0 ? "%s" : " %s", e->value); e = e->next; cnt++; ok = ok && !error_check(); } } ``` * 需要先使用 `container_of` 來得知 `list_ele_t` 的 entry 位址才能取得其 value 。 * 修改後 : (透過 `list_head` 來走訪,使用 `container_of` 來取得 entry ) ```clike= struct list_head *e = &q->head; if (exception_setup(true)) { list_ele_t *ent; while (ok && cnt < qcnt) { e = e->next; ent = container_of(e, list_ele_t, list); if (cnt < big_queue_size) report_noreturn(vlevel, cnt == 0 ? "%s" : " %s", ent->value); cnt++; ok = ok && !error_check(); } } ``` * 其中的 while 迴圈內 `ok && e && cnt < qcnt` 判斷式改為 `ok && cnt < qcnt` ,原因是在 circular 的結構下, `list_head * e` 只可能會指回自己,不可能指向 `NULL` 。 * 在原 `show_queue()` 的 447 行,將 `if (e == NULL) ... else` 拿掉,原因是 `e` 在 circular 的結構下是不會為 `NULL` 的,若不移除,一定會進入 else 而產生 ERROR 。 ```clike=447 if (e == NULL) { if (cnt <= big_queue_size) report(vlevel, "]"); else report(vlevel, " ... ]"); } else { report(vlevel, " ... ]"); report( vlevel, "ERROR: Either list has cycle, or queue has more than %d elements", qcnt); ok = false; } ``` --- ### 結果 * `make test` 也可以達到 ==100/100== * 但是相較於原本的 `lab0-c` 除了程式碼更加精簡外,效率不進反退,也可能是我實作的問題 :confused: * `lab0-c` 的 `perf stat ./scripts/driver.py` ``` Performance counter stats for './scripts/driver.py': 514.600727 task-clock (msec) # 1.001 CPUs utilized 33 context-switches # 0.064 K/sec 0 cpu-migrations # 0.000 K/sec 99,410 page-faults # 0.193 M/sec 1,835,221,189 cycles # 3.566 GHz 4,152,494,737 instructions # 2.26 insn per cycle 803,315,987 branches # 1561.047 M/sec 1,004,655 branch-misses # 0.13% of all branches 0.513896153 seconds time elapsed ``` * `lab-list` 的 `perf stat ./scripts/driver.py` ``` Performance counter stats for './scripts/driver.py': 574.978929 task-clock (msec) # 1.001 CPUs utilized 39 context-switches # 0.068 K/sec 8 cpu-migrations # 0.014 K/sec 115,044 page-faults # 0.200 M/sec 2,110,763,505 cycles # 3.671 GHz 4,498,759,005 instructions # 2.13 insn per cycle 831,566,851 branches # 1446.256 M/sec 1,089,808 branch-misses # 0.13% of all branches 0.574192893 seconds time elapsed ```