Try   HackMD

Linux Kernel 中的 container_of 巨集

container_of 巨集被定義在 <linux/kernel.h> 中。 container_of 需要使用到 offsetof 巨集幫忙計算結構 (struct) 起始位址到結構成員 (member) 間的偏移量 (offset)。只要知道結構中任一成員的偏移量就可以計算出以該結構為型別 (type) 之物件在記憶體中的起始位址。
關於 offsetof 的相關說明可以參考 Linux Kernel 中的 offset 巨集

#define container_of(ptr, type, member) ({			\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

typeof 的功用

首先,瞭解一下 typeof 的作用。 typeofgcc 的 extension 。 typeof 主要功能是讓編譯器在編譯時期推算運算式 (expression) 之結果的型別。

6.7 Referring to a Type with typeof

寫個小程式來驗證一下。

#include <stdio.h> #include <limits.h> int main(int argc, char *argv[]) { int a = INT_MAX; long b = 1; printf("sizeof(int): %ld, sizeof(long): %ld, sizeof(a + b): %ld\n", sizeof(int), sizeof(long), sizeof(typeof(a + b))); printf("a: %d, b: %ld, a + b: %ld\n", a, b, a + b); int c = 1; printf("sizeof(a + c): %ld\n", sizeof(typeof(a + c))); printf("a: %d, c: %d, a + c: %d\n", a, c, a + c); return 0; }

將上述程式編譯並執行後,可得下列結果。

$ gcc test.c -o test
$ ./test 
sizeof(int): 4, sizeof(long): 8, sizeof(a + b): 8
a: 2147483647, b: 1, a + b: 2147483648
sizeof(a + c): 4
a: 2147483647, c: 1, a + c: -2147483648

由 C99 spec 可知,一個 int 物件與一個 long 物件相加所得結果之型別為 long

C99 spec, 6.3.1.8 Usual arithmetic conversions
Otherwise, if both operands have signed integer types or both have unsigned
integer types, the operand with the type of lesser integer conversion rank is
converted to the type of the operand with greater rank.

第 6 行將 int 變數 a 初始化為 INT_MAX (即 2147483647 ) 。
第 7 行將 long 變數 b 初始化為 1
所以第 9 行的運算式 a + b 的結果並沒有產生溢位 (overflow) 。其結果為 2147483648 。且由 a + b 運算結果的大小為 8 bytes 可知, typeof(a + b) 推得 a + b 的型別為 long
另外,由於變數 ac 的型別皆為 int ,所以運算式 a + c 的型別也是 int 。因此, a + c 會產生溢位,其結果為 -2147483648

container_of 巨集的作用

(type *)0)->member 可以找到結構中的 member 成員。因此, typeof( ((type *)0)->member ) 就會推得 member 的型別。假設 member 的型別為 int ,則 __mptr 的型別就是 int * (pointer to int) 。
因為__mptr 是物件中 member 的記憶體位址,所以 (char *)__mptr - offsetof(type,member) 的運算結果就是以 type 為型別之物件在記憶體中的起始位址。
Linux Kernel 中的 offset 巨集 中的討論可知, offsetof(type,member) 的作用是計以 type 為型別之算物件的起始位址到 memeber 間的偏移量。所以, container_of 巨集就是透過物件的某個成員在記憶體中的位址 (ptr) ,結構的名稱(就是物件的型別, type) 以及該成員的名稱 (member) 計算出這個以 type 為型別之物件在記憶體中的起始位址。

範例

#include <stdio.h>
#include <limits.h>

#define offsetof(TYPE, MEMBER)  ((size_t)&((TYPE *)0)->MEMBER)

#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})

struct test {
    int data_a;
    long data_b;
    double data_c;
};

int main(int argc, char *argv[])
{
    struct test obj = {123, 456L, 789.0};

    struct test *p = container_of(&obj.data_c, struct test, data_c);
    printf("p->data_a: %d\n", p->data_a);
    printf("p->data_b: %ld\n", p->data_b);
    printf("p->data_c: %f\n", p->data_c);

    return 0;
}
$ gcc test.c -o test
$ ./test 
p->data_a: 123
p->data_b: 456
p->data_c: 789.000000

container_of 巨集的另一種實作

下列程式碼片段是 container_of 巨集的另一種實作。與原本的實作有相同的效果。

#define container_of(ptr, type, member) ({                      \
        const typeof( ptr ) __mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})