container_of() 宏的源码分析

  • container_of() 宏的源码分析已关闭评论
  • 252 次浏览
  • A+
所属分类:linux技术
摘要

container_of(ptr, type, member)是内核中的经典函数之一。该函数的作用是:根据结构体中一个成员的地址,找到结构体的地址。这个函数是内核实现面向对象的基础设施,且最近在学习中经常见到这个函数,于是笔者在内核中查看了该函数的实现,故在此记录。本文原本是为了展示container_of的实现,但写着写着,发现有些内建函数与GNU C拓展的使用,所以就顺便查了资料,也一并记录于此,写得比较乱,请大家谅解。


简介

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))); }) 

参数

  • ptr:成员指针
  • type:结构体类型
  • mem:成员在结构体里的名称

第一行:赋值

将传入的成员变量的地址,转换为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

一个断言,用于检查ptrmember的类型一致性。这个断言函数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宏定义中#符号的用法,可以总结为以下两点:

  1. 前加##,转换为合法标识符
#define to_symbol(x)  T_##x  // 下面这句等效于 int T_1 = 10; int to_symbol(1) = 10; 
  1. 前加#,转换为字符串
#define to_string(x) #x  // 下面这句等效于 "a+b+c" to_string(a+b+c); 

##__VA_ARGS__又是什么呢?它的功能有两个:

  1. 如果可变参数列表为空,使编译器忽略它以及它前面的逗号
  2. 如果可变参数列表不为空,编译器将其替换为可变参数列表

接着再来看一看_Static_assert(expr, msg, ...),这是一个C11特性,用来在编译时测试expr的正确性,如果正确则什么都不会发生,如果错误,则打印指定信息msg。文档地址:https://www.gnu.org/software/gnulib/manual/html_node/assert_002eh.html

综上所述,第二行的作用就是:判断传入的ptrmember(或者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

原创文章,如有错漏,敬请补充指正,如对于文章风格有建议,请在评论区直接提出,感谢。