--- tags: System Software --- # Linux Linked List ## Material > [2021q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2021-quiz1) > [GitHub](https://github.com/RinHizakura/linux-list) ## Introduction [linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 是 Linux 作業系統中相當實用的 doubly linked list 封裝,只要在自定義的結構中加入 `struct list_head`,就可以搭配 Linux 中一系列的 linked list 操作(詳見[The Linux Kernel API - List Management Functions](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html)) 來建立自定義結構的 linked list。在撰寫 kernel module 時會有機會跟其打交道。除了會使用到相關的 API 之外,其中的實作細節也很值得我們細細品味,因此,何嘗不是個好注意呢來看看程式碼呢? ## Start From Simple 我們可以先從 [linux-list](https://github.com/RinHizakura/linux-list),由成大的[黃敬群(jserv)](https://zh.wikipedia.org/wiki/%E9%BB%83%E6%95%AC%E7%BE%A4) 教授所建立的專案來一探究竟。這是一個和 Linux linked list 很相似的實作,但做了適當的簡化,並只留下一部分函數。 ### Code Explanation #### `container_of()` ```cpp #if defined(__GNUC__) #define __LIST_HAVE_TYPEOF 1 #endif #ifndef container_of #ifdef __LIST_HAVE_TYPEOF #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) #else #define container_of(ptr, type, member) \ ((type *) ((char *) (ptr) -offsetof(type, member))) #endif #endif ``` * 如果 GNUC extension 為開啟狀態(`#if defined(__GNUC__)`), `__LIST_HAVE_TYPEOF` 會被設為 1 * `__extension__`: 根據 [3.4 Options Controlling C Dialect](https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html),當語法被要求和 [ANSI C](https://en.wikipedia.org/wiki/ANSI_C) 相容時(如 gcc 中加入 `-ansi` 參數),像是 `typeof` 等 gcc extension 將不能被使用。而在 [6.48 Alternate Keywords](https://gcc.gnu.org/onlinedocs/gcc/Alternate-Keywords.html#index-_005f_005fextension_005f_005f) 則提到可以用替換關鍵字的方法實作沒有 extension 時的替代方案。但意思就是這種設計會存在用到 extension 和沒用到的兩種實作,編譯器會對於使用到 extension 的部份扔出警告,可以透過 `__extension__` 避免警告的產生。 * 使用 extension 的實作版本: * `({ ... })` 是 [statement expression](https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html) 的語法 * 透過 `((type *) 0)->member` 取得目標 member 的 instance,[typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) 會取得其型別。(注意到 pointer 0 沒有被 dereference 且 instance 沒被 access,所以合法),於是可以得到正確型別的 `ptr` 指標,存入 `__pmember` * 此步驟是為了做 type checking,確保輸入的 `ptr` 是 `type` 中的 `member` 型別之指標 * 將 `__pmember` 減去該成員在 struct 中的偏移就可以得到 struct 的開頭位址 * [`offsetof`](https://www.man7.org/linux/man-pages/man3/offsetof.3.html) 定義在 `stddef.h` 中 * 計算 offset 時要轉成 `char *`,以確保 address 的計算符合預期(可參考 [The (char *) casting in container_of() macro in linux kernel](https://stackoverflow.com/questions/20421910/the-char-casting-in-container-of-macro-in-linux-kernel) 的說明) * 不使用 extension 的實作版本: * 直接省略 type checking #### `LIST_HEAD()` / `INIT_LIST_HEAD()` ```cpp #define LIST_HEAD(head) struct list_head head = {&(head), &(head)} static inline void INIT_LIST_HEAD(struct list_head *head) { head->next = head; head->prev = head; } ``` * 初始化 `struct list_head`,先將 `next` 和 `prev` 都指向自己 * `head` 的 `next` 所指代表 linked list 結構的開頭而 `prev` 則指向結構的結尾 #### `list_add` / `list_add_tail` ```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; } 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; } ``` * 將指定的 `node` 插入 linked list `head` 的開頭或者結尾 #### `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; #ifdef LIST_POISONING node->prev = (struct list_head *) (0x00100100); node->next = (struct list_head *) (0x00200200); #endif } ``` * 將 `node` 從其所屬的 linked list 結構中移除 * 需注意 `node` 本體甚至是包含 `node` 的結構所分配的記憶體皆未被釋放,僅僅是將 `node` 從其原本的 linked list 連接移除 * `LIST_POISONING` 使得被刪除的 `node` 對於 `next` 或者 `prev` 的存取會引發 build time 的 invalid memory access(如果系統禁止 predefined memory access) #### `list_del_init` ```cpp static inline void list_del_init(struct list_head *node) { list_del(node); INIT_LIST_HEAD(node); } ``` * 基於 `list_del`,但移除的 `node` 會額外呼叫 `INIT_LIST_HEAD` 把 `prev` 和 `next` 指向自己 #### `list_empty` ```cpp static inline int list_empty(const struct list_head *head) { return (head->next == head); } ``` * 檢查 `head` 的 `next` 是否指向自己,確認 list 是否為 empty 狀態 #### `list_singular` ```cpp static inline int list_is_singular(const struct list_head *head) { return (!list_empty(head) && head->prev == head->next); } ``` * 如果 `head` 非 empty 狀態且 `prev` 和 `next` 是同一個節點,表示 linked list 只有一個節點 #### `list_splice` / `list_splice_tail` ```cpp static inline void list_splice(struct list_head *list, struct list_head *head) { struct list_head *head_first = head->next; struct list_head *list_first = list->next; struct list_head *list_last = list->prev; if (list_empty(list)) return; head->next = list_first; list_first->prev = head; list_last->next = head_first; head_first->prev = list_last; } static inline void list_splice_tail(struct list_head *list, struct list_head *head) { struct list_head *head_last = head->prev; struct list_head *list_first = list->next; struct list_head *list_last = list->prev; if (list_empty(list)) return; head->prev = list_last; list_last->next = head; list_first->prev = head_last; head_last->next = list_first; } ``` * 將 `list` 的所有 node 都插入到 `head` 的開始 / 結束位置中 * 注意 `list` 本身仍維持原貌 #### `list_spice_init` / `list_splice_tail_init` ```cpp static inline void list_splice_init(struct list_head *list, struct list_head *head) { list_splice(list, head); INIT_LIST_HEAD(list); } static inline void list_splice_tail_init(struct list_head *list, struct list_head *head) { list_splice_tail(list, head); INIT_LIST_HEAD(list); } ``` * 和 `list_splice` / `list_splice_tail` 類似,但移除的 `list` 會額外呼叫 `INIT_LIST_HEAD` 把 `prev` 和 `next` 指向自己 #### `list_cut_position` ```cpp static inline void list_cut_position(struct list_head *head_to, struct list_head *head_from, struct list_head *node) { struct list_head *head_from_first = head_from->next; if (list_empty(head_from)) return; if (head_from == node) { INIT_LIST_HEAD(head_to); return; } head_from->next = node->next; head_from->next->prev = head_from; head_to->prev = node; node->next = head_to; head_to->next = head_from_first; head_to->next->prev = head_to; } ``` * 將從 `head_from` 的第一個節點到 `node` 間的一系列節點都移動到 `head_to` 上 * `head_to` 必須是 empty 狀態(`next` 和 `prev` 都指向自己),否則可能發生 memory leak #### `list_move` / `list_move_tail` ```cpp static inline void list_move(struct list_head *node, struct list_head *head) { list_del(node); list_add(node, head); } static inline void list_move_tail(struct list_head *node, struct list_head *head) { list_del(node); list_add_tail(node, head); } ``` * 將 `node` 從原本的 linked list 移動到另一個 linked list `head` 的開頭或尾端 #### `list_entry` ```cpp #define list_entry(node, type, member) container_of(node, type, member) ``` * `container_of` 等價的 wrapper #### `list_first_entry` / `list_last_entry` ```cpp #define list_first_entry(head, type, member) \ list_entry((head)->next, type, member) #define list_last_entry(head, type, member) \ list_entry((head)->prev, type, member) ``` * 取得 linked list 的開頭或者結尾的 entry #### `list_for_each` ```cpp #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` * linear 遍歷整個 linked list * `node` 和 `head` 不能在迴圈中被更改,否則行為不可預期 #### `list_for_each_entry` ```cpp #ifdef __LIST_HAVE_TYPEOF #define list_for_each_entry(entry, head, member) \ for (entry = list_entry((head)->next, __typeof__(*entry), member); \ &entry->member != (head); \ entry = list_entry(entry->member.next, __typeof__(*entry), member)) #endif ``` * 遍歷包含 `struct list_head` 的另外一個結構之 entry * `node` 和 `head` 不能在迴圈中被更改,否則行為不可預期 * 因為 `typeof` 之限制,只能在 GNUC 下使用 #### `list_for_each_safe` / `list_for_each_entry_safe` ```cpp #define list_for_each_safe(node, safe, head) \ for (node = (head)->next, safe = node->next; node != (head); \ node = safe, safe = node->next) #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)) ``` * 透過額外的 `safe` 紀錄每個 iteration 所操作的節點(entry)的下一個節點(entry),因此刪除目前的節點(entry)是可被允許的,其他操作則同為不可預期行為 ### Example #### Quicksort 我們可以從一些範例來看實際的使用。例如在專案中的 [quick-sort.c](https://github.com/RinHizakura/linux-list/blob/master/examples/quick-sort.c) 程式碼。 ```cpp static void list_qsort(struct list_head *head) { struct list_head list_less, list_greater; struct listitem *pivot; struct listitem *item = NULL, *is = NULL; if (list_empty(head) || list_is_singular(head)) return; INIT_LIST_HEAD(&list_less); INIT_LIST_HEAD(&list_greater); ``` * 先確認 `head` 所承載的 linked list 有兩個以上 entry,否則就返回,不需要被排序 * 先以 `INIT_LIST_HEAD` 初始化另外兩個 list 結構,它們分別是用來插入 entry 中比 pivot 小或者其他的節點 ```cpp pivot = list_first_entry(head, struct listitem, list); list_del(&pivot->list); list_for_each_entry_safe (item, is, head, list) { if (cmpint(&item->i, &pivot->i) < 0) list_move_tail(&item->list, &list_less); else list_move(&item->list, &list_greater); } ``` * 透過 `list_first_entry` 取得第一個 entry 選為 pivot * `list_del` 將該 pivot entry 從 linked list 中移除 * 遍歷整個 linked list,`cmpint` 回傳兩個指標中的值相減的數值,因此小於零表 `item->i` 的值比 `pivot` 的值小,加入 `list_less`,反之則同理 ```cpp list_qsort(&list_less); list_qsort(&list_greater); list_add(&pivot->list, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); } ``` * 遞迴呼叫將 `list_less` 和 `list_greater` 排序 * 在 `list_for_each_entry_safe` 中,`list_move_tail` 和 `list_move` 會將所有原本在 `head` 中的節點移出,因此首先 `list_add` 加入 `pivot`,再把已經排好的 `list_less` 放在 pivot 前 `list_greater` 放在 pivot 後,完成排序 #### Quicksort Without Recursive Call 上面的程式碼是由 jserv 老師所實作,有了對其的理解後,我們也可以自己來實際使用一下相關的 API。這裡我用 [2021q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2021-quiz1) 的作業要求作為範例,參考 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/),嘗試一下基於 Linux like 的 linked list 實作類似的非遞迴 quick sort。 ```cpp static void list_qsort_no_recursive(struct list_head *head) { struct listitem *begin[MAX_LEN], *end[MAX_LEN], *L, *R; struct listitem pivot; int i = 0; begin[0] = list_first_entry(head, struct listitem, list); end[0] = list_last_entry(head, struct listitem, list); ``` * `begin` 和 `end` 表示在 linked list 中的排序目標的開頭和結尾,因此最初是整個 linked list 的頭至尾 ```cpp while (i >= 0) { L = begin[i]; R = end[i]; ``` * `begin` 和 `end` 的效果類似 stack,會填入每一輪要處理的節點開頭至結尾 ,因此先取出該輪的頭尾至 `L` 和 `R` ```cpp if (L != R && &begin[i]->list != head) { // pivot is the actual address of L pivot = *begin[i]; if (i == MAX_LEN - 1) { assert(-1); return; } ``` * 接著,以最開頭的節點作為 pivot * `i == MAX_LEN - 1` 的目的是額外檢查這輪如果填入 `begin` 和 `end` 是否會超出原本給定的陣列大小,因為我們所給予的空間是有限的 ```cpp while (L != R) { while (R->i >= pivot.i && L != R) R = list_entry(R->list.prev, struct listitem, list); if (L != R) { L->i = R->i; L = list_entry(L->list.next, struct listitem, list); } while (L->i <= pivot.i && L != R) L = list_entry(L->list.next, struct listitem, list); if (L != R) { R->i = L->i; R = list_entry(R->list.prev, struct listitem, list); } } ``` * 否則的話,從結尾的點(`R`)一直往 `prev` 前進,找到比 `pivot` 值更小的節點的話就將其值移到開頭的 `L` 去 * 同理,從開頭的點(`L`)一直往 `next` 前進,找到比 `pivot` 值更大的節點的話,就將其值移到結尾的 `R` 去 * `L != R` 則負責判斷當 `L` 往 `next` 而 `R` 往 `prev` 移動碰在一起時,當 `L == R` 時,不再做上述的操作,離開迴圈 ```cpp L->i = pivot.i; begin[i + 1] = list_entry(L->list.next, struct listitem, list); end[i + 1] = end[i]; end[i++] = L; ``` * 此時 `L` 所在地方是 `pivot` 的值應在的正確位置,因此將 `pivot` 的值填入 `L` * 此時需要被處理的排序是 `pivot` 往後到結尾的一段,兩個點分別是 `L` 的 `next`,和這輪的 `end[i]` * 另一段則是 `pivot` 以前從原本的 `begin[i]` 到 `L` 一段 ```cpp } else i--; } } ``` * 如果 `L == R` 或者 `&begin[i]->list == head`,表示此段 linked list 已經不需要再做處理,`i--` 類似於 pop stack 的概念 :::warning Some issue: * 原本表示此實作在 array 下效能有可能較好,那麼在 doubly linked list 條件下,這個非遞迴的實作會有比較好的效能嗎 * 上面的實作中,用交換 list entry 中的內容的方式來實現,但考慮到整個 entry 的結構如果變得巨大,有沒有可能改成調成 link 方式來交換會比較有效率,要如何改寫? ::: ### Compare to Linux 比較 [linux-list](https://github.com/RinHizakura/linux-list) 與 [linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 的實作,會發現最大的差異在於 `WRITE_ONCE` macro 的使用。 我們可以比較 `list_add` 來探討差異的細節 * [linux-list](https://github.com/RinHizakura/linux-list#L94) ```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; } ``` * [linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h#L84) ```cpp static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { if (!__list_add_valid(new, prev, next)) return; next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); } static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } ``` 乍看之下,如果把 `WRITE_ONCE(prev->next, new)` 替換成 `prev->next = new`,兩者其實不是也沒甚麼差異嗎? `WRITE_ONCE` 定義在 [linux/tools/include/linux/compiler.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/compiler.h#L184) 路徑下,我們可以從註解中對其作用進行理解: > Prevent the compiler from merging or refetching reads or writes. The compiler is also forbidden from reordering successive instances of READ_ONCE and WRITE_ONCE, but only when the compiler is aware of some particular ordering. One way to make the compiler aware of ordering is to put the two invocations of READ_ONCE or WRITE_ONCE in different C statements. 簡而言之,通過 `WRITE_ONCE` 和 `READ_ONCE` 的使用,可以避免編譯器合併、切割、甚至重排特定的記憶體讀寫操作。下面是 `WRITE_ONCE` 的定義: ```cpp #define WRITE_ONCE(x, val) \ ({ \ union { typeof(x) __val; char __c[1]; } __u = \ { .__val = (val) }; \ __write_once_size(&(x), __u.__c, sizeof(x)); \ __u.__val; \ }) ``` 我們可以再次看到 [statement expression](https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html) 技巧的應用。此外,可以看到 `WRITE_ONCE` 把 `val` 被寫入 union 結構中,使其與一個 `char _c[1]` 共享空間。藉由以 union 為基礎的 [`type-punning`](https://en.wikipedia.org/wiki/Type_punning#Use_of_union) 技巧,可避免違反 strict aliasing 規則,使得 `__c` 能作為這個 union 的指標來使用,從而允許編譯器最佳化。 需注意 type tunning 的方式有不同類型,而在 gcc 中,使用 union 進行 type punning 的方式被作為語言的擴展並合法(詳見 [gcc: nonbugs](https://gcc.gnu.org/bugs/#nonbugs)),該方式不會違反 gcc 的 [strict aliasing](https://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization) 規則。但在其他編譯器中則可能因為違反規則導致 undefined behavior。 > 延伸閱讀: > * [What is the strict aliasing rule?](https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule) > * [Unions and type-punning](https://stackoverflow.com/questions/25664848/unions-and-type-punning) 關鍵的 `__write_once_size` 的任務則是把 `__val` 寫入至 `x` 中,但通過巧妙的設計避免了編譯器將其做錯誤的優化,細節請見下面的 `__write_once_size` 說明 ```cpp typedef __u8 __attribute__((__may_alias__)) __u8_alias_t; typedef __u16 __attribute__((__may_alias__)) __u16_alias_t; typedef __u32 __attribute__((__may_alias__)) __u32_alias_t; typedef __u64 __attribute__((__may_alias__)) __u64_alias_t; ``` 在深入 `__write_once_size` 之前,先來看看這個型別定義。 首先需要了解何謂 [aliasing](https://en.wikipedia.org/wiki/Aliasing_(computing)): 其所指為同一個位置可被多個 symbol 存取時,這種情形會造成編譯器優化上的難處。根據 [Options That Control Optimization](https://gcc.gnu.org/onlinedocs/gcc-4.9.1/gcc/Optimize-Options.html), `-fstrict-aliasing` 參數會讓編譯器預設程式撰寫者不會將相似類型(例如 int 和 unsigned int) 以外的 pointer 指向同一個記憶體位置,以允許做更激進的優化,這個參數在 `-O2`、`-O3`、`-Os` 下會預設開啟。 但我們可以根據 [Specifying Attributes of Types](https://gcc.gnu.org/onlinedocs/gcc-4.0.2/gcc/Type-Attributes.html) 中所提到,使用 `__may_alias__` 告知編譯器此型別允許 aliasing 的發生,避免對此的過度優化導致非預期的程式行為,至於 Linux 這裡特別定義 `*_alias_t` 的目的可以參考註解: > Following functions are taken from kernel sources and break aliasing rules in their original form. > > While kernel is compiled with -fno-strict-aliasing, perf uses -Wstrict-aliasing=3 which makes build fail under gcc 4.4. Using extra `__may_alias__` type to allow aliasing in this case. ```cpp static __always_inline void __write_once_size(volatile void *p, void *res, int size) { switch (size) { case 1: *(volatile __u8_alias_t *) p = *(__u8_alias_t *) res; break; case 2: *(volatile __u16_alias_t *) p = *(__u16_alias_t *) res; break; case 4: *(volatile __u32_alias_t *) p = *(__u32_alias_t *) res; break; case 8: *(volatile __u64_alias_t *) p = *(__u64_alias_t *) res; break; default: barrier(); __builtin_memcpy((void *)p, (const void *)res, size); barrier(); } } ``` 對於 1 / 2 / 4 / 8 bytes 的變數,可以直接搭配 [`volatile`](https://en.wikipedia.org/wiki/Volatile_(computer_programming)) 的使用提示編譯器不要優化這個寫入操作。`volatile` 所考慮的情境在於: 變數的值即使從程式中乍看不會改變,在每一次存取時實際則有可能被更動(例如該變數可能指向某個 memory map I/O,會被硬體更動值,或者被 signal handler 和 main program 共用的 global variable),因此可以避免寫入操作被錯誤優化。 對於其他長度的變數,則可以透過 memory barriers 搭配 `memcpy` 的方法來寫入。`barrier()` 如同其名稱是個屏障,告訴編譯器不能 `barrier()` 前的 load/store 移動到 `barrier()` 後,反之亦然。 值得一題的是,Linux 中採用了這種 "volatile access" 而非直接將 object 標註為 volitile 屬性,在 [doc: volatile considered evil](https://lwn.net/Articles/233482/) 中有提及後者可能隱藏的問題。 > 延伸閱讀: [Nine ways to break your systems code using volatile](https://blog.regehr.org/archives/28) ## list_sort 在 [linux/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 中,可以看到 linux 特別針對系統層級的考量所設計的 linked list sort。在最頂端 GitHub 連結中也可以看到整合至使用者層級的實作。接下來,嘗試探討其中的設計原理和可能的考量。 ```cpp /** * list_sort - sort a list * @priv: private data, opaque to list_sort(), passed to @cmp * @head: the list to sort * @cmp: the elements comparison function * ``` 感謝詳細的註解,我們可以不必貿然的挑戰程式碼,先探討其中的實作內容。首先是需要的三個參數 * `priv` 是一個 pointer,可以用來傳輸 `cmp` 需要的參數 * `head` 要被排序的 list * `cmp` 是比較*自定義大小*的 function pointer ```cpp /*... * The comparison funtion @cmp must return > 0 if @a should sort after * @b ("@a > @b" if you want an ascending sort), and <= 0 if @a should * sort before @b *or* their original order should be preserved. It is * always called with the element that came first in the input in @a, * and list_sort is a stable sort, so it is not necessary to distinguish * the @a < @b and @a == @b cases. * * This is compatible with two styles of @cmp function: * - The traditional style which returns <0 / =0 / >0, or * - Returning a boolean 0/1. * The latter offers a chance to save a few cycles in the comparison * (which is used by e.g. plug_ctx_cmp() in block/blk-mq.c). * * A good way to write a multi-word comparison is:: * * if (a->high != b->high) * return a->high > b->high; * if (a->middle != b->middle) * return a->middle > b->middle; * return a->low > b->low; ``` 註解中說明了在比較 `a` `b` 時,如果 `a` 需至於 `b` 的之後,則 `cmp` 需回傳大於 0 的值。list_sort 是 [stable sort](https://en.wikipedia.org/wiki/Category:Stable_sorts),所以不必區分小於或等於 0 的分別。 最後則展示了一個簡單的 multi-word comparision。 ```cpp /* This mergesort is as eager as possible while always performing at least * 2:1 balanced merges. Given two pending sublists of size 2^k, they are * merged to a size-2^(k+1) list as soon as we have 2^k following elements. * * Thus, it will avoid cache thrashing as long as 3*2^k elements can * fit into the cache. Not quite as good as a fully-eager bottom-up * mergesort, but it does use 0.2*n fewer comparisons, so is faster in * the common case that everything fits into L1. ``` 接著,註解中提到這裡的 merge sort 實作重點。方法會保持兩個要被 merge 的 list 至少是 2 : 1 的狀態(比較大的 list 至多是小的的兩倍),這可以有效的降低比較的次數。list_sort 與一般的 fully-eager bottom-up mergesort 方法不同,後者只要出現兩個 2^k 大小的 list,就會立刻被合併。而前者的方法是在出現兩個 $2^k$ 大小的 list 的時候,不立即合併,反之,一直等到下一個 2^k 的 list 被蒐集起來時,前者的這兩個 linked list 才會被合併起來。只要這 2 : 1 的 list 可以完全被放到 cache 裡,就可以避免 cache thrashing。 在 [[RESEND PATCH v2 5/5] lib/list_sort: Optimize number of calls to comparison function ](https://www.mail-archive.com/linux-kernel@vger.kernel.org/msg1957556.html) 可以看到更完整的敘述。 ```cpp /* The merging is controlled by "count", the number of elements in the * pending lists. This is beautiully simple code, but rather subtle. * * Each time we increment "count", we set one bit (bit k) and clear * bits k-1 .. 0. Each time this happens (except the very first time * for each bit, when count increments to 2^k), we merge two lists of * size 2^k into one list of size 2^(k+1). ``` 那麼前述的規則該怎麼維持呢? 方法中會透過一個 `count` 來紀錄 pending 的節點數量。當目前的 count + 1 後,會設置第 k 個 bit 為 1,k-1 至 0 bit 為 0 時(除了 `count` 為 $2^k - 1$ 的情境),我們就把兩個 $2^k$ 長度的 list 做合併。細節在後面探討程式碼時會再嘗試說明得更清楚。 後續註解有點掌握不到意思(~~菜英文~~),所以下面直接從程式碼的部份做理解並解釋。 ```cpp= __attribute__((nonnull(2,3))) void list_sort(void *priv, struct list_head *head, int (*cmp)(void *priv, struct list_head *a, struct list_head *b)) { struct list_head *list = head->next, *pending = NULL; size_t count = 0; /* Count of pending */ if (list == head->prev) /* Zero or one elements */ return; /* Convert to a null-terminated singly-linked list. */ head->prev->next = NULL; ``` 首先破壞掉 linked list 的 circular 結構,最後的節點(`head->prev`) 的 `next` 指向 NULL。 * [`__attribute__((nonnull))`](https://www.keil.com/support/man/docs/armcc/armcc_chr1359124976631.htm) 讓 compiler 可以對指定位置的 pointer 是 NULL 時發出警告 ```cpp=14 ... do { size_t bits; struct list_head **tail = &pending; /* Find the least-significant clear bit in count */ for (bits = count; bits & 1; bits >>= 1) tail = &(*tail)->prev; /* Do the indicated merge */ if (likely(bits)) { struct list_head *a = *tail, *b = a->prev; a = merge(priv, (cmp_func)cmp, b, a); /* Install the merged result in place of the inputs */ a->prev = b->prev; *tail = a; } /* Move one element from input list to pending */ list->prev = pending; pending = list; list = list->next; pending->next = NULL; count++; } while (list); ``` 這裡我們可以先來觀察 `count` 與 merge 的進行之規律。注意到這裡的格式中,左邊是在該 count 的 iter 開始時的狀態,右邊則是經可能的 merge 後把自己加入 pending list 後的狀態。 * $2^0$ -> count = 1 = $1_{2}$ -> $2^0$ + $2^0$ (no merge) * $2^0$ + $2^0$ -> count = 2 = $10_{2}$ -> $2^1$ + $2^0$ (將 $10_{2}$ 加 1 的話 set bit 0,merge to 2^1) * $2^1$ + $2^0$ -> count = 3 = $11_{2}$ -> $2^1$ + $2^0$ + $2^0$ (no merge) * $2^1$ + $2^0$ + $2^0$ -> count = 4 = $100_{2}$ -> 2 * $2^1$ + $2^0$ (將 $100_{2}$ 加 1 的話 set bit 0->merge to 2^1) * 2 * $2^1$ + $2^0$ -> count = 5 = $101_{2}$ -> $2^2$ + $2^0$ + $2^0$ (將 $101_{2}$ 加 1 的話 set bit 1->merge to 2^2) * $2^2$ + $2^0$ + $2^0$ -> count = 6 = $110_{2}$ -> $2^2$ + $2^1$ + $2^0$ (將 $110_{2}$ 加 1 的話 set bit 0->merge to 2^1) 可以觀察到,`count` 的最小位元的 0 假設在位置 k,根據規則就要 merge 出 $2^{k+1}$ 的 list (除了 `count` 為 $2^k - 1$)。而後面為 1 的 bit k - 1, k - 2 ... 0 可以代表 pending 裡有 $2^{k-1}, 2^{k-2} ...... 2^0$ 大小的 Llist 各一個,且因為 `*tail` 一開始就指向 $2^0$ 的那個 list,所以 `*tail` 往 prev 走 k 步就會找到 $2^k$ 大小的 list A,而 A 的 `prev` 是另一個 $2^k$ 大小的 list B,我們要把兩者 merge 在一起。 再回到程式碼,for 迴圈中透過 `count` 最低位元的連續 1 找到此輪要 merge 的兩個 list,且 bits 若不為 0 剛好會等同於我們提到要 merge 的 `count`。最後。每一輪的 list 會把自己的 `next` 設為 NULL 而 `prev` 指向 pending,並更新成原本的 `next` 所指向的下一個 node 繼續流程。 下面看一下 merge 具體的實作: ```cpp __attribute__((nonnull(2,3,4))) static struct list_head *merge(void *priv, cmp_func cmp, struct list_head *a, struct list_head *b) { struct list_head *head, **tail = &head; for (;;) { /* if equal, take 'a' -- important for sort stability */ if (cmp(priv, a, b) <= 0) { *tail = a; tail = &a->next; a = a->next; if (!a) { *tail = b; break; } } else { *tail = b; tail = &b->next; b = b->next; if (!b) { *tail = a; break; } } } return head; } ``` merge 接受參數的兩個 `list_head` a / b 後,`tail` 初始時指向一個 dummy pointer。然後 for 迴圈比較 a / b。如果 a 應該在 b 之前,則 `*tail` 改指向 a,並且 `tail` 被更新以指向 "指向 a 的下個節點的 pointer"(注意這裡沒多打一個指向XD),這個步驟等同把 a 加到一個新的 linked list 中,然後下一次改成比 a 的 `next` 和 b,反之也是類似道理。直到 `a` 或 `b` 的 `next` 為 NULL 時,把另一個加入 `*tail` 則完成。 繼續來看 list_sort 的實作。 ```cpp=41 /* End of input; merge together all the pending lists. */ list = pending; pending = pending->prev; for (;;) { struct list_head *next = pending->prev; if (!next) break; list = merge(priv, (cmp_func)cmp, pending, list); pending = next; } /* The final merge, rebuilding prev links */ merge_final(priv, (cmp_func)cmp, head, pending, list); } ``` 當所有的節點都被加入 pending 後,把所有的 pending 都 merge 在一起。刻意留下 pending 中的其一 `pending` 和其他的 pending merge 在一起的 `list` 去做 `merge_final`,後者的目的是因為 `merge` 只有做單向(`next`)的鏈結,需要把 `prev` 也接去正確的點上,並且回復至 circular linked list 的狀態。 ## Reference > * [WRITE_ONCE in linux kernel lists](https://stackoverflow.com/questions/34988277/write-once-in-linux-kernel-lists) > * [Linux内核中的READ_ONCE和WRITE_ONCE宏](https://blog.csdn.net/Roland_Sun/article/details/107365134) > * [编译乱序](https://zhuanlan.zhihu.com/p/102370222) ## TODO - [ ] 閱讀 [A comparative study of linked list sorting algorithms](https://pages.mtu.edu/~shene/PUBLICATIONS/1996/3Conline.pdf) 並將其落實於 [linux-list](https://github.com/RinHizakura/linux-list) 中