您的位置:首页 > 编程语言 > C语言/C++

【嵌入式】C语言高级编程-container_of宏(04)

2020-07-06 21:09 113 查看

00. 目录

文章目录

01. typeof 关键字

GNU C 扩展了一个关键字 typeof,用来获取一个变量或表达式的类型。这里使用关键字可能不太合适,因为毕竟 typeof 还没有被写入 C 标准,是 GCC 扩展的一个关键字。为了方便,我们就姑且称之为关键字吧。

通过使用 typeof,我们可以获取一个变量或表达式的类型。所以 typeof 的参数有两种形式:表达式或类型

示例:

int i ;

//typeof(i)等价于int
//等价于 int j = 20;
typeof(i) j = 20;

//等价于int * a;
typeof(int *) a;

int f();

//typeof(f())等价于int
//等价于int k;
typeof(f()) k;

程序示例

#include <stdio.h>

int main(void)
{
int i = 1;

//等价于 int j = 6;
typeof(i) j = 6;

int *p = &j;

//等价于int * q = &i;
typeof(p) q = &i;

printf("j = %d\n", j);
printf("*p = %d\n", *p);
printf("i = %d\n", i);
printf("*q = %d\n", *q);

return 0;
}

执行结果

deng@itcast:~/tmp$ gcc 7.c
deng@itcast:~/tmp$ ./a.out
j = 6
*p = 6
i = 1
*q = 1

typeof高级用法

typeof (int *) y;   // 把 y 定义为指向 int 类型的指针,相当于int *y;
typeof (int)  *y;   //定义一个执行 int 类型的指针变量 y
typeof (*x) y;      //定义一个指针 x 所指向类型 的指针变量y
typeof (int) y[4];  //相当于定义一个:int y[4]
typeof (*x) y[4];   //把 y 定义为指针 x 指向的数据类型的数组
typeof (typeof (char *)[4]) y;//相当于定义字符指针数组:char *y[4];
typeof(int x[4]) y;  //相当于定义:int y[4]

02. typeof与宏结合

使用 typeof 关键字来直接获取参数的数据类型。

#define MAX(x, y) ({    \
typeof(x) _x = x;   \
typeof(y) _y = y;   \
(void)(&_x == &_y);  \
_x > _y ? _x : _y;  \
})

有了这个思路,我们同样也可以将以前定义的一些宏通过这种方式改写,这样 SWAP 宏也可以支持多种类型的数据了。

linux-3.5/include/linux/kernel.h

/*
* swap - swap value of @a and @b
*/
#define swap(a, b) \
do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0)

03. typeof在内核源码中应用

关键字 typeof 在 Linux 内核中被广泛使用,主要用在宏定义中,用来获取宏参数类型。比如内核中,min/max 宏的定义:

linux-3.5/include/linux/kernel.h

/*
* min()/max()/clamp() macros that also do
* strict type-checking.. See the
* "unnecessary" pointer comparison.
*/
#define min(x, y) ({                \
typeof(x) _min1 = (x);          \
typeof(y) _min2 = (y);          \
(void) (&_min1 == &_min2);      \
_min1 < _min2 ? _min1 : _min2; })

#define max(x, y) ({                \
typeof(x) _max1 = (x);          \
typeof(y) _max2 = (y);          \
(void) (&_max1 == &_max2);      \
_max1 > _max2 ? _max1 : _max2; })

#define min3(x, y, z) ({            \
typeof(x) _min1 = (x);          \
typeof(y) _min2 = (y);          \
typeof(z) _min3 = (z);          \
(void) (&_min1 == &_min2);      \
(void) (&_min1 == &_min3);      \
_min1 < _min2 ? (_min1 < _min3 ? _min1 : _min3) : \
(_min2 < _min3 ? _min2 : _min3); })

#define max3(x, y, z) ({            \
typeof(x) _max1 = (x);          \
typeof(y) _max2 = (y);          \
typeof(z) _max3 = (z);          \
(void) (&_max1 == &_max2);      \
(void) (&_max1 == &_max3);      \
_max1 > _max2 ? (_max1 > _max3 ? _max1 : _max3) : \
(_max2 > _max3 ? _max2 : _max3); })

/**
* min_not_zero - return the minimum that is _not_ zero, unless both are zero
* @x: value1
* @y: value2
*/
#define min_not_zero(x, y) ({           \
typeof(x) __x = (x);            \
typeof(y) __y = (y);            \
__x == 0 ? __y : ((__y == 0) ? __x : min(__x, __y)); })

内核中定义的宏跟我们上面举的例子有点不一样,多了一行代码:

(void) (&_max1 == &_max2);

看起来是一句废话,其实用得很巧妙!它主要是用来检测宏的两个参数 x 和 y 的数据类型是否相同。如果不相同,编译器会给一个警告信息,提醒程序开发人员。

warning:comparison of distinct pointer types lacks a cast

让我们分析一下,它是怎么实现的:语句 &_max1 == &_max2 用来判断两个变量 _max1 和 _max2的地址是否相等,即比较两个指针是否相等。&_max1 和 &_max2分别表示两个不同变量的地址,怎么可能相等呢!既然大家都知道,内存中两个不同的变量地址肯定不相等,那为什么还要在此多此一举呢?妙就妙在,当两个变量类型不相同时,对应的地址,即指针类型也不相同。比如一个 int 型变量,一个 char 变量,对应的指针类型,分别为 char * 和 int *,而两个指针比较,它们必须是同种类型的指针,否则编译器会有警告信息。所以,通过这种“曲线救国”的方式,这行程序语句就实现了这样一个功能:当宏的两个参数类型不相同时,编译器会及时给我们一个警告信息,提醒开发者。

看完这个宏的实现,不得不感叹内核的博大精深!每一个细节,每一个不经意的语句,细细品来,都能学到很多知识,让你的 C 语言功底更加深厚。

04. container_of 宏分析

知道了 container_of 宏的用法之后,我们接着去分析这个宏的实现。作为一名 Linux 内核驱动开发者,除了要面对各种手册、底层寄存器,有时候还要应付底层造轮子的事情,为了系统的稳定和性能,有时候我们不得不深入底层,死磕某个模块,进行分析和优化。底层的工作虽然很有挑战性,但有时候也是很枯燥的,不像应用开发那样有意思。所以,为了提高对工作的兴趣,大家表面上虽然不说自己牛 X,但内心深处,一定要建立起自己的职位优越感。人不可有傲气,但一定要有傲骨:我们可不像应用开发,知道 API 接口、读读文档、完成功能就 OK 了。作为一名底层开发者,要时刻记住,要和寄存器、内存、硬件电路等各族底层群众打成一片。从群众中来,到群众中去,急群众所急,想群众所想,这样才能构建一个稳定和谐的嵌入式系统:稳定高效、上下通畅、运行365个日出也不崩溃。

container_of 宏的实现主要用到了我们上两节所学的知识:语句表达式和 typeof,再加上结构体存储的基础知识。为了帮助大家更好地理解这个宏,我们先复习下结构体存储的基础知识。

结构体内存布局

我们知道,结构体作为一个复合类型数据,它里面可以有多个成员。当我们定义一个结构体变量时,编译器要给这个变量在内存中分配存储空间。除了考虑数据类型、字节对齐因素之外,编译器会按照结构体中各个成员的顺序,在内存中分配一片连续的空间来存储它们。

程序示例

struct student
{
int id;
char sex;
int age;
};

int main(void)
{
struct student s = {1, 'M', 18};

printf("&s = %p\n", &s);
printf("&s.id = %p\n", &s.id);
printf("&s.sex = %p\n", &s.sex);
printf("&s.age = %p\n", &s.age);

return 0;
}

执行结果

deng@itcast:~/tmp$ ./a.out
&s = 0x7ffd3562d00c
&s.id = 0x7ffd3562d00c
&s.sex = 0x7ffd3562d010
&s.age = 0x7ffd3562d014

从运行结果我们可以看到,结构体中的每个成员变量,从结构体首地址开始,依次存放。每个成员变量相对于结构体首地址,都有一个固定偏移。比如 sex 相对于结构体首地址偏移了4个字节。age的存储地址,相对于结构体首地址偏移了8个字节。

计算结构体成员在结构体中的偏移
一个结构体数据类型,在同一个编译环境下,各个成员相对于结构体首地址的偏移是固定的。我们可以修改一下上面的程序,当结构体的首地址为0时,结构体中的各成员地址在数值上等于结构体各成员相对于结构体首地址的偏移。

程序示例

struct student
{
int id;
char sex;
int age;
};

int main(void)
{
printf("&id: %p\n", &((struct student*)0)->id);
printf("&id: %p\n", &((struct student*)0)->sex);
printf("&id: %p\n", &((struct student*)0)->age);

return 0;
}

执行结果

deng@itcast:~/tmp$ ./a.out
&id: (nil)
&id: 0x4
&id: 0x8

因为常量指针为0,即可以看做结构体首地址为0,所以结构体中每个成员变量的地址即为该成员相对于结构体首地址的偏移。container_of 宏的实现就是使用这个技巧来实现的。

计算偏移的宏实现

那如何计算结构体某个成员在结构体内的偏移呢?内核中定义了 offset 宏来实现这个功能,我们且看它的定义:

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

这个宏有两个参数,一个是结构体类型 TYPE,一个是结构体的成员 MEMBER,它使用的技巧跟我们上面计算0地址常量指针的偏移是一样的:将0强制转换为一个指向 TYPE 的结构体常量指针,然后通过这个常量指针访问成员,获取成员 MEMBER 的地址,其大小在数值上就等于 MEMBER 在结构体 TYPE 中的偏移。

container_of 宏的实现

有了上面的基础,我们再去分析 container_of 宏的实现就比较简单了。知道了结构体成员的地址,如何去获取结构体的首地址?很简单,直接拿结构体成员的地址,减去该成员在结构体内的偏移,就可以得到该结构体的首地址了。

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
/**
* 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) );})

从语法角度,我们可以看到,container_of 宏的实现由一个语句表达式构成。语句表达式的值即为最后一个表达式的值:

(type *)( (char *)__mptr - offsetof(type,member) );

最后一句的意义就是,拿结构体某个成员 member 的地址,减去这个成员在结构体 type 中的偏移,结果就是结构体 type 的首地址。因为语句表达式的值等于最后一个表达式的值,所以这个结果也是整个语句表达式的值,container_of 最后就会返回这个地址值给宏的调用者。

因为结构体的成员数据类型可以是任意数据类型,所以为了让这个宏兼容各种数据类型。我们定义了一个临时指针变量

__mptr
,该变量用来存储结构体成员 MEMBER 的地址,即存储 ptr 的值。那如何获取 ptr 指针类型呢,通过下面的方式:

const typeof( ((type *)0)->member ) *__mptr = (ptr);

我们知道,宏的参数 ptr 代表的是一个结构体成员变量 MEMBER 的地址,所以 ptr 的类型是一个指向 MEMBER 数据类型的指针,当我们使用临时指针变量 __mptr 来存储 ptr 的值时,必须确保 __mptr 的指针类型是一个指向 MEMBER 类型的指针变量。typeof( ((type *)0)->member )表达式使用 typeof 关键字,用来获取结构体成员 member 的数据类型,然后使用该类型,使用 typeof( ((type *)0)->member ) *__mptr 这行程序语句,就可以定义一个指向该类型的指针变量了。

还有一个需要注意的细节就是:在语句表达式的最后,因为返回的是结构体的首地址,所以数据类型还必须强制转换一下,转换为 TYPE* ,即返回一个指向 TYPE 结构体类型的指针,所以你会在最后一个表达之中看到一个强制类型转换(TYPE *)。

05. container_of 宏应用

有了上面语句表达式和 typeof 的基础知识,接下来我们就可以分析 Linux 内核第一宏:container_of。这个宏在 Linux 内核中应用甚广。

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
/**
* 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) );})

GNU C 高端扩展特性的综合运用,宏中有宏,不得不佩服内核开发者这天才般地设计。那这个宏到底是干什么的呢?它的主要作用就是:根据结构体某一成员的地址,获取这个结构体的首地址。根据宏定义,我们可以看到,这个宏有三个参数,它们分别是:

  • type:结构体类型
  • member:结构体内的成员
  • ptr:结构体内成员member的地址

也就是说,我们知道了一个结构体的类型,结构体内某一成员的地址,就可以直接获得到这个结构体的首地址。container_of 宏返回的就是这个结构体的首地址。

比如现在,我们定义一个结构体类型 student:

程序示例

#include <stdio.h>

struct student
{
int id;
char sex;
int age;
};

int main(void)
{
struct student s;

printf("&s = %p\n", &s);

printf("&s = %p\n", container_of(&s.sex, struct student, sex));

return 0;
}

执行结果

deng@itcast:~/tmp$ gcc 7.c
deng@itcast:~/tmp$ ./a.out
&s = 0x7ffc101be72c
&s = 0x7ffc101be72c

在这个程序中,我们定义一个结构体类型 student,然后定义一个结构体变量 s,我们现在已经知道了结构体成员变量 s.sex的地址,那我们就可以通过 container_of 宏来获取结构体变量 s的首地址。

这个宏在内核中非常重要。我们知道,Linux 内核驱动中,为了抽象,对数据结构体进行了多次封装,往往一个结构体里面嵌套多层结构体。也就是说,内核驱动中不同层次的子系统或模块,使用的是不同封装程度的结构体,这也是 C 语言的面向对象思想。分层、抽象、封装,可以让我们的程序兼容性更好,适配更多的设备,但同时也增加了代码的复杂度。

我们在内核中,经常会遇到这种情况:我们传给某个函数的参数是某个结构体的成员变量,然后在这个函数中,可能还会用到此结构体的其它成员变量,那这个时候怎么办呢?container_of 就是干这个的,通过它,我们可以首先找到结构体的首地址,然后再通过结构体的成员访问就可以访问其它成员变量了。

程序示例

struct student
{
int id;
char sex;
int age;
};

int main(void)
{
struct student s = {1, 'M', 18};

int *p = &s.age;

struct student *p1 = NULL;

p1 = container_of(p, struct student, age);

printf("&s = %p\n", &s);
printf("id:%d\n", p1->id);
printf("sex:%c\n", p1->sex);
printf("age: %d\n", p1->age);

return 0;
}

执行结果

deng@itcast:~/tmp$ ./a.out
&s = 0x7ffd1ca8e6bc
id:1
sex:M
age: 18

在这个程序中,我们定义一个结构体变量 s,知道了它的成员变量 age 的地址 &stu.age,我们就可以通过 container_of 宏直接获得 s结构体变量的首地址,然后就可以直接访问 s结构体的其它成员 s->id和 s->sex。

06. 附录

参考:C语言嵌入式Linux高级编程

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: