在编写驱动程序时,经常会用到
container_of
宏,这个宏的作用很强大,它根据结构体中一个已知成员的名字及其地址推导出整个结构体的起始地址,正因为有了这个宏,内核中经典的数据结构代码才得以高效复用。
下面来简单剖析一下这个宏。
源码
摘自linux-5.7.8版本源码,它们还有很多变种宏,但是原理是一样的,只不过可能更加安全而已。
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
#ifndef container_of
/**
* 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) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
#endif
GNU扩展语法
注意对于GNU扩展语法,在某些编译器上面是不一定支持的,比如微软的MSVC编译器就不支持。
在看container_of
宏的实现时,你是否也好奇({})
是什么含义,其实这个是GNU的扩展语法,也就是在标准C里面是没有的,对于用{}
括起来的内容,一般称为代码块,在代码块里面就和在函数里面是一样的,我们可以使用各种表达式实现想要的功能,而在{}
外面再加一对()
组合起来的意思就是将这个代码块的最后一个表达式的结果作为整个代码块的运行结果。
例:
#include <stdio.h>
int main(int argc,char *argv[])
{
int a,b,c;
a = 5;b = 6;
printf("%d,%d\r\n",c,({c = a + b;}));
return 0;
}
测试结果:
11,11
接下来看另一个扩展,typeof
这个关键字是用来获取一个变量的类型的,在标准C++里面就有对应的关键字,但是C语言里面目前是没有的,看一下它的用法。
例:
#include <stdio.h>
int main(int argc,char *argv[])
{
int a,b;
/*用typeof获取变量a的类型,并用此类型定义一个变量c*/
typeof(a) c;
a = 5;b = 6;
c = a + b;
printf("%d\r\n",c);
/*注意里面的表达式是不会被执行的*/
typeof(++a) d;
printf("%d\r\n",a);
return 0;
}
测试结果:
11
5
实现原理
看一下模型图:
从上面的模型图可以看出,如果我们要知道整个结构体的起始地址,那么直接用ptr
减去member
成员在结构体中的偏移量offset
就行了,那么问题的关键就变成了如何得到这个偏移量offset
。
这个时候就该另一个宏定义offsetof
上场了,它的作用就是获取一个结构体成员在结构体中的偏移量,看下它是如何实现的。
(size_t) &((TYPE *)0)->MEMBER
其中TYPE
是结构体的类型,MEMBER
为已知的结构体成员,其将数字0
强制转换为(TYPE *)
类型的指针,那么MEMBER
成员的偏移量就是其在结构体中的地址值。这里可能你会有点不明白,为什么这样就得到了偏移量,那么继续看一个例子。
#include <stdio.h>
struct test_t {
int a;
char b;
short c;
};
int main(int argc,char *argv[])
{
struct test_t test;
printf("%p,%p\r\n",&test,&test.b);
return 0;
}
测试结果:
0x7ffd31669700,0x7ffd31669704
根据结果可知整个结构体的起始地址为0x7ffd31669700
,成员b
的起始地址为0x7ffd31669704
,那么成员b
的偏移量就是4
,假设编译器将这个结构体的内存地址安排在0
地址,那么成员b
的偏移量是不是就是其地址值。
到了这里我们已经得到了偏移量,那么整个结构体的地址值也就显而易见了。
(char *)ptr - offset;
再看源码
有了前面那些内容的铺垫,再来看源码实现就容易了。
下面的代码是获取成员member
的类型,并用此类型定义一个临时变量__mptr
用于保存ptr
,其实这行代码大部分情况要不要都不影响结果的,只是为了让代码更加健壮,以适应某些特殊场景,因为宏定义是完全替换,在某些场景中,替换过去会存在问题,这都是属于极端情况考虑。
const typeof(((type *)0)->member) * __mptr = (ptr);
真正完成功能的其实就是下面这行,最终将得到的地址值强制类型转换为对应的结构体指针类型,然后返回:
(type *)((char *)__mptr - offsetof(type, member));
如何使用
container_of(ptr, type, member)的三个入参:
- ptr:结构体成员member的地址
- type:我们要获取的结构体变量地址的结构体类型
- member:结构体成员member的名字
例:
#include <stdio.h>
/*此处省略了两个宏的源码*/
struct test_t {
int a;
char b;
short c;
};
int main(int argc,char *argv[])
{
struct test_t test;
test.a = 5;test.b = 6;test.c = 7;
struct test_t *p = container_of(&test.b,struct test_t,b);
printf("%p,%p\r\n",&test,p);
printf("%d,%d,%d\r\n",p->a,p->b,p->c);
return 0;
}
测试结果:
0x7ffc091fafe0,0x7ffc091fafe0
5,6,7
欢迎扫码关注我的微信公众号