LINUX内核中常用的两个宏就是offsetof和container_of
1.offsetof介绍
定义:offsetof在linux内核的include/linux/stddef.h中定义
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
offsetof用于计算TYPE结构体中MEMBER成员的偏移位置,也就是TYPE是一个结构体类型,MEMBER是这个结构体类型中的变量,首先0转化成一个TYPE结构体类型的指针,可想而知在0地址是没有一个TYPE结构体类型的变量,0地址是给操作系统用的,那么为什么这里直接使用0地址不会导致程序崩溃,要知道的是对于编译器来说,它清楚的知道结构体变量的偏移量,通过结构体变量首地址与偏移量定位成员变量
例如:
struct ST
{
int i ; //0
int j; //4
char c; //8
}
struct ST s = {0};
struct ST *pst = &s;
int *pi = &(pst->i); //(unsigned int )&s + 0 就是成员i的地址
int *pj = &(pst->j); //(unsigned int)&s + 4 就是成员j的地址
char *pc =&(pst->c) //(unsigned int)&s +8 就是成员c的地址
从上面可以看出,编译器根本没有真正访问pst所指向的地址中的内容,没有做任何的访问的工作.可以通过下面的成员进行验证:
#include <stdio.h>
struct ST
{
int i;
int j;
char c;
};
void func(struct ST* pst)
{
int* pi = &(pst->i);
int* pj = &(pst->j);
char* pc = &(pst->c);
printf("pst = %p\n",pst);
printf("pi = %p\n",pi);
printf("pj = %p\n",pj);
printf("pc = %p\n",pc);
}
int main()
{
struct ST s = {0};
func(&s);
func(NULL);
return 0;
}
结果:
sice@sice:~$ ./a.out
pst = 0xbfc49104
pi = 0xbfc49104
pj = 0xbfc49108
pc = 0xbfc4910c
pst = 0x00000000
pi = 0x00000004
pj = 0x00000008
pc = 0x0000000c
可以看出结构体中成员变量的偏移编译器是很清楚的,而且我们空指针调用func函数后是不会发生异常的,并且能够打印出我们想要的结果,所以可以知道(TYPE *)0)->MEMBER 这个地方不会真正的去访问0地址处的内容,仅仅是编译器做了一个加法而已.用0+MEMBER在结构体中的偏移量,最后计算出一个地址.在此处该地址与MEMBER在结构体中的偏移量相等,最后用(size_t) &转化为无符号类型后就得到我们想要MEMBER在TYPE中的偏移量
修改主函数代码:
int main()
{
struct ST s = {0};
func(&s);
printf("offset i : %d\n",offsetof(struct ST,i));
printf("offset j : %d\n",offsetof(struct ST,j));
printf("offset c : %d\n",offsetof(struct ST,c));
return 0;
}
结果:
sice@sice:~$ ./a.out
pst = 0xbfb49594
pi = 0xbfb49594
pj = 0xbfb49598
pc = 0xbfb4959c
offset i : 0
offset j : 4
offset c : 8
2.container_of介绍
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
作用:通过结构体成员变量member的地址,反推出member成员所在结构体变量的首地址,ptr指向成员变量member
解析1:
({ })是GNU C编译器的语法扩展
({ })与逗号表达式类似,结果作为最后一个语句的值
例子:
#include <stdio.h>
void method_1()
{
int a = 0;
int b = 0;
int r = (
a = 1,
b = 2,
a+b
);
printf("r=%d\n",r);
}
void method_2()
{
int r = (
{
int a =1;
int b =2;
a+b;
}
);
printf("r=%d\n",r);
}
int main()
{
method_1();
method_2();
return 0;
}
结果:
sice@sice:~$ ./a.out
r=3
r=3
解析2:
typeof是GNU C编译器的特有的关键字
typeof只在编译期生效,用于得到变量的类型
int i =100;
typeof(i) j = i; //int j = i;
const typeof(i) *p = &j; //const int*p = &j;
printf("sizeof(j) = %d\n",sizeof(j));
printf("j = %d\n",j);
printf("*p = %d\n",*p);
解析3
首先通过offsetof计算出成员变量c在结构体中的偏移量,pc是指向结构体中成员变量c的指针,(char *)pc 将pc指针强制类型转换为char ,目的就是为了做指针运算所实现的就是通过结构体中的一个成员变量的地址反推结构体变量的首地址,container_of就是利用这个原理
例子:
#include <stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
struct ST
{
int i;
int j;
char c;
};
int main()
{
struct ST s = {0};
char* pc = &s.c;
struct ST* pst = container_of(pc,struct ST,c);
printf("pst = %p\n",pst);
printf("&s = %p\n",&s);
return 0;
}
结果:
sice@sice:~$ ./a.out
pst = 0xbf80fa78
&s = 0xbf80fa78
读者可能就会有疑惑为什么不直接写成
container_of(ptr, type, member) ((type*)(char*)(ptr) - offsetof(type, member)))
也就是说为什么要加入以下语句
const typeof(((type *)0)->member) * __mptr = (ptr);
其实以上的代码是为了做类型检查
看这个例子
container_of的功能只能用宏来实现,宏其实是由预处理器在编译的时候来进行处理的.预处理器做的是单纯的文本替换,不会进行任何的类型检查.这就有可能导致我们在编写代码的时候由粗心大意而造成的错误,就像上面的这个错误误用了pi指针,这时候,为了增加代码的安全性,为了有一点点的类型检查,所以在linux内核中该宏的定义中加上了const typeof(((type *)0)->member) * __mptr = (ptr);这条语句。
可能还有疑问?
不使用({}),使用逗号表达式能否实现 不可以,因为里面有指针的定义,不能存在于逗号表达式中
(type *)0)->member 访问了0地址,会导致程序的崩溃吗?
typeof是在编译期有效,在编译期间就可以拿到成员变量的类型了,不用等到运行期间了.在运行的时候,该条语句就不存在了,因此不会导致程序的崩溃