第31课 - 老生常谈的两个宏
第31课 - Linux老生常谈的两个宏
1. Linux 内核中常用的两个宏定义
1.1 offsetof 宏
- 在 include/linux/stddef.h 头文件中定义
- TYPE 是结构体类型、MEMBER 是结构体中一个成员的成员名
- 作用:offsetof 宏返回的是 MEMBER 成员相对于整个结构体变量的首地址的偏移量,类型是 size_t(unsigned int)
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
1.2 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) );})
- 在 include/linux/kernel.h 头文件中定义
- type 是结构体类型、member是该结构体的某一个成员、ptr 是指向成员 member 的指针
- container_of 宏使用 offsetof 宏完成其功能
- 作用:container_of 宏通过结构体变量中一个成员的地址得到这个结构体变量的首地址
上面这两个宏在 Linux 内核中非常常见,比如 linux 内核链表 list_head、工作队列 work_struct 中。乍一看,这两个宏给人感觉很复杂,尤其是 container_of 宏。
其实,再复杂的结构也是由简单的东西构造而成的,下面我们就一层一层的剥开这两个宏的真相!!!
2. 逐步分析这两个宏
2.1 分析 offsetof 宏
offsetof 用于计算 TYPE 结构体中 MEMBER 成员的偏移位置。
第一次看到这个宏定义,第一反应就是直接使用 0 地址不会导致程序崩溃吗?
要理解这里需要知道编译器在其中扮演的角色。
(1)编译器清楚的知道结构体成员变量的偏移位置
(2)编译器通过结构体变量首地址与结构体成员的偏移量定位成员变量
下面的代码片段展示了编译器如何在编译期间计算结构体成员的地址:(注意这里都是在编译期间完成的,并没有实际的访问结构体成员)
如果 pst 为 NULL(0),那么结构体成员的地址就等于该成员在所处结构体中的偏移量,这也是 offsetof 宏实现其功能的关键所在。
【编程实战】
1 #include <stdio.h> 2 3 #ifndef offsetof 4 #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER) 5 #endif 6 7 struct ST 8 { 9 int i; // 0 10 int j; // 4 11 char c; // 8 12 }; 13 14 void func(struct ST* pst) 15 { 16 int* pi = &(pst->i); // 0 17 int* pj = &(pst->j); // 4 18 char* pc = &(pst->c); // 8 19 20 printf("pst = %p\n", pst); 21 printf("pi = %p\n", pi); 22 printf("pj = %p\n", pj); 23 printf("pc = %p\n", pc); 24 } 25 26 int main() 27 { 28 struct ST s = {0}; 29 30 func(&s); 31 func(NULL); 32 33 printf("offset i: %d\n", offsetof(struct ST, i)); 34 printf("offset j: %d\n", offsetof(struct ST, j)); 35 printf("offset c: %d\n", offsetof(struct ST, c)); 36 37 return 0; 38 }
// 输出结果
2.2 ({}) 是何方神圣?
(1)({}) 是GNU C 编译器的语法扩展
(2)({}) 与逗号表达式类似,结果为最后一个语句的值
(3)因此 container_of 宏最终的结果是其第二条语句的值
【编程实验】
1 #include <stdio.h> 2 3 int main(void) 4 { 5 int r = ({ 6 int a = 1; 7 int b = 2; 8 a + b; 9 }); 10 11 printf("r = %d\n", r); // r = 3,即 a+b 的值 12 13 return 0; 14 }
2.3 typeof 是一个关键字吗?
(1)typeof 是 GNU C 编译器的特有关键字
(2)typeof 只在编译器生效(和 sizeof 一样),用于得到变量的类型
结合上面对 ({}) 的解析,我们知道 container_of 宏最终的结果是第二条语句的值。再结合这里 typeof 关键字的含义,就可以理解 container_of 宏中第一句代码的含义,它起到一个类型安全检查的作用,如果传入的 ptr 指针的类型与 member 成员的类型不符,编译时就会发出警告。(专业的程序员应该把警告当成错误处理)(指针定义不能在逗号表达式中完成,因此引入了新的语法 ({}) )
【编程实验】
1 #include <stdio.h> 2 3 int main(void) 4 { 5 int i = 100; 6 typeof(i) j = i; // int j = i; 7 const typeof(i) *p = &j; // const int *p = &j 8 9 printf("sizeof(i) = %lu\n", sizeof(j)); // sizeof(i) = 4 10 printf("j = %d\n", j); // j = 100 11 printf("*p = %d\n", *p); // *p = 100 12 13 return 0; 14 }
2.4 最后的原理
经过上面几个知识点的学习,再结合下面这张图 container_of 宏的原理,是不是立马就明白了!!!
整个结构体的首地址就等于该结构体某一成员的地址减去该成员的偏移地址!
3. 小结
(1)编译器清楚的知道结构体成员变量的偏移位置
(2)({}) 与逗号表达式类似,结果为最后一个语句的值
(3)typeof 只在编译期生效,用于得到变量的类型
(4)container_of 使用 ({}) 进行类型安全检查
注:本文整理于狄泰《数据结构实战开发教程》课程内容
狄泰QQ群:199546072
本人QQ号:502218614