一、简介
container_of(ptr, type, member) 是一个宏函数,其作用是通过结构体成员的地址找到结构体的地址。
该宏函数常在 linux 内核链表遍历之后使用,以此来获取虚节点所寄生的宿主结构体,详情可以参考Linux 链表这篇文章。
参数简介:
container_of(ptr, type, member) 中,ptr 为结构体成员地址,type 为结构体的类型,member 为结构体成员在结构体里的名字
二、源码分析
container_of(ptr, type, member) 相关源码定义如下:
path:kernel/msm-5.4/include/linux/kernel.h
/**
* container_of_safe - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#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))); })
可以看到 container_of 是一个宏函数,会被宏替换为后面的表达式,下面来分析一下表达式里的具体逻辑。
1. void *__mptr = (void *) (ptr);
第一句是将传入的结构体成员的地址赋给临时指针 __mptr。可以发现临时指针变量 __mptr 被定义成了 void 类型的指针,且 ptr 在赋值时也被强制类型转换成 void 类型。为什么这么做呢?原因是不同类型的指针虽然可以相互赋值,但是在编译过程中会出现 warning 或 error。并且当需要指针做自加减类的操作时,由于不同数据类型所占的字节数不一样,指针移动的量也不一样,很容易出错。所以在这里强制转换成了 __mptr 指针的类型。
之前的版本的对于 container_of 的定义如下:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) );})
这里把临时变量 __mptr 定义成了结构体成员的类型,但是若 ptr 和 __mptr 类型不一致就会同上面讲的一样出现 warning。所以现在的写法更规范一点,并且现在第二句使用了 __same_type 来判断两个参数是否一致,不一致会直接停掉编译,这点下一小节会详细解释。
个人感觉写成下列形式应该也可以:
#define container_of(ptr, type, member) ({ \
const typeof(ptr) *__mptr = (ptr); \
(type *)( __mptr - offsetof(type, member) );})
2. BUILD_BUG_ON_MSG(....);
2.2.1 BUILD_BUG_ON_MSG
/*
* BUILD_BUG_ON_MSG - break compile if a condition is true & emit supplied
* error message.
* @condition: the condition which the compiler should know is false.
*
* See BUILD_BUG_ON for description.
*/
#define BUILD_BUG_ON_MSG(cond, msg) compiletime_assert(!(cond), msg)
BUILD_BUG_ON_MSG 是一个宏,被替换为编译时的断言 compiler_assert()。如果 条件 cond 为 ture 就会停止编译并且发送 msg 错误信息。
2.2.2 __same_type
container_of() 里 BUILD_BUG_ON_MSG 的 cond 是宏函数 __same_type(),其作用是判断两个参数的类型是否一致。
path:kernel/msm-5.4/include/linux/compiler_types.h
#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
2.2.2.1 __builtin_types_compatible_p
__same_type() 如上源码所示,会被宏替换为 __builtin_types_compatible_p()。__builtin_types_compatible_p(type1, type2) 是 GCC 所提供的 builtin 内置函数,其作用是判断 type1 和 type2 的数据类型是否相同,相同返回 1,不相同返回 0。
关于此内置函数需要注意以下几点:
- 忽略顶级限定符(例如const、volatile)。例如,const int等价于 int,也就是说他俩的类型都是 int 型。
- 相同数据类型,但若指针级数不同,他们的类型也不同。例如 int *a 和 int **b。
- 用typedef定义的两个类型是等效的,当且仅当它们的底层类型是等效。
-
枚举类型是一种特殊情况,因为两种枚举类型不被视为等效。
由上可知,若两个变量的类型不一致,则 __builtin_types_compatible_p() 返回 0,即 __same_type() 返回 0 ,!__same_type 为真,所以此时 BUILD_BUG_ON_MSG 会中断编译,并且发送错误信息。
2.2.2.2 (type *)0)->member
介绍完上面的内容,我们再来追一下 (type *)0)->member 这个地方。(type *)0,可以理解为把 0 地址强制转换为 type 结构体类型的指针,此时 0 就成了 type 结构体的首地址,指向该结构体,既然为结构体指针,那么自然可以引用该结构体的成员,所以 (type *)0)->member 的整体意义就是引用 type 结构体的成员 member。
这点在 offsetof 函数里会用到,详见第 3 节。
2.2.3 小插曲
在看 (!__same_type(*(ptr), ((type *)0)->member) && !__same_type(*(ptr), void))这个表达式时,脑子没转过来,错误理解了这里的意思,在这里记录一下。
刚开始我疑惑:
- 临时指针 __mptr 已经定义成 void 类型的指针了,如果要去判断 ptr 的类型,只是与 void 做比较即可,为什么还跟 member 做比较呢?
- 另外:① 一般结构体成员的指针不会定义为 void 类型的指针。② 若传入的 ptr 类型没有发生改变时其类型就为 typeof((type *)0)->member) 类型,这时候 && 的第一个表达式是 1。③ 按照①所说的, typeof((type *)0)->member)类型不为 void,那么 && 的第二个表达式是 0,0 && 1 结果为 0,取非为 1,condition 为 1,编译报错。(优先级搞错)
好好分析一下代码逻辑:
首先,临时指针 __mptr 被定义成 void 类型了。回顾一下 void 类型指针的知识:任何类型的指针都可以赋值给 void 类型的指针,但若将 void 类型的指针赋值给其他类型的指针则需要进行强制类型转换。所以,无论是传入的 ptr 是 void 类型还是 typeof((type *)0)->member) 类型,都可以赋值给 __mptr ,都不会报错。
接着看 !__same_type(*(ptr), ((type *)0)->member) && !__same_type(*(ptr), void)。
①当 ptr 传入的就是成员类型的指针,类型同((type *)0)->member 一致,第一个 __same_type() 返回 1,取非为 0 。此时 ptr 与 void 类型不一致,第二个 __same_type() 返回 0,取非为 1。1 && 0 结果还是 0,condition 为 0,编译不会出错。
②当 ptr 传入的是 void 类型的指针,分析同上,此时 condition 依旧为 0,编译也不会出错。另外,为 void 可以理解为在传入 container_of() 之前已经将 ptr 的指针传给了一个 void 的临时指针(再次注意:只能传给 void 类型的临时指针!),如下示例:
//此段代码仅作所述问题的演示
void *n = NULL;
......
n = ptr;
container_of(n, type, member);
......
所以,还是上面说的:无论是传入的 ptr 是 void 类型还是 typeof((type *)0)->member) 类型,都可以赋值给 __mptr ,都不会报错,这也就是为啥做两个类型判断的原因。而其他类型都不对,必然哪里出错了,所以一旦检测到是其他类型毫无疑问会终止编译。
3. ((type *)(__mptr - offsetof(type, member)));
这一句是 container_of 的核心语句。
其算法就是:结构体的地址 = 结构体成员的地址 - 该成员相对于结构体起始地址的偏移量
前面提过已经提过 __mptr 了,先看下 offsetof() 宏函数,其源码定义如下:
path:kernel/msm-5.4/include/linux/stddef.h
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
代码逻辑很简单,通过 &((TYPE *)0)->MEMBER) 先取 TYPE 结构体类型成员的地址,强制转换成 size_t 类型后返回结构体类型成员的地址。
2.2.2.2 (type *)0)->member 小结已经讲到此时地址 0 是该结构体的首地址。当结构体起始地址为 0 时,结构体成员的地址为多少,其相对于结构体的偏移量就为多少。所以此时结构体类型成员的地址就是该成员相对结构体起始地址的偏移量。
因此我们可以得出 offsetof 宏函数的作用就是:获取给定成员相对于结构体起始地址的偏移量。
然后我们再设想一下,若结构体 bigelt 的地址为100,成员 head 的地址为112,head 相对于 bigelt 的起始地址的偏移量为 12。在已知 head 的地址和偏移量的情况下,很容易求出结构体 bigelt 的起始地址,这便是这行代码的根本逻辑,也是 container_of() 函数的构建思路。
总结一下 container_of() 宏函数的逻辑:先将成员地址赋给 __mptr,然后用 __mptr 减去偏移量得到了结构体起始地址,接着将该地址强制类型转换成结构体类型。因为最后一句是替换体表达式的最后一句,所以 container_of 的返回值便是已经强制类型转换成结构体类型的结构体指针。
三、用法示例
正如文章开头所说,container_of() 用的比较多的地方就是 linux 内核链表遍历时,遍历得到虚节点的指针,然后调用 container_of() 来获取宿主结构体的地址,下面我也以此为例,简单写段代码来做演示。
static LIST_HEAD(list_head);
struct bigelt {
int id;
//虚节点在结构体里的名字,定义为list
struct list_head list;
};
int function() {
struct bigelt *bigelt_i,*bigelt_f;
//定义临时指针在遍历时指向虚节点
struct list_head *pos;
struct list_head *head = &list_head;
*bigelt_i = (strut bigelt *)malloc(sizeof(struct bigelt));
if (bigelt_i != NULL ) {
INIT_LIST_HEAD(&bigelt_i->list);
bigelt_i->id = 1;
list_add_tail(&bigelt_i->list, &head);
}
list_for_each(pos,&head) {
//pos为指向虚节点的临时指针,第二个参数为结构体类型,第三个参数为虚节点在结构体里的名字为list,返回值为结构体的地址,赋给已经事先定义好的该结构体类型的指针 bigelt_f
bigelt_f = container_of(pos, struct bigelt, list);
if (bigelt_f != NULL) {
bigelt_f->id = 0;
return 0;
}
}
}