# 2019q1 Homework1 (list) contributed by < `johnnylord` > ###### tags: `sysprog2019` ## 為何 Linux 採用 macro 來實作 linked list?一般的 function call 有何成本? Linux kernel 中充斥著大量對於 linked list 的操作和資料結構。使用 macro 來實做 linked list,最大的考量點應該是效能的問題。一般的 function call 需要額外的成本來維護其 stack frame,例如參數的傳遞,返回地址,新舊的 stack pointer 等等。 以下實驗 function call 和 macro 在組合語言中的差別 * Function based ```clike #include <stdio.h> int add(int a, int b) { return a + b; } int main(void) { int a = 10, b = 100; printf("%d\n", add(a, b)); return 0; } ``` ```shell $ gdb -q func Reading symbols from func...done. (gdb) b *main Breakpoint 1 at 0x65e: file func.c, line 8. (gdb) b *add Breakpoint 2 at 0x64a: file func.c, line 4. (gdb) r Starting program: /home/johnnylord/Desktop/linux-list/func Breakpoint 1, main () at func.c:8 8 int main(void) { (gdb) disassemble Dump of assembler code for function main: => 0x000055555555465e <+0>: push %rbp 0x000055555555465f <+1>: mov %rsp,%rbp 0x0000555555554662 <+4>: sub $0x10,%rsp 0x0000555555554666 <+8>: movl $0xa,-0x8(%rbp) 0x000055555555466d <+15>: movl $0x64,-0x4(%rbp) 0x0000555555554674 <+22>: mov -0x4(%rbp),%edx 0x0000555555554677 <+25>: mov -0x8(%rbp),%eax 0x000055555555467a <+28>: mov %edx,%esi 0x000055555555467c <+30>: mov %eax,%edi 0x000055555555467e <+32>: callq 0x55555555464a <add> 0x0000555555554683 <+37>: mov %eax,%esi 0x0000555555554685 <+39>: lea 0x98(%rip),%rdi # 0x555555554724 0x000055555555468c <+46>: mov $0x0,%eax 0x0000555555554691 <+51>: callq 0x555555554520 <printf@plt> 0x0000555555554696 <+56>: mov $0x0,%eax 0x000055555555469b <+61>: leaveq 0x000055555555469c <+62>: retq End of assembler dump. (gdb) c Continuing. Breakpoint 2, add (a=0, b=0) at func.c:4 4 { (gdb) disassemble Dump of assembler code for function add: => 0x000055555555464a <+0>: push %rbp 0x000055555555464b <+1>: mov %rsp,%rbp 0x000055555555464e <+4>: mov %edi,-0x4(%rbp) 0x0000555555554651 <+7>: mov %esi,-0x8(%rbp) 0x0000555555554654 <+10>: mov -0x4(%rbp),%edx 0x0000555555554657 <+13>: mov -0x8(%rbp),%eax 0x000055555555465a <+16>: add %edx,%eax 0x000055555555465c <+18>: pop %rbp 0x000055555555465d <+19>: retq End of assembler dump. ``` * Macro based ```clike #include <stdio.h> #define ADD(a, b) a + b int main() { int a = 10, b = 100; printf("%d\n", ADD(a, b)); return 0; } ``` ```shell $ gdb -q macro Reading symbols from macro...done. (gdb) b *main Breakpoint 1 at 0x64a: file macro.c, line 6. (gdb) r Starting program: /home/johnnylord/Desktop/linux-list/macro Breakpoint 1, main () at macro.c:6 6 { (gdb) disassemble Dump of assembler code for function main: => 0x000055555555464a <+0>: push %rbp 0x000055555555464b <+1>: mov %rsp,%rbp 0x000055555555464e <+4>: sub $0x10,%rsp 0x0000555555554652 <+8>: movl $0xa,-0x8(%rbp) 0x0000555555554659 <+15>: movl $0x64,-0x4(%rbp) 0x0000555555554660 <+22>: mov -0x8(%rbp),%edx 0x0000555555554663 <+25>: mov -0x4(%rbp),%eax 0x0000555555554666 <+28>: add %edx,%eax 0x0000555555554668 <+30>: mov %eax,%esi 0x000055555555466a <+32>: lea 0xa3(%rip),%rdi # 0x555555554714 0x0000555555554671 <+39>: mov $0x0,%eax 0x0000555555554676 <+44>: callq 0x555555554520 <printf@plt> 0x000055555555467b <+49>: mov $0x0,%eax 0x0000555555554680 <+54>: leaveq 0x0000555555554681 <+55>: retq End of assembler dump. ``` 其實觀看上面的組合語言,可以發現其實 main 的部份都差不多,差別就在 `add` function 的組合語言,每次呼叫 `add` 就得在額外做這些指令,分配新的 stack frame,和參數的傳遞。 ``` => 0x000055555555464a <+0>: push %rbp 0x000055555555464b <+1>: mov %rsp,%rbp 0x000055555555464e <+4>: mov %edi,-0x4(%rbp) 0x0000555555554651 <+7>: mov %esi,-0x8(%rbp) ``` :::info 透過上面的觀察發現一個很有趣的點,傳遞到 `add` 的參數,**其空間不一定會在當前的 stack frame 中**。不過若是 `add` 裡面有再做其他宣告或操作,則 compiler 會更新 `$rsp` 的值,使得傳遞的參數在當前的 stack frame 中。 ::: ## Linux 應用 linked list 在哪些場合?舉三個案例並附上對應程式碼,需要解說,以及揣摩對應的考量 1. **virtual memory area** **[include/linux/mm_types.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/mm_types.h#L261)** ```clike struct vm_area_struct { /* The first cache line has the info for VMA tree walking. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next, *vm_prev; struct rb_node vm_rb; /* ... */ } ``` 每個 process (`struct task_struct`) 都會有其自己的 memory descriptor(`mm` field in `task_struct`)。而 `mm` (`struct mm_struct`) 紀錄著所有屬於這個 process 的 VMA。VMA 全名為 virtual memory area,照其字面翻譯就是紀錄著這個 process 的 virtual memory 中的某些區域,如 data segment, text segment 等等。 **[fs/proc/task_mmu.c](https://elixir.bootlin.com/linux/v4.18/source/fs/proc/task_mmu.c#L297)** ```clike static void show_map_vma(struct seq_file *m, struct vm_area_struct *vma, int is_pid) { struct mm_struct *mm = vma->vm_mm; struct file *file = vma->vm_file; vm_flags_t flags = vma->vm_flags; unsigned long ino = 0; unsigned long long pgoff = 0; unsigned long start, end; dev_t dev = 0; const char *name = NULL; if (file) { struct inode *inode = file_inode(vma->vm_file); dev = inode->i_sb->s_dev; ino = inode->i_ino; pgoff = ((loff_t)vma->vm_pgoff) << PAGE_SHIFT; } start = vma->vm_start; end = vma->vm_end; show_vma_header_prefix(m, start, end, flags, pgoff, dev, ino); /* ... */ } ``` 我們可以在 `/proc/[pid]/maps` 中觀看每個 process 的 memory map,此檔案中的每一行紀錄就對應著相對應的 VMA。 2. **Mounted file system** Linux 允許我們並行的使用多個 file system,但是我們需要能夠追蹤當前系統中有多少個被掛載的裝置或 file system,所以在 kernel 中有個變數 `super_blocks`,會以 linked list 的方式串連所有個當前掛載的 file system (`struct super_block`)。 **[fs/super.c](https://elixir.bootlin.com/linux/v4.18/source/fs/super.c#L42)** ```clike static LIST_HEAD(super_blocks); ``` **[include/linux/fs.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/fs.h#L1348)** ```clike struct super_block { struct list_head s_list; /* Keep this first */ dev_t s_dev; /* search index; _not_ kdev_t */ unsigned char s_blocksize_bits; unsigned long s_blocksize; loff_t s_maxbytes; /* Max file size */ struct file_system_type *s_type; /* ... */ } ``` 3. **Inodes in file system** 每個 file system 都有其需要管理的資料,`inode` 是在其 file system 底下的物件,如一般檔案,目錄,連結等等。雖然他是有 linked list 的方式做管理,但是在 VFS 中,有相關的 cache,如 inode cache, 和 dentry cache,做存取的加速。 **[include/linux/fs.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/fs.h#L1348)** ```clike struct super_block { /* ... */ struct list_head s_inodes; /* ... */ } ``` ## GNU extension 的 typeof 有何作用?在程式碼中扮演什麼角色? GNU C 上說若要使用 `typeof` 的功能在 ISO C99 上,必須改為 `__typeof__`。 `typeof` 的用處讓開發者可以容易撰寫更通用的程式碼,不用 case by case 的撰寫相對應的程式碼,但是做相同的事情。 `typeof` 的參數為一個 **`expression`** 或是 **`type`**,傳化為此 `expression` 或 `type` 的 type。 以下是一個小測試 ```clike __typeof__(int) a = 10; // int a = 10; __typeof__(&a) b = &a; // int *b = &a; ``` ## 解釋以下巨集的原理 ```cpp= #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` 上術用到了很多 GCC extension。`__extension__` 是用來告訴 Compiler 說我將會使用一些非原本 C 有的語法,忽略其警告。而 `({ ... })` 是 statement in expression 的語法,他是一個 expression,且回傳最後一項 statement 的值。 上述第四行,會計算出某個 type(通常是 `struct` 型別) 的物件的起始位置,藉由從已知此物件中的某個 member,減去到包含此 member 的物件的起始位置的位址的位移。 看巨集 `offsetof` 是如何算出偏移量。 ```cpp #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) ``` `(TYPE *)0` 會指向 `0x00` 的位置。由於我們將指標做了轉型,所以其後接續 `->MEMBER` 的操作是系統認得的,得到 `MEMBER` 的位址。由於我們的起始點是 0x00,所以 MEMBER 的位址,就相當於 MEMBER 到此 TYPE 起始位置的偏移量。 以下是使用上面兩個巨集的實驗 ```clike #include <stdio.h> #define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *) 0)->MEMBER) struct list_head { struct list_head *next, *prev; }; struct A { char c[24]; struct list_head list; }; int main() { printf("Offset from list elment's address to struct A's address: %d\n", (int) offsetof(struct A, list)); return 0; } ``` ```shell $ gcc -gdwarf -g3 -o offset offset.c $ gdb offset (gdb) break main Breakpoint 1 at 0x64e: file test.c, line 16. (gdb) r Starting program: /home/johnnylord/Kernel/linux-list/examples/offset Breakpoint 1, main () at offset.c:14 16 printf("Offset from list elment's address to struct A's address: %d\n", (gdb) n Offset from list elment's address to struct A's address: 24 17 return 0; (gdb) macro expand offsetof(struct A, list) expands to: ((size_t)&((struct A*)0)->list) (gdb) p (struct A*)0 $1 = (struct A *) 0x0 (gdb) p &((struct A*)0)->list $2 = (struct list_head **) 0x18 ``` ## `LIST_POISONING` 這樣的設計有何意義? ```cpp /** * list_del() - Remove a list node from the list * @node: pointer to the node * * The node is only removed from the list. Neither the memory of the removed * node nor the memory of the entry containing the node is free'd. The node * has to be handled like an uninitialized node. Accessing the next or prev * pointer of the node is not safe. * * Unlinked, initialized nodes are also uninitialized after 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. */ 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 } ``` `LIST_POISONING` 的存在是為了除錯用的。當我們從 linked list 中刪除(delete)一個 node,此 node 只是脫離原本的 linked list(從上述程式碼可以得知並沒有做 `free` 的動作)並且將其指向 `LIST_POISONING` 這個位址。被刪除的 node 可能之後會被合併到其他的 linked list 或用作其他用途。 但是當開發者有意的想要完全刪除這個 node,但是忘記做 free 的動作時,這個 node 所指到的位置 `LIST_POISONING` 便發會了作用。因為當我們下次對這個 node 做操作時,如 `node->next` 或 `node->pre`,我們會觸發一個通知給系統,因為我們存取到系統預定義的除錯位址。 ## Linked list 採用環狀是基於哪些考量? 一個重要的事實是 linux 中 linked list 的開頭指標並不包含 linked list 中所串連的 node 該有的資料負載,意思是開頭的資料和 nodes 的資料並無相關,linked list 的操作只關心這些 nodes,開頭指標只是一個基準點。因此 linux 不會刪除或修改開頭的資料,這使得開頭指標成為程式邏輯判斷的可靠指示。 基於上述的事實,linux 採用環狀 linked list 時將得到大大的好處,它可以不用考慮 edge case,操作 linked list 並不因為操作位置的不同而有不同的動作。 ```clike 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 } ``` 可以看到 `list_del` 並不會因為傳進去的 `head` 的位置不同,而有不同的操作。 ## `list_for_each_safe` 和 `list_for_each` 的差異在哪?“safe” 在執行時期的影響為何? 這兩個巨集都是用來走訪 linked list 用的,但是`delete` 這項操作只能在 `list_for_each_safe` 中. ```cpp /** * 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) ``` 由於 `list_for_each_safe` 會預先儲存當前 node 的下一個 node(safe node),所以即使在當回的迴圈中將 node 才 linked list 中移除,也可以繼續走訪到下一個節點(safe node) ## for_each 風格的開發方式對程式開發者的影響為何? 它讓程式開發者能夠更專注的在程式邏輯上,隱藏了繁瑣的細節。這不僅讓程式的維護更容易,程式碼也更簡潔明瞭。 以下就是一個簡單的例子,用 `list_for_each_safe` 代替複雜的 for 迴圈設定 ```clike #define list_for_each_safe(node, safe, head) \ for (node = (head)->next, safe = node->next; node != (head); \ node = safe, safe = node->next) ``` ## 程式註解裡頭大量存在 @ 符號,這有何意義?你能否應用在後續的程式開發呢? 大型多人的程式專案,程式碼需要有相對應的說明文件,例如函式的描述,參數的型態,回傳值等等。這些說明文件若能在開發過程中,透過特殊的註解語法,讓產生說明文件和程式開發同步進行會減去很多工作。 第三方軟題 `Doxygen` 就是一個專為程式專案產生說明文件的軟體,只要我們在程式碼中用 `Doxygen` 指定的特殊註解語法,便可以讓 `Doxygen` 為我們產生說明文件。例如 `@param` 用於描述函數或方法的參數信息。 ## `tests/` 目錄底下的 unit test 的作用為何?就軟體工程來說的精神為何? `tests/` 下的文件用於測試函式庫中提供的函數。例如,`list_add.c`用於測試 `list.h` 中的`list_add()` 函數。 軟體工程很重要的精神,其中一個就是希望盡可能減少錯誤,提供高質量的軟體。因此,測試是軟件工程中必不可少的一部分。