container_of(ptr, type, member)
是内核中的经典函数之一。该函数的作用是:根据结构体中一个成员的地址,找到结构体的地址。这个函数是内核实现面向对象的基础设施,且最近在学习中经常见到这个函数,于是笔者在内核中查看了该函数的实现,故在此记录。本文原本是为了展示container_of
的实现,但写着写着,发现有些内建函数与GNU C拓展的使用,所以就顺便查了资料,也一并记录于此,写得比较乱,请大家谅解。
结构体在内存中的分布,是按照成员的顺序分配内存,同时保持内存对齐的要求
该函数在5.17.5中的实现在include/linux/container_of.h
中
5.16之前,这个宏都被放在
include/linux/kernel.h
中
源码如下:
/** * container_of - 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); \ static_assert(__same_type(*(ptr), ((type *)0)->member) || \ __same_type(*(ptr), void), \ "pointer type mismatch in container_of()"); \ ((type *)(__mptr - offsetof(type, member))); })
将传入的成员变量的地址,转换为void *
类型,并赋给另一个值。这个操作笔者没有理解,所以找了以前版本的源码来进行分析,在2.6.23里,他的实现是这样的:
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
这个版本中,第一行的作用其实相当于赋值+检查,考虑如果传进来的指针类型和member不一致,编译器会报warning。
在使用查看了相关的log之后,发现这个宏是在提交c7acec713d14c
被改变的,改变的原因是:如果结构体内引入了一个非const数组成员,那么这个指针就会产生变量赋值给常量的问题,这会在gcc-4.9中产生一个warning: initialization from incompatible pointer type
。这一笔改动抽离出了类型检查,但__mptr
仍留在原处,笔者实在不清楚这个操作的深意,又或许只是历史遗留问题?
static_assert(__same_type(*(ptr), ((type *)0)->member) || \ __same_type(*(ptr), void), \ "pointer type mismatch in container_of()"); \
这个地方在5.16后修改成static_assert,之前使用的是
BUILD_BUG_ON()
这个宏,他和static_assert被定义在同一个文件里,感兴趣的朋友们可以去看一看相关实现,根据commit message显示,使用static_assert
可以给出更加直接的错误提示,并且在理论上可以提升一点点的build速度(commit message里写了a tiny bit faster
)
一个断言,用于检查ptr
和member
的类型一致性。这个断言函数static_assert()
我们先放在一边,来分析一下这个断言的第一个参数:内部使用了__same_type()
这个宏,来看看这个宏的实现:
/* Are two types/vars the same type (ignoring qualifiers)? */ #define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
这个宏使用了两个函数:__builtin_types_compatible_p()
和typeof()
。
typeof()
想必大家都比较熟悉了,它是一个GNU C的拓展,作用是获取变量的类型。文档地址:https://gcc.gnu.org/onlinedocs/gcc/Typeof.html
__builtin_types_compatible_p(type1, type2)
是一个GNU C的内建函数,用于比较两个类型是否相等,若相等则返回1,不等则返回0。需要注意的是,这个函数的参数并不是表达式,而是变量类型,所以需要使用typeof()
先取得变量类型后再传入。文档地址:https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
最后我们再来看一看static_assert()
,这个函数的实现位于include/linux/build_log.h
,源码如下:
#define static_assert(expr, ...) __static_assert(expr, ##__VA_ARGS__, #expr) #define __static_assert(expr, msg, ...) _Static_assert(expr, msg)
关于C宏定义中#
符号的用法,可以总结为以下两点:
##
,转换为合法标识符#define to_symbol(x) T_##x // 下面这句等效于 int T_1 = 10; int to_symbol(1) = 10;
#
,转换为字符串#define to_string(x) #x // 下面这句等效于 "a+b+c" to_string(a+b+c);
那##__VA_ARGS__
又是什么呢?它的功能有两个:
接着再来看一看_Static_assert(expr, msg, ...)
,这是一个C11特性,用来在编译时测试expr
的正确性,如果正确则什么都不会发生,如果错误,则打印指定信息msg
。文档地址:https://www.gnu.org/software/gnulib/manual/html_node/assert_002eh.html
综上所述,第二行的作用就是:判断传入的ptr
和member
(或者void
)是否为同一类型,若否,则打印"pointer type mismatch in container_of()"
这一行真正用于获取结构体的地址。
((type *)(__mptr - offsetof(type, member)));
看上去很简单!就是用传进来的成员变量地址值减去它在结构体里的偏移值嘛!
逻辑上来讲确实很简单,但是如何实现呢?如何在不同的对齐下让这个函数均能成功运行呢?让我们带着这个疑问走进offsetof()
这个宏:
// At include/linux/stddef.h #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
也很简单对吧?把0地址转换成结构体类型指针,然后利用这个特殊的结构体指针获取member
,然后再对member
取地址,得到的这个值就是member
相对于0地址的偏移值,这个偏移值不就是member相对于结构体首地址的偏移值嘛!
看到这里,如果你和笔者一样是内核初学者,你可能会和笔者一样惊讶:0地址还能这么用?!!笔者也是在发出了这样的感叹之后,才决定记录下这篇随笔。
offsetof
这个宏还有另一个实现,即调用GNU C的内建函数__builtin_offsetof
,本质上和上面的定义是一致的。
这个宏包括了三步:赋值、检查、寻址。笔者分析了2.6.23中的赋值操作目的与最新的5.17.5中的检查和寻址操作。
在最后希望询问看到这篇文章的朋友们一个问题:为什么最新的版本还需要赋值给__mptr
,能否在第三行中直接使用(void *)ptr
代替__mptr
?
原创文章,如有错漏,敬请补充指正,如对于文章风格有建议,请在评论区直接提出,感谢。