contributed by < johnnylord
>
sysprog2019
Linux kernel 中充斥著大量對於 linked list 的操作和資料結構。使用 macro 來實做 linked list,最大的考量點應該是效能的問題。一般的 function call 需要額外的成本來維護其 stack frame,例如參數的傳遞,返回地址,新舊的 stack pointer 等等。
以下實驗 function call 和 macro 在組合語言中的差別
其實觀看上面的組合語言,可以發現其實 main 的部份都差不多,差別就在 add
function 的組合語言,每次呼叫 add
就得在額外做這些指令,分配新的 stack frame,和參數的傳遞。
透過上面的觀察發現一個很有趣的點,傳遞到 add
的參數,其空間不一定會在當前的 stack frame 中。不過若是 add
裡面有再做其他宣告或操作,則 compiler 會更新 $rsp
的值,使得傳遞的參數在當前的 stack frame 中。
每個 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 等等。
我們可以在 /proc/[pid]/maps
中觀看每個 process 的 memory map,此檔案中的每一行紀錄就對應著相對應的 VMA。
super_blocks
,會以 linked list 的方式串連所有個當前掛載的 file system (struct super_block
)。inode
是在其 file system 底下的物件,如一般檔案,目錄,連結等等。雖然他是有 linked list 的方式做管理,但是在 VFS 中,有相關的 cache,如 inode cache, 和 dentry cache,做存取的加速。GNU C 上說若要使用 typeof
的功能在 ISO C99 上,必須改為 __typeof__
。
typeof
的用處讓開發者可以容易撰寫更通用的程式碼,不用 case by case 的撰寫相對應的程式碼,但是做相同的事情。
typeof
的參數為一個 expression
或是 type
,傳化為此 expression
或 type
的 type。
以下是一個小測試
上術用到了很多 GCC extension。__extension__
是用來告訴 Compiler 說我將會使用一些非原本 C 有的語法,忽略其警告。而 ({ ... })
是 statement in expression 的語法,他是一個 expression,且回傳最後一項 statement 的值。
上述第四行,會計算出某個 type(通常是 struct
型別) 的物件的起始位置,藉由從已知此物件中的某個 member,減去到包含此 member 的物件的起始位置的位址的位移。
看巨集 offsetof
是如何算出偏移量。
(TYPE *)0
會指向 0x00
的位置。由於我們將指標做了轉型,所以其後接續 ->MEMBER
的操作是系統認得的,得到 MEMBER
的位址。由於我們的起始點是 0x00,所以 MEMBER 的位址,就相當於 MEMBER 到此 TYPE 起始位置的偏移量。
以下是使用上面兩個巨集的實驗
LIST_POISONING
這樣的設計有何意義?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
,我們會觸發一個通知給系統,因為我們存取到系統預定義的除錯位址。
一個重要的事實是 linux 中 linked list 的開頭指標並不包含 linked list 中所串連的 node 該有的資料負載,意思是開頭的資料和 nodes 的資料並無相關,linked list 的操作只關心這些 nodes,開頭指標只是一個基準點。因此 linux 不會刪除或修改開頭的資料,這使得開頭指標成為程式邏輯判斷的可靠指示。
基於上述的事實,linux 採用環狀 linked list 時將得到大大的好處,它可以不用考慮 edge case,操作 linked list 並不因為操作位置的不同而有不同的動作。
可以看到 list_del
並不會因為傳進去的 head
的位置不同,而有不同的操作。
list_for_each_safe
和 list_for_each
的差異在哪?“safe” 在執行時期的影響為何?這兩個巨集都是用來走訪 linked list 用的,但是delete
這項操作只能在 list_for_each_safe
中.
由於 list_for_each_safe
會預先儲存當前 node 的下一個 node(safe node),所以即使在當回的迴圈中將 node 才 linked list 中移除,也可以繼續走訪到下一個節點(safe node)
它讓程式開發者能夠更專注的在程式邏輯上,隱藏了繁瑣的細節。這不僅讓程式的維護更容易,程式碼也更簡潔明瞭。
以下就是一個簡單的例子,用 list_for_each_safe
代替複雜的 for 迴圈設定
大型多人的程式專案,程式碼需要有相對應的說明文件,例如函式的描述,參數的型態,回傳值等等。這些說明文件若能在開發過程中,透過特殊的註解語法,讓產生說明文件和程式開發同步進行會減去很多工作。
第三方軟題 Doxygen
就是一個專為程式專案產生說明文件的軟體,只要我們在程式碼中用 Doxygen
指定的特殊註解語法,便可以讓 Doxygen
為我們產生說明文件。例如 @param
用於描述函數或方法的參數信息。
tests/
目錄底下的 unit test 的作用為何?就軟體工程來說的精神為何?tests/
下的文件用於測試函式庫中提供的函數。例如,list_add.c
用於測試 list.h
中的list_add()
函數。
軟體工程很重要的精神,其中一個就是希望盡可能減少錯誤,提供高質量的軟體。因此,測試是軟件工程中必不可少的一部分。