# 2019q1 Homework1 (list) contributed by <`MetalheadKen`> :::info :mega: 題目出處 * [F02: list](https://hackmd.io/s/S12jCWKHN) ::: ### 自我檢查清單 [TOC] ### 為何 Linux 採用 macro 來實作 linked list?一般的 function call 有何成本? * macro * [用法](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256#page=158):`#define macro_name replacement-list new-line` * 用 `#define` 來定義一個常數或指令 * 在編譯前,preprocessor 會把 `macro_name` 替換為先前定義好的常數或指令 * 沒有 function call 在執行時期的成本,但會增加 code size,且比起 function 的寫法較不易 debug 和維護 * function * [用法](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256#page=153) * 由於在執行時期呼叫 function 時,需要將 return address 和 function parameter 藉由 `push` 和 `pop` 進 stack 裡,因此在執行時期會耗費大量時間成本 * TODO * 比較 macro、function 和 inline function 的 code size 和 execution time ### Linux 應用 linked list 在哪些場合?舉三個案例並附上對應程式碼,需要解說,以及揣摩對應的考量 ### GNU extension 的 [typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) 有何作用?在程式碼中扮演什麼角色? * `typeof` 會回傳參數內的資料型態 * 例如: ```C int x[10]; typeof(x) y; // 相當於 int y[10]; typeof(x[0]) z; // 相當於 int z; ``` * 在 GCC manual 中的範例可以看到使用了 `typeof` 可以依參數內的變數來動態的去調整其資料型態,可以讓程式變得更有彈性以及更容易維護 ```C #define max(a, b) \ ({ typeof(a) _a = (a); \ typeof(b) _b = (b); \ _a > _b ? _a : _b; )}) ``` ### 解釋 `container_of` 巨集的原理 ```C #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` * `container_of` * 為藉由結構體中的成員的記憶體位址,找到該結構體最一開始的記憶體位址 * `__extension__` * 根據 [GCC manual](https://gcc.gnu.org/onlinedocs/gcc/Alternate-Keywords.html#Alternate-Keywords) 中提到,在編譯的參數裡添加 `-pedantic` 或 `-ansi` 的話,編譯器會對所有使用到 GNU extension 的地方提出警告,但是並不會對包含在 `__extension__` 的運算式產生警告 > You can prevent such warnings within one expression by writing `__extension__` before the expression. `__extension__` has no effect aside from this. * `__typeof__` * 根據 [GCC manual](https://gcc.gnu.org/onlinedocs/gcc/Alternate-Keywords.html#Alternate-Keywords) 中提到,若想要在 ISO C 程式中可以相容的話,可以把 `typeof` 改寫為 `__typeof__` > If you are writing a header file that must work when included in ISO C programs, write `__typeof__` instead of `typeof`. * `__typeof__(((type *) 0)->member)` * 一般來說對 0 作 dereference 應該會造成 segmentation fault,但是在 [GCC manual](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) 中提到在使用 `typeof` 時只有在執行時期才能確定的 expression 才會去求值 > The operand of typeof is evaluated for its side effects if and only if it is an expression of variably modified type or the name of such a type. * `const __typeof__(((type *) 0)->member) *__pmember = (ptr);` * 宣告一個指向結構體中的成員 `member` 的資料型態的指標 `__pmember`,並指派一個變數 `ptr` * 若程式設計師在使用 `container_of` 時傳進去的參數其 `ptr` 的資料型態與 `member` 的資料型態不相符時,在編譯期間會輸出 `error: initialization from incompatible pointer type` 的錯誤訊息,可藉此在 compile time 檢查出人為上的疏失 * `offsetof` * `size_t offsetof(type, member)` * 依據 [man pages](http://man7.org/linux/man-pages/man3/offsetof.3.html) 說明,`offsetof` 巨集會回傳在結構體 `type` 中,成員 `member` 的位址偏移量 * `(type *) ((char *) __pmember - offsetof(type, member));` * 先把 `__pmember` 轉型成 pointer to char 的資料型態,藉此讓之後的 pointer arithmetics 時的 offset 位移量可以為 n * `sizeof(char)` > Ref: [cplusplus.com](http://www.cplusplus.com/doc/tutorial/pointers) * 之後把 `__pmember` 與 `member` 在結構體 `type` 中的位址偏移量相減即可得到該結構體最一開始的記憶體位址 * `({...})` * 在 [GCC manual](https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html) 的第 6.1 章說明此寫法為一個 GNU extension,並提及最終會回傳在 compound statement (大括號) 內最後一個 expression 的計算後的結果 ### 除了你熟悉的 add 和 delete 操作,`list.h` 還定義一系列操作,為什麼呢?這些有什麼益處? * `list_add` 和 `list_add_tail` * 若有 LIFO 的需要可以使用 `list_add`,而若有 FIFO 的需求則可以使用 `list_add_tail` * `list_del_init` * 與一般的 `list_del` 不同的是,`list_del_init` 在呼叫 `list_del` 來把節點從 list 移除後,還會呼叫 `INIT_LIST_HEAD` 來對已經移除的節點再作一次初始化 * 用意為因為在 `list_del` 中並未對已移除的節點其 `prev` 和 `next` 做初始化,有可能會對已移除的節點造成誤用的情況發生 * `list_empty` * 檢查 list 中的 `head` 是否沒有任何節點連接 * `list_is_singular` * 檢查 list 中的 `head` 是否只有一個節點連接 * `list_splice` 系列 * 把兩條 list 連接在一起,不需一直呼叫 `list_add` 來把一個個的節點連接上去,增加效率 * `list_cut_position` * 已某個節點為基準點來切割成兩個 list * `list_move` 系列 * 把 list 中的某一個節點移到最前頭或最後頭 * `list_entry` 系列 * 在 `list.h` 可以看到 doubly linked list 的定義為 ```clike struct list_head { struct list_head *prev; struct list_head *next; }; ``` 而在使用時可以定義如下 ```clike struct listitem { int val; struct list_head list; }; struct listitem *item; ``` 並在走訪整個 doubly linked list 時可撰寫為 ```clike struct list_head *node; struct list_head *curr = item->list; for (node = curr->next; node != curr; node = node->next) { ... } ``` 從上述程式碼中可以發現到,在走訪時我們只能取得到 `struct listitem` 中的 `struct list_head` 也就是其節點,但無法取得到節點中的資料 `val`,但利用 `list_entry` 和其巨集內使用的 `container_of` 可以取得到 `struct listitem` 的最一開始的記憶體位址也跟著可以取得到節點中的資料 `val` 了 總體來說,藉由定義了一系列操作,並將所有常用的操作用巨集或函式包裝起來,可以 * 不必再重新造輪子,加速開發時程 * 使其抽象化,讓程式可讀性增加 * [Linux kernel coding style](https://www.kernel.org/doc/html/v4.18/process/coding-style.html#functions) 中提到函式的寫法要又短又甜而且只做一件事,並且內容要可以在一兩頁內顯示完畢 > Functions should be short and sweet, and do just one thing. They should fit on one or two screenfuls of text (the ISO/ANSI screen size is 80x24, as we all know), and do one thing and do that well. * Robert C. Martin, *Clean Code: A Handbook of Agile Software Craftsmanship* > The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. *[LIFO]: Last-In-First-Out *[FIFO]: First-In-First-Out ### `LIST_POISONING` 這樣的設計有何意義? 在 `list.h` 的函式 `list_del` 的註解中提到在節點移除後,會將該節點的 `prev` 和 `next` 指向一個非法存取的記憶體位置,若之後有任何程式想要去存取已經移除掉的節點的話,即造成 segmentation fault,可藉此用來在除錯階段得知哪裡有誤 ```c /** * list_del() - Remove a list node from the list * * ... * * 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. */ 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; #ifdef LIST_POISONING node->prev = (struct list_head *) (0x00100100); node->next = (struct list_head *) (0x00200200); #endif } ``` 根據 [LITMUS wiki](https://wiki.litmus-rt.org/litmus/KernelDebugging) 中可以得知 `0x00100100` 和 `0x00200200` 為 Well-known addresses,目的為可以在 kernel oops 的時候發現是何種錯誤,因而挑選的一個特殊的位址 ### linked list 採用環狀基於哪些考量? * 從任何一個節點皆可以走訪整個 list * 若想存取該節點的前一個節點無須從頭開始走訪 * 在刪除某一節點時,其時間複雜度可為 $O(1)$ 而不是 singly linked list 的 $O(n)$ * [Linux Device Driver 3/e](https://static.lwn.net/images/pdf/LDD3/ch11.pdf#page=10) 提到若 linked list 是環狀的,那麼每一個節點的操作都是一樣的,因此不用像 singly linked list 一樣要去維護 `head` 和 `tail` > Since Linux lists are circular, the head of the list is not generally differnet from any other entry. ### 什麼情境會需要對 linked list 做排序呢?舉出真實世界的應用,最好在作業系統核心出現 ### 什麼情境會需要找到 [第 k 大/小的元素](https://www.geeksforgeeks.org/kth-smallestlargest-element-unsorted-array) 呢?又,你打算如何實作? ### `list_for_each_safe` 和 `list_for_each` 的差異在哪?"safe" 在執行時期的影響為何? ```clike /** * list_for_each - iterate over list nodes * @node: list_head pointer used as iterator * @head: pointer to the head of the list * * The nodes and the head of the list must must be kept unmodified while * iterating through it. Any modifications to the the list will cause undefined * behavior. */ #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) /** * list_for_each_safe - iterate over list nodes and allow deletes * @node: list_head pointer used as iterator * @safe: list_head pointer used to store info for next entry in list * @head: pointer to the head of the list * * The current node (iterator) is allowed to be removed from the list. Any * other modifications to the the list will cause undefined behavior. */ #define list_for_each_safe(node, safe, head) \ for (node = (head)->next, safe = node->next; node != (head); \ node = safe, safe = node->next) ``` * 差別在於多了一個變數 `safe` 來預先儲存下一個節點 * 當使用 `list_del_init` 或是使用 `LIST_POISONING` 來對移除的節點作初始化時,需要多一個變數來預先儲存下一個的節點,否則在移除節點後,因已經移除的節點的 `prev` 和 `next` 已經被改變了,故在執行 `node = node->next` 時會造成不預期的結果 ### for_each 風格的開發方式對程式開發者的影響為何? * 提示︰對照其他程式語言,如 Perl 和 Python ### 程式註解裡頭大量存在 `@` 符號,這有何意義?你能否應用在後續的程式開發呢? * 提示︰對照看 Doxygen ### `tests/` 目錄底下的 unit test 的作用為何?就軟體工程來說的精神為何? ### `tests/` 目錄的 unit test 可如何持續精進和改善呢?