--- tags: LINUX KERNEL, LKI --- # Linux 核心原始程式碼巨集: `container_of` > 資料整理: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv) :::success `container_of` 巨集在 Linux 核心原始程式碼出現將近 7 千次 (v5.13),不僅在 linked list 和 hash table 一類通用資料結構中可簡化程式設計,甚至是 Linux 核心達成物件導向程式設計的關鍵機制之一。 若要征服 Linux 核心原始程式碼,對 `container_of` 巨集的掌握度絕對要充分。 ::: ## 跟你想像不同的 `struct` 考慮以下 C 程式: ```c struct data { short a; char b; double c; }; ``` 假設依據 `struct data` 建立的物件為 `x`,也就是宣告為 `struct data x;`,給定指標 `char *p = (char *) &x;` 你或許會認為物件 `x` 在記憶體 (明確來說,是指虛擬記憶體分頁) 展現的樣貌如下圖: ![](https://i.imgur.com/NihOvLg.png) 也就是 `data` 這個結構體中,成員 `a` (型態為 `short` 整數) 佔 2 個位元組、成員 `b` (型態為 `char`,在 C 語言規格保證至少佔 1 個位元組),以及緊接著出現的成員 `c` (型態為 `double`,在 C 語言規格中,至少需要 8 個位元組)。 我們常借助示意圖來理解事物,但也可能無意間偏離事實。以下是測試程式碼 (檔名: `data.c`) ```c= #include <stdio.h> struct data { short a; char b; double c; }; int main() { struct data x = {.a = 25, .b = 'A', .c = 12.45}; char *p = (char *) &x; printf("a=%d\n", *((short *) p)); p += sizeof(short); printf("b=%c\n", *((char *) p)); p += sizeof(char); printf("c=%lf\n", *((double *) p)); return 0; } ``` 編譯和執行: ```shell $ gcc -o data data.c $ ./data ``` 輸出的前 2 行符合預期,即 `a=25` 和 `b=A`,但 `c=` 的輸出跟預期的 `12.45` 不同,為什麼呢?你或許會納悶: > 成員 `c` 在結構體 `data` 中,不就是自開頭位移 `sizeof(short)` (即成員 `a`) 和 `sizeof(char)` (即成員 `b`) 嗎? 我們將第 15 行的 `return 0;` 換為以下程式碼: ```c=15 printf("p=%p, &x.c=%p\n", p, &(x.c)); ``` 在 Aarch64 (64 位元 Arm 架構) Linux 上執行會得到: (受限於 [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization),每次執行結果都可能不同,這不影響我們判斷) ``` p=0xffffd6161a4b, &x.c=0xffffd6161a50 ``` 從執行結果可知,表示式 `(char *) &x + sizeof(short) + sizeof(char)` 和 `&(x.c)` 實際是不同的數值,既然指標內含值不同,進行 dereference (即 `*` 操作) 當然不會得到預期的輸出。問題出在我們忽略編譯器為了滿足 alignment 需求,進行的 [structure padding](http://www.catb.org/esr/structure-packing/)。 解決方式之一,是修改上述程式碼的第 6 行,將 `};` 改為以下: ```c=6 } __attribute__((packed)); ``` 這裡的 `__attribute__((packed))` 是 GNU extension,引述 GCC 手冊 [Common Type Attributes](https://gcc.gnu.org/onlinedocs/gcc/Common-Type-Attributes.html): > This attribute, attached to a `struct`, `union`, or C++ `class` type definition, specifies that each of its members (other than zero-width bit-fields) is placed to minimize the memory required. This is equivalent to specifying the packed attribute on each of the members. 原來要額外加上 `packed` 屬性,結構體的成員實際排列和空間佔用才會跟程式碼順序一致,這是 C 語言程式設計其中一個陷阱,`packed` 過的結構體可能會犧牲資料存取的效率。 > 延伸閱讀: [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory) 為了提升程式碼的可攜性 (portability),C89/C99 提供 [offsetof](https://man7.org/linux/man-pages/man3/offsetof.3.html) 巨集,可接受給定成員的型態及成員的名稱,傳回「成員的位址減去 struct 的起始位址」。示意如下: ![](https://i.imgur.com/DYiZ1sd.jpg) [offsetof](https://man7.org/linux/man-pages/man3/offsetof.3.html) 定義於 `<stddef.h>`,因此我們應該在第 1 行後面新增一行: ```c #include <stddef.h> ``` 再將上述程式碼的第 13 行的 `p += sizeof(char);` 敘述換為以下: ```c p = (char *) &x + offsetof(struct data, c); ``` 這樣無論結構體是否 `packed`,均可符合預期地執行。 另外,如果你覺得 `offsetof(struct data, c)` 書寫很冗長,可換為: ```c offsetof(typeof(x), c) ``` 其中 [typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) 也是 GNU extension,可在編譯時期得到物件 `x` 或 `*p` (型態也是 `struct data`) 的型態,這樣改寫後,我們就聚焦在「物件」和「成員名稱」。 在一些老舊的編譯器,如 [Sun WorkShop Compiler](https://en.wikipedia.org/wiki/Oracle_Developer_Studio),用以下方式定義 `offsetof` 巨集: ```c #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER) ``` 但後來的 GCC 和 clang 就用 builtin function 取代,例如 `__builtin_offsetof`。Linux 核心原始程式碼 [include/linux/stddef.h](https://github.com/torvalds/linux/blob/master/include/linux/stddef.h) 定義 `offsetof` 巨集: ```c #ifdef __compiler_offsetof #define offsetof(TYPE, MEMBER) __compiler_offsetof(TYPE, MEMBER) #else #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) #endif ``` 上述程式碼已於 [commit 14e8307](https://github.com/torvalds/linux/commit/14e83077d55ff4b88fe39f5e98fb8230c2ccb4fb) 更改為: ```c #define offsetof(TYPE, MEMBER) __builtin_offsetof(TYPE, MEMBER) ``` 在較新的 GCC 中,對應有效的 `__compiler_offsetof` 巨集。當巨集展開時,針對老舊的編譯器可能會成為 `(size_t)&((TYPE *)0)->MEMBER` 敘述,其原理是先將 TYPE 型態結構體的地址變更為 `0`,然後再加上成員 `MEMBER` 的偏移量。值得思忖的是,`&((TYPE *)0)->MEMBER中` 這樣對地址為 `0` 的記憶體區域進行操作,難道不會導致程式崩潰嗎?答案是,不會! 編譯器處理 `&((TYPE *)0)->MEMBER` 時,不會真正去存取地址為 `0` 的記憶體區段,只是將 `0` 看做指標操作的一個運算元,除非額外有 dereference 操作。 ## `container_of` 巨集作為資料封裝的基礎 巨集 `container_of` 則在 [offsetof](https://man7.org/linux/man-pages/man3/offsetof.3.html) 的基礎上,擴充為「給定成員的位址、struct 的型態,及成員的名稱,傳回此 struct 的位址」,以下是示意圖 ![](https://i.imgur.com/IgayoN9.jpg) 請不要小看這巨集,畢竟大量在 Linux 核心原始程式碼採用的巨集,應有其獨到之處。在 `container_of` 巨集出現前,程式設計的思維往往是: 1. 給定結構體起始地址 2. 求出結構體特定成員的記憶體內容 3. 傳回結構體成員的地址,作日後存取使用 `container_of` 巨集則逆轉上述流程,特別在 C 語言程式設計中,我們通常會定義一系列公開介面 (interface),從而區隔各式實作 (implementation)。更明確來說,考慮以下 UML: ![](https://i.imgur.com/Ck9NHy3.png) `Object` 作為抽象的展現,是所有衍生物件的基底,`Vehicle` 正如其寓意,泛指交通工具,而 `Car` 和 `Truck` 就是特定的交通工具「模板」。在 C 語言可用以下程式碼描述: ```c typedef struct { int ref; } Object; typedef struct { Object base; /* Vehicle-specific members */ } Vehicle; typedef struct { Vehicle base; /* Car-specific members */ } Car; void vehicleStart(Vehicle *obj) { if (obj) printf("%x derived from %x\n", obj, obj->base); } int main(void) { Car c; vehicleStart((Vehicle *) &c); } ``` > 完整程式碼: [doxygen-oop-in-c](https://github.com/jserv/doxygen-oop-in-c) 這段程式碼雖然只是印出物件之間的「繼承」關係,但蘊含重要的特性: * `Car` 是實作本體,無論結構體再複雜,都有 `base` 這個成員,其型態為 `Vehicle` * 同理,`Vehicle` 也包含一個 `base` 成員,後者的型態為 `Object` * `Object` 是公開介面,內部只有 `ref` 成員,可充當 [reference counting](https://en.wikipedia.org/wiki/Reference_counting) 使用,本例沒有特別意義 於是,只要依循同樣的規則,無論有多少 `Car`, `Truck` 或更多的交通工具「模板」,我們都可藉由結構體的 `base` 成員,得知繼承關係,從而存取到上一層的公開資訊。物件導向程式設計的「封裝」(encapsulation)、繼承 (inheritance),和多型 (polymorphism) 都可運用這樣的手法來達成。 > 延伸閱讀: [你所不知道的 C 語言:物件導向程式設計篇](https://hackmd.io/@sysprog/c-oop) 舉例來說,Linux 核心內建 V4L2 (video for Linux APIs, version 2) 框架來提供豐富的 Linux 多媒體處理,原始程式碼 [drivers/media/i2c/imx214.c](https://github.com/torvalds/linux/blob/master/drivers/media/i2c/imx214.c) 則是其中一個裝置驅動程式,部分程式碼列表如下: ```c struct imx214 { struct device *dev; ... struct v4l2_ctrl_handler ctrls; ... }; static int imx214_set_ctrl(struct v4l2_ctrl *ctrl) { struct imx214 *imx214 = container_of(ctrl->handler, struct imx214, ctrls); ... /* Applying V4L2 control value only happens * when power is up for streaming */ if (!pm_runtime_get_if_in_use(imx214->dev)) return 0; ... } static const struct v4l2_ctrl_ops imx214_ctrl_ops = { .s_ctrl = imx214_set_ctrl, }; ``` 顯然,定義在 [drivers/media/i2c/imx214.c](https://github.com/torvalds/linux/blob/master/drivers/media/i2c/imx214.c) 的結構體 `imx214` 是 imx214 這個裝置驅動程式特有的定義,在其他 C 程式也無從得知,但 `v4l2_` 開頭的結構體則是 V4L2 框架的公開介面,上述程式碼的 `imx214_set_ctrl` 函式最終會被註冊為 V4L2 的一個實作,利用 `container_of` 巨集,我們就可隱藏 imx214 實作細節,但又能存取到 `imx214` 結構體的開頭,從而繼續存取到其他實作相關的成員。 > 延伸閱讀: [解析 Linux 共享記憶體機制](https://hackmd.io/@sysprog/linux-shared-memory) 也許乍看這沒什麼了不起,又不是語言糖 (syntax sugar),也不是什麼神奇的演算法,但我們見識到 Linux 核心開發者追求的「優雅」 —— 清晰地界定介面和實作本體。 ## `container_of` 實作手法 關於 `container_of` 巨集的實作手法,可參見下圖: ![](https://i.imgur.com/6h0Bgax.jpg) 對應的原始程式碼如下: ```c /* container_of() - Calculate address of object that contains address ptr * @ptr: pointer to member variable * @type: type of the structure containing ptr * @member: name of the member variable in struct @type * * Return: @type pointer of object containing ptr */ #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *(__pmember) = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` 解析巨集的過程中,我們可留意 `__extension__`,依據 GCC 手冊 [Alternate Keywords](https://gcc.gnu.org/onlinedocs/gcc/Alternate-Keywords.html) > `-pedantic` and other options cause warnings for many GNU C extensions. You can prevent such warnings within one expression by writing `__extension__` before the expression. `__extension__` has no effect aside from this. 儘管 GCC 幾乎支援 C 和 C++ 最新的規格,但仍考慮到傳統 C 程式 (即最廣泛支援的 [ANSI C](https://en.wikipedia.org/wiki/ANSI_C) C89 或 C90,注意: C90 和 C89 幾乎一致,差別在於 C90 納入 ISO/IEC 9899:1990 標準時,進行格式和排版調整),提供 `-pedantic` 編譯選項,讓編譯器主動提醒開發者,是否用到 C89/C90 以外的特徵。 由於上述巨集用到 [typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) 這個 GNU extension,於是特別用 `__extension__` 標注,避免 GCC 搭配 `-pedantic` 編譯選項時,會拋出警告訊息。 上述程式碼是從 struct 中的 member 推算出原本 struct 的位址。解析: * 先透過 `__typeof__` 得到 type 中的成員 `member` 的型別,並宣告一個指向該型別的指標 `__pmember` * 將 `ptr` 指派到 `__pmember` * `__pmember` 目前指向的是 `member` 的位址 * `offsetof(type, member)` 可得知 `member` 在 `type` 這個結構體位移量,即 offset * 將絕對位址 `(char *) __pmember` 減去 `offsetof(type, member)` ,可得到結構體的起始位址。計算 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) 的說明) * 最後 `(type *)` 再將起始位置轉型為指向 `type` 的指標 > 延伸閱讀: > * [你所不知道的 C 語言:指標篇](https://hackmd.io/@sysprog/c-pointer) > * [你所不知道的 C 語言:技巧篇](https://hackmd.io/@sysprog/c-trick) 值得留意的是,Linux 核心原始程式碼提供的 `container_of` 巨集定義事實上更複雜: ```c= #define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \ !__same_type(*(ptr), void), \ "pointer type mismatch in container_of()"); \ ((type *)(__mptr - offsetof(type, member))); }) ``` 第 3 行藉由 `BUILD_BUG_ON_MSG` 巨集達到 static assert 的作用,儘量在編譯時期就查驗 `container_of` 巨集的使用是否合法:預先進行指標型態的檢查 > 延伸閱讀: [Linux 核心巨集: BUILD_BUG_ON_ZERO](https://hackmd.io/@sysprog/c-bitfield) 關於 `__same_type` 巨集,可見 [include/linux/compiler_types.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler_types.h): ```c /* Are two types/vars the same type (ignoring qualifiers)? */ #define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b)) ``` 同樣是運用 GNU extension 達成。 ## 應用案例: 雙向環狀鏈結串列 接下來,我們仿效 Linux 核心 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 的精簡雙向環狀鏈結串列 (circular doubly-linked list,以下簡稱 `list`) 實作。關鍵概念是,將結構體 `list_head` 嵌入到所需的結構體中,再藉由 `container_of` 巨集得知 list 個別節點的地址。示意如下圖: ![](https://i.imgur.com/kOvwKZw.png) 自 `Head` 開始,鏈結 list 各節點,個別節點皆嵌入 `list_head` 結構體,不過 `Head` 是個特例,無法藉由 `container_of` 巨集來找到對應的節點,因為後者並未嵌入到任何結構體之中,其存在是為了找到 list 整體。 程式碼列表: ```c #include <stddef.h> /* struct list_head - Head and node of a doubly-linked list * @prev: pointer to the previous node in the list * @next: pointer to the next node in the list */ struct list_head { struct list_head *prev, *next; }; /* LIST_HEAD - Declare list head and initialize it * @head: name of the new object */ #define LIST_HEAD(head) struct list_head head = {&(head), &(head)} /* INIT_LIST_HEAD() - Initialize empty list head * @head: pointer to list head */ static inline void INIT_LIST_HEAD(struct list_head *head) { head->next = head; head->prev = head; } /* list_add_tail() - Add a list node to the end of the list * @node: pointer to the new node * @head: pointer to the head of the list */ 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; } /* list_del() - Remove a list node from the list * @node: pointer to the node */ static inline void list_del(struct list_head *node) { struct list_head *next = node->next, *prev = node->prev; next->prev = prev; prev->next = next; } /* list_empty() - Check if list head has no nodes attached * @head: pointer to the head of the list */ static inline int list_empty(const struct list_head *head) { return (head->next == head); } /* list_is_singular() - Check if list head has exactly one node attached * @head: pointer to the head of the list */ static inline int list_is_singular(const struct list_head *head) { return (!list_empty(head) && head->prev == head->next); } /* list_splice_tail() - Add list nodes from a list to end of another list * @list: pointer to the head of the list with the node entries * @head: pointer to the head of the list */ static inline void list_splice_tail(struct list_head *list, struct list_head *head) { if (list_empty(list)) return; struct list_head *head_last = head->prev; struct list_head *list_first = list->next, *list_last = list->prev; head->prev = list_last; list_last->next = head; list_first->prev = head_last; head_last->next = list_first; } /* list_cut_position() - Move beginning of a list to another list * @head_to: pointer to the head of the list which receives nodes * @head_from: pointer to the head of the list * @node: pointer to the node in which defines the cutting point */ 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; } /* list_entry() - Calculate address of entry that contains list node * @node: pointer to list node * @type: type of the entry containing the list node * @member: name of the list_head member variable in struct @type */ #define list_entry(node, type, member) container_of(node, type, member) /* list_first_entry() - get first entry of the list * @head: pointer to the head of the list * @type: type of the entry containing the list node * @member: name of the list_head member variable in struct @type */ #define list_first_entry(head, type, member) \ list_entry((head)->next, type, member) /* list_for_each - iterate over list nodes * @node: list_head pointer used as iterator * @head: pointer to the head of the list */ #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` 上方程式碼的好處在於,只要 `list_head` 納入新的結構體的一個成員,即可操作,且不用自行維護一套 doubly-linked list 。 ![](https://i.imgur.com/d3bG8t6.png) > 延伸閱讀: [Linux 鏈結串列 struct list_head 研究](https://myao0730.blogspot.com/2016/12/linux.html) 注意到 `list_entry` 利用 `container_of` 巨集,藉由 `struct list_head` 這個公開介面,「反向」去存取到自行定義的結構體開頭地址。 以下為圖解: - [ ] `list_head` 結構體 ```c struct list_head { struct list_head *prev, *next; }; ``` ```graphviz digraph list_head { rankdir=LR; node[shape=record, style=bold]; head [label="{<prev>prev|<next>next}"]; } ``` - [ ] 在自行定義的結構體置入 `list_head` 物件 ```c typedef struct { char *value; struct list_head list; } my_data; ``` ```graphviz digraph list { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_0 { node [shape=record]; value [label="{value}"]; head [label="{<prev>prev|<next>next}"]; style=bold; label=my_data } } ``` 簡化為下圖: ```graphviz digraph list_ele { rankdir=LR; node[shape=record]; node [shape=record]; head [label="value|{<left>prev|<right>next}", style=bold]; } ``` - [ ] `LIST_HEAD - Declare list head and initialize it` ```c #define LIST_HEAD(head) struct list_head head = {&(head), &(head)} ``` ```graphviz digraph list_init { rankdir=LR; node[shape=record]; style=bold node [shape=record]; head [label="{<left>prev|<right>next}", style="bold"]; head:right:e -> head:left:w[arrowhead=normal, arrowtail=normal, dir=both]; } ``` - [ ] `list_entry() - Calculate address of entry that contains list node` ```c #define list_entry(node, type, member) container_of(node, type, member) ``` ```graphviz digraph container { rankdir=LR; node[shape=record]; compound=true; style=bold; subgraph cluster_0 { container [label = "container" style=filled,color=white]; subgraph cluster_1 { node [shape=record]; element [label="|{|}", style="bold"]; label = "member" } } element -> container[ltail=cluster_1, lhead=cluster_0]; } ``` - [ ] `list_for_each - iterate over list nodes` ```c #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; h [label="head", style=dashed, color=grey]; h -> e1:right:w [style=dashed, color=grey]; e1 [label="head node|{<left>prev|<right>next}", style="bold", color=grey]; e2 [label="cat|{<left>prev|<right>next}", style="bold"]; e3 [label="...", style="bold"]; e4 [label="eat|{<left>prev|<right>next}", style="bold"]; e5 [label="fat|{<left>prev|<right>next}", style="bold"]; e1:right:e -> e2:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e2:right:e -> e3:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e3:right:e -> e4:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e4:right:e -> e5:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e5:right:e -> e1:left:w[arrowhead=normal, arrowtail=normal, dir=both]; } ``` > 延伸閱讀: [你所不知道的C語言: linked list 和非連續記憶體操作](https://hackmd.io/@sysprog/c-linked-list)