# D05: list contributed by < `e94046165` > ## 生日悖論 ### 如何透過近似的運算式,估算上述機率計算呢?附上有效的 LaTeX 算式 若今天進入一個有22個人的房間內,加上自己有23個人。若用一年365天來計算,則在這個房間至少有一個人與我生日相同的機率已經超過50% 。 https://en.wikipedia.org/wiki/Birthday_problem 其實這個議題在 wiki 上就有很完整的分析了,晚點有空再來把證明用自己的話打一次。先繼續往下看。 ### [Birthday problem](https://en.wikipedia.org/wiki/Birthday_problem) 對資訊安全的影響? 第一眼看到這個問題,我聯想到的是密碼的安全性,~~畢竟我是一個熱愛猜密碼的人,曾經為了作業寫不出來去猜別人的 Moodle 借東西來交(汗~~ 經過一番搜尋之後發現,其實不只是猜密碼這麼簡單,更重要的是可以用來破解加密訊息。以下節錄自 [birthday attack in wiki](https://en.wikipedia.org/wiki/Birthday_attack) The attack depends on the higher likelihood of collisions found between random attack attempts and a fixed degree of permutations. With a birthday attack, it is possible to find a collision of a hash function in 2^n/2^, with 2^n^ being the classical preimage resistance security。 利用 birthday problem 的概念,只要輸入 2^n/2^ 筆資料,就可以令一個 2^n^ 大小的 Hash function 發生 collision。以下是證明(參考自[看起來很猛的開放課程](https://www.coursera.org/learn/crypto/lecture/pyR4I/generic-birthday-attack)) **Proof** $\ let\ r_1.....r_n\ 屬於\ hashfunction\ key\ 值集合\{1\ ...\ B\}$且 Be independent、identically distributed integers. 證明當 $n = 1.2\times B^\dfrac{1}{2} , Pr[\exists\ i\neq j, r_i=r_j]=0.5$ 即發生碰撞機率達 50% $Pr[\exists\ i\neq j, r_i=r_j]=1-Pr[\forall i\neq j,r_i\neq r_j]\\ =1-(\dfrac{B}{B}\cdot\dfrac{B-1}{B}\cdot\dfrac{B-2}{B}...\cdot\dfrac{B-n+1}{B})\\ =1-(1-\prod_{i=0}^{n-1}\frac{i}{B})\geq1-\prod_{i=0}^{n-1}e^\frac{i}{B}=1-e^{-\frac{1}{B}\sum_{i=1}^{n-1}\ \ \ i}\\ \geq e^{-\frac{n^2}{2B}}$ 將 $n = 1.2\times B^\frac{1}{2}$代入上式可得 53...% 這個數據顯示出,只要輸入B^1\2^的資料量,就有一半機率發生 collision 上面證明出來的結果可以用來檢視一開始的 birthday problem B = 365 代入,n = 1.2*365^1\2^ = 22.9 ## Linux 風格的 linked list #### 為何 Linux 採用 macro 來實作 linked list?一般的 function call 有何成本? 參考自[阿夢的程式設計天地](https://dotblogs.com.tw/ace_dream/2016/01/16/function_macro) 巨集( macro )和函式( function )的比較 函式是拿時間換取空間的,透過執行時期在函式堆疊的切換,相同邏輯區塊只要抽出成為函式,則大家共用同一份程式即可(在記憶體中只有一份實體,較節省記憶體空間)。函式實務上還有另一個好處是可以取得函式位址,也就是說可以把函式當參數。 而巨集則是拿空間換取時間,在編譯之前,前置處理器就將該程式替換至各個呼叫區塊,所以相同的邏輯在會重複出現在各個地方,但是由於執行時間不需要在函式堆疊切換,減少時間的耗損。 也就是說選擇使用 macro 還是 function call 來實作,取決於著重在時間還是空間的成本。 #### Linux 應用 linked list 在哪些場合?舉三個案例並附上對應程式碼,需要解說,以及揣摩對應的考量 首先想到的是上學期修作業系統的時候,寫了一支模擬 CPU 排班的程式,其中各個 queue(如 ready queue、waiting queue 等等)用 linked list 來實作感覺上不錯。因此在 linux source code 關於 CPU 排班的部份試著找尋 linked list 的蹤跡: 1. [include/linux/sched.h](https://elixir.bootlin.com/linux/latest/source/include/linux/sched.h#L524) 裡的 task_struct... 嗯....是個看不到底的 struct 呢,裡面不只有指向其他 task_struct 的 link,還有一大堆名稱很熟悉的成員與其他 struct ,以下僅挑幾個我們較關心的 link 出來討論: ```C= struct task_struct __rcu *real_parent; /* Recipient of SIGCHLD, wait4() reports: */ struct task_struct __rcu *parent; /* * Children/sibling form the list of natural children: */ struct list_head children; struct list_head sibling; struct task_struct *group_leader; ``` 在 Linux 系统中,所有 Process 之間都有著直接或間接的聯繫,每个 process 都有其 parent process,也可能擁有 0 或 多個 children。而同一個 parent process 底下的所有 children 為各自的 sibling 。 ~~這個 task_struct 根本把我能想到 link 都用上了,我為什麼不選個只有單純 next link 的 struct 就好。~~ 在追蹤這些 link 使用情形時,遇到了瓶頸~~還有瓶蓋~~:想觀察一個 kernel 裡的 function 運作情形時,可能必須追蹤數十個其他的 function,因為這些 function 往往是一個 call 一個,一直綿延下去的。更令人感到害怕的是,如果一個 kernel 裡的 function 改變了某個變數值或是鏈結(如 task_struct 裡的成員),那麼其他使用到這個被改變變數的函式也都要考慮進去,頭有夠痛... 在所有跟排程相關的函式裡都會看到這個 task_struct ,用來追蹤 child process 進行狀況的 ptrace 就會使用到 parent、real_parent、child 等指標,來確定 process 間的親屬關係。 #### GNU extension 的 typeof 有何作用?在程式碼中扮演什麼角色? #### typeof [參考資料](https://feichashao.com/kernel-doubly-linked-list/) typeof 用來回傳輸入變數的 type,應用上可以靈活的定義為變數定義 type (可能依照不同的情境需要),下面試著使用 typeof 做些練習: ```C #define swap(a, b) \ do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0) int main(int agrc, char **argv) { char a = 'a'; char b = 'b'; printf("a = %c, b = %c\n",a ,b); swap(a, b); printf("new a = %c, new b = %c\n",a ,b); int c = 1; int d = 2; printf("c = %d, d = %d\n",c ,d); swap(c, d); printf("new c = %d,new d = %d\n",c ,d); } ``` ``` root@acnes-X550JX:~/tests#./typeof a = a, b = b new a = b, new b = a c = 1, d = 2 new c = 2,new d = 1 ``` 可以看到同一個 macro swap 在使用了 typeof 便可以處理不同 type 的 swap 功能。 #### 解釋以下 container_of 巨集的原理 ```clike #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` container_of 巨集分為兩個部份: 第一部份: ```C const typeof (((type *)0)->member)*__pmember = (ptr); ``` 通過 typeof 定義一個 member 指針類型的指針變量 __pmember,並將其賦值為 ptr 第二部份: ```C (type *)((char *)__pmember - offsetof(type, member)); ``` 透過 offsetof 巨集算出 member 在 struct 中的偏移,後用 member 的地址 __pmember 減掉偏移,得到 struct 的起始位置。offsetof 的巧妙之處在於將地址 0 強制轉換為 type 類型的指針,編譯器認為 0 是一個有效的地址,從而認為 0 是 type 指針的起始地址。如此一來 Member 的地址就為偏移量。可以做一個小實驗來驗證這件事: ```clike= #include <stdio.h> #include <string.h> #include <stdlib.h> #define offsetof(type, member) ((size_t)&((type *)0)->member) //注意此時將 0 轉型成 Type 類型的指針 #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ printf("__pmember address:%p\n", __pmember); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) typedef struct student_info { int id; char name[32]; int age; }student_info; int main() { size_t off_set = 0; off_set = offsetof(student_info, id); printf("id offset: %u\n",off_set); off_set = offsetof(student_info, name); printf("name offset: %u\n",off_set); off_set = offsetof(student_info, age); printf("age offset: %u\n",off_set); return 0; } ``` ``` root@acnes-X550JX:~/tests#./container_of id offset: 0 name offset: 4 age offset: 36 ``` 可以看到此時輸出正確的偏移量。 那麼把 0 改成 1 會發生什麼事呢? ``` root@acnes-X550JX:~/tests#./container_of id offset: 1 name offset: 5 age offset: 37 ``` 如同預期的,全部 Member 的偏移量都增加了 1。 ```clike= #include <stdio.h> #include <string.h> #include <stdlib.h> #define offsetof(type, member) ((size_t)&((type *)0)->member) #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ printf("__pmember address:%p\n", __pmember); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) typedef struct student_info { int id; char name[32]; int age; }student_info; int main() { size_t off_set = 0; off_set = offsetof(student_info, id); printf("id offset: %u\n",off_set); off_set = offsetof(student_info, name); printf("name offset: %u\n",off_set); off_set = offsetof(student_info, age); printf("age offset: %u\n",off_set); printf("\n"); student_info *stu = (student_info *)malloc(sizeof(student_info)); printf("stu->age address:%p\n", &(stu->age)); student_info *ptr = container_of(&(stu->age), student_info, age); printf("\n"); printf("stu address:%p\n", stu); printf("ptr address:%p\n", ptr); return 0; } ``` 測試結果: ``` id offset: 0 name offset: 4 age offset: 36 stu->age address:0xfc8444 __pmember address:0xfc8444 stu address:0xfc8420 ptr address:0xfc8420 ``` 從上面的測試結果可以清楚的了解到 offsetof 與 container_of 的作用: 首先是在透過 offsetof 得到某個 member 量後就可以用已知的 member 的 addr 減掉偏移量,獲得 struct 起始位置的記憶體地址。 本來覺得好像可以用 sizeof 做一樣的事,但是在不知道 struct 裡有多少成員、不知道已知 member 離 struct 開頭地址多遠的情況下,只用 sizeof 獲得某成員佔的空間好像沒有太大的意義。 #### 除了你熟悉的 add 和 delete 操作,list.h 還定義一系列操作,為什麼呢?這些有什麼益處? 由於看了一整天的程式碼,~~豆頁ㄔ艮痛~~,不如這次先把 list.h 裡所有註解都看一次好了,至少它們是我認識的語言(?): ``` /* * Simple doubly linked list implementation. * * Some of the internal functions ("__xxx") are useful when * manipulating whole lists rather than single entries, as * sometimes we already know the next/prev entries and we can * generate better code by using them directly rather than * using the generic single-entry routines. */ ``` 從這段註解可以看到,除了基本版本的 functions,list.h 裡還提供了很多針對不同情境設計的 functions,他們都用 ___xxx 標示出來,有的是出於安全考量,而有的是為了效能考量。例如: ``` /* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */ ``` ```clike 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); } ``` 這個 __list_add 可以讓我們在任何已知 prev 、 next 的情況下將新節點 new 加到 list 中特定的位置,而更有趣的是也可以當我們只是要將 new 加到 head 之後,對應的 list_add 也是 call 這個 function ```clike static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } ``` 因為置入 head 之後,所以 prev 當然是 head,而 next 則是原來的第一個節點,也就是 head->next。 ``` /** * list_add - add a new entry * @new: new entry to be added * @head: list head to add it after * * Insert a new entry after the specified head. * This is good for implementing stacks. */ ``` 在這個加入 entry 在 head 後一個的 function 旁有一段註解,最後一句提到 "This is good for implementing stacks"因為這符合 stack LIFO 的特性,猜測在 list.h 中應該也可以找到一個刪除 head 後的第一個 entry 的 list_del function 兩者合起來就可以實作 stack 的 pop/push 動作。 雖只看了幾個 functions 但是可以深刻的感受到,如果依照特定的需求,找到適合的 function,實作起來應該可以事半功倍,整份 list.h 應也是因應不同的場景需求,每個 linked list 的動作(add、del...)都維護了數種不同的實作方式。 #### LIST_POISONING 這樣的設計有何意義? #### linked list 採用環狀是基於哪些考量?