原地址:http://radek.io/2012/11/10/magical-container_of-macro/
当你开始内核编程的时候,你会随便看看代码,那么你可能很快就会碰到这个神奇的预处理代码结构。
它是用来干嘛的?正如它的名称那样,它获取的是它的容器的地址。该函数需要三个变量--指针,容器的类型,指向成员的指针。(译者注:最终返回的是,该成员所处的结构体的指针。知道成员的指针,根据结构体的size就可以算出来了,具体的算法还要看下面)该宏将返回包含对应成员的容器的指针地址。它确实是一个实现巧妙的宏,但是原理是什么呢?下面让我们来看看。
第一幅插图显示了宏container_of(ptr, type, member) 的原理。一图胜前言,基本上就不需要上面啰嗦的描述了。
container_of宏的工作原理插图
下面是Linux内核中该宏的具体实现:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
第一眼看上去感觉就像表演魔法一样,下面就让我们一步一步的来解析这个魔法。(译者注:确实是非常优雅的实现。再次感叹。)
表达式中的语句
第一眼看上去,是不是觉得整个表达式的结构很奇怪,不是应该返回一个指针吗,但是为毛看起来只有两个类似{}的表达式在里面。好吧,其实很简单,这个表达式是C语言的GNU扩展(braced-group within expression,不知道怎么翻译,只需要知道是扩展就好)。编译器会执行整个语句块,并且将第二个语句的结果作为最终的结果返回。比如下面的语句将打印5:
int x = ({1; 2;}) + 3; printf("%d\n", x);
typeof()
这个是一个非标准的GNU C扩展。获取参数x的类型。精确的语义在gcc文档中有详细的说明。
int x = 5; typeof(x) y = 6; printf("%d %d\n", x, y);
0指针的间接引用
但是为毛出现了一个0指针啊!!好吧,这个实际上是获取成员类型的小魔法。它不会崩溃,因为这个表达式本身并不会被求值。编译器在乎的指示它的类型。为了避免我们要求地址,同样的状况也会发生(求大神翻译:The same situation occurs in case we ask back for the address. )。重复一遍,编译器并不关心0指针的值,编译器会在分配给结构体的地址加上该成员的偏移,并且返回加上偏移之后的新地址。
struct s { char m1; char m2; }; /* This will print 1 */ printf("%d\n", &((struct s*)0)->m2);
注意,下面的两个定义是等价的:
typeof(((struct s *)0)->m2) c; char c;
offsetof(st, m)
这个宏会返回成员在相对结构体首地址的偏移量字节。它甚至作为了标准库的一部分(在stddef.h中可用)。并不是运行在内核空间中,标准C库并没有在这个地方出现。这个有点像我们前面提到的0指针引用,现在的编译器通常会提供一个内建的函数来替换。下面是一个复杂版本(来自内核):
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
It returns an address of a member called MEMBER of a structure of type TYPE that is stored in memory from address 0 (which happens to be the offset we’re looking for).
它会返回TYPE类型结构体中一个名为MEMBER成员的地址,这个地址是内存中从0地址开始计算的地址(这个地址其实也恰好我们需要的偏移量。注:这个0地址其实也就是结构体的首地址)。
将所有的结合起来
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
当你仔细看看文章开始给出的原始定义的时候,你可能会想,第一行代码真的符合所有情况吗?
你没有猜错。第一行代码说对这个宏的返回结果并不会产生本质上的影响,但是它的作用只作为类型检查的目的。第二行是干嘛的呢?我们提供了结构体中某个成员的地址,然后减去该成员在结构体中的偏移,就的到了包含它的结构体的地址(译者注:这样就可以访问结构体中的其他成员了,牛b吧!在Linux代码中经常看到结构体中包含一个包含struct list_head *next,*prev;的语句,通过遍历这个就可以获取结构体的首地址了,可以很方便的通过包含这个来实现链表,而不是包含该结构体的前后指针,万一这个结构体很复杂呢。Linux中可是有很多复杂的结构体的)。
当我们一点点剥离魔法操作符(0指针),构造和小技巧之后,发现它就是这么简单:-).
(译者注:该宏的总点是对于结构体中每个成员在内存中地址的分配和结构体地址的关系。仅此而已!)
引用
container_of() explained by Greg KH
container_of() definition in Linux Kernel
Statements and Declarations in Expressions
Referring to a Type with typeof
offsetof()