嵌入式C语言自我修养 (04):Linux 内核第一宏:container_of

4.1 typeof 关键字

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

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

int i ;
typeof(i) j = 20;
​
typeof(int *) a;
​
int f();
typeof(f()) k;

在上面的代码中,因为变量 i 的类型为 int,所以 typeof(i) 就等于 int,typeof(i) j =20 就相当于 int j = 20,typeof(int *) a; 相当于 int * a;,函数也是有类型的,函数的类型即其返回值类型,所以 typeof(f()) k; 就相当于 int k;

 

4.2 typeof 使用示例

根据上面 typeof 的用法,我们编写一个程序,来学习一下 typeof 的使用。

int main(void)
{
    int i = 2;
    typeof(i) k = 6;
​
    int *p = &k;
    typeof(p) q = &i;
​
    printf("k = %d\n",k); 
    printf("*p= %d\n",*p); 
    printf("i = %d\n",i); 
    printf("*q= %d\n",*q); 
    return 0;
}

运行结果为:

k  = 6
*p = 6
i  = 2
*q = 2

通过运行结果可知,通过 typeof 获取一个变量的类型 int 后,可以使用该类型再定义一个变量。这跟我们直接使用 int 定义一个变量,效果是一样的。

 

4.3 typeof 的其它使用方法

除了使用 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]

 

4.4 继续完善 MAX(a,b) 宏

在上一节中,我们定义了一个宏 MAX(x,y),用来求出两个数中较大的那个,而且可以支持不同类型数据:

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

这个宏虽然可以支持任意数据类型,但是仍有瑕疵:我们必须把数据的类型作为一个单独的参数传递给宏。接下来,我们继续优化这个宏:不需要再单独传递这个参数,而是使用 typeof 关键字来直接获取参数的数据类型。

#define MAX(x,y)({     \
    typeof(x) _x = x;        \
    typeof(x) _y = y;        \
    _x > _y ? _x : _y; \
})
​
int main(void)
{
    int i = 2;
    int j = 6;
    printf("max: %d\n", MAX(i, j));
    printf("max: %f\n", MAX(3.14, 3.15));
    return 0;
}

通过 typeof 直接获取宏的参数类型,这样我们就不必再单独将参数的类型传给宏了。改进后的宏同样也支持任意类型的数据比较大小。在 main 函数中,我们分别使用这个宏去比较 int 型数据和 float 型数据,发现都可以正常工作!是不是很酷?等你面试时把这个宏写给面试官看,你觉得面试官还会舍得让你回去等消息么?

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

#define swap(a, b) \
do {               \
    typeof(a) __tmp = (a);  \ 
    (a) = (b);         \
    (b) = __tmp; \
} while (0)

 

4.5 typeof 在内核中的应用

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

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

在 min\max 宏定义中,使用 typeof 直接获取参数类型,就不必再给宏单独传递参数 type 了。内核中定义的宏跟我们上面举的例子有点不一样,多了一行代码:

(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 语言功底更加深厚。不要走,我们接着分析 Linux 内核中另一个更有意思的宏。

 

4.6 Linux 内核中的 container_of 宏

container_of 宏介绍

有了上面语句表达式和 typeof 的基础知识,接下来我们就可以分析 Linux 内核第一宏:container_of。这个宏在 Linux 内核中应用甚广。会不会用这个宏,看不看得懂这个宏,也逐渐成为考察一个内核驱动开发者 C 语言功底的不成文标准。废话少说,我们还是先一睹芳容吧。

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

作为内核第一宏,绝对不是盖的:看看这身段,这曲线,高端大气上档次,低调奢华有内涵,不出去再做个头发,简直就是暴殄天物。GNU C 高端扩展特性的综合运用,宏中有宏,不得不佩服内核开发者这天才般地设计。那这个宏到底是干什么的呢?它的主要作用就是:根据结构体某一成员的地址,获取这个结构体的首地址。根据宏定义,我们可以看到,这个宏有三个参数,它们分别是:

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

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

 

container_of 宏使用示例

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

struct student
{
    int age;
    int num;
    int math;
};
int main(void)
{
    struct student stu;
    struct student *p;
    p = container_of( &stu.num, struct student, num);
    return 0;
}

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

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

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

struct student
{
    int age;
    int num;
    int math;
};
int main(void)
{
    struct student stu = { 20, 1001, 99};
​
    int *p = &stu.math;
    struct student *stup = NULL;
    stup = container_of( p, struct student, math);
    printf("%p\n",stup);
    printf("age: %d\n",stup->age);
    printf("num: %d\n",stup->num);
​
    return 0;     
}

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

 

4.7 container_of 宏实现分析

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

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

 

结构体在内存中的存储

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

struct student{
    int age;
    int num;
    int math;
};
int main(void)
{
    struct student stu = { 20, 1001, 99};
    printf("&stu = %p\n", &stu);
    printf("&stu.age =%p\n", &stu.age);
    printf("&stu.num =%p\n", &stu.num);
    printf("&stu.math =%p\n", &stu.math);
​
    return 0;
}

在这个程序中,我们定义一个结构体,里面有三个 int 型数据成员,我们定义一个变量,然后分别打印结构体的地址、各个成员变量的地址,运行结果如下:

&stu      = 0028FF30
&stu.age  = 0028FF30
&stu.num  = 0028FF34
&stu.math = 0028FF38

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

 

计算成员变量在结构体内的偏移

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

struct student{
    int age;
    int num;
    int math;
};
int main(void)
{
    printf("&age = %p\n",&((struct student*)0)->age);
    printf("&num = %p\n",&((struct student*)0)->num);
    printf("&math= %p\n",&((struct student*)0)->math);
    return 0;   
}

在上面的程序中,我们没有直接定义结构体变量,而是将数字0,通过强制类型转换,转换为一个指向结构体类型为 student 的常量指针,然后分别打印这个常量指针指向的结构体的各成员地址。运行结果如下:

&age = 00000000
&num = 00000004
&math= 00000008

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

 

container_of 宏的实现

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

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

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

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

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

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

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

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

因为结构体的成员数据类型可以是任意数据类型,所以为了让这个宏兼容各种数据类型。我们定义了一个临时指针变量 __mptr,该变量用来存储结构体成员 MEMBER 的地址,即存储 ptr 的值。那如何获取 ptr 指针类型呢,通过下面的方式:

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 *)。

 

小结

好了,到这里,我们对 container_of 宏的分析也就接近尾声了。任何一个复杂的东西,我们都可以把它分解,运用所学的基础知识一点一点剖析:先去降维分析,然后再进行综合。比如 container_of 宏的定义,就运用了结构体的存储、语句表达式、typeof 等知识点。掌握了这些基础知识,有了分析方法,以后在内核中再遇到这样类似的宏,就不用再百度、Google了,万一搜不到怎么办?在这样一个考察工程师技术能力的关键时刻,我们可以自信从容地去自己分析了。这就是你的核心竞争力,也是你超越其他工程师、脱颖而出的机会。

 

本文根据《C语言嵌入式Linux高级编程》部分章节改编,视频学习可访问优快云学院:https://edu.youkuaiyun.com/combo/detail/1038

微信公众号:宅学部落(armlinuxfun)

QQ群:475504428

更多嵌入式视频教程:https://wanglitao.taobao.com

电子书籍下载地址:https://pan.baidu.com/s/1a6L0cyIQKKLlmIfRw7U6Dg

<think> 我们正在讨论的是`container_of`对C标准版本的要求。根据提供的引用,`container_of`的实现依赖于两个关键部分: 1. `offsetof`:用于计算结构体中成员的偏移量。 2. 指针运算:通过将成员的指针减去其偏移量来获取整个结构体的起始地址。 其中,`offsetof`在C89/C90标准中就已经是标准库的一部分(定义在`<stddef.h>`中)。而指针运算(特别是将指针转换为`char*`然后进行算术运算)在C语言中一直是被支持的。 因此,我们可以得出以下结论: ### `container_of`对C标准版本的支持情况 1. **C89/C90标准**: - 完全支持,因为: - `offsetof`已标准化(在`<stddef.h>`中)[^1][^4] - 指针与`char*`的转换及指针算术运算符合标准 2. **C99标准**: - 完全支持,保留C89特性并增加新特性但不影响`container_of`的使用 3. **C11/C17标准**: - 完全支持,基础特性不变 ### 注意事项 尽管标准C支持`container_of`的实现,但实际应用中需注意: 1. **严格别名规则(Strict Aliasing)**: ```c type *p = (type*)((char*)ptr - offset); ``` 这种转换可能违反严格别名规则(通过一种类型的指针访问另一种类型的对象)。但内核中通常通过`-fno-strict-aliasing`编译选项禁用此规则[^4]。 2. **对齐问题**: 指针运算假设结构体起始地址与成员指针满足对齐要求,这在标准C中通常成立[^3]。 3. **未定义行为(UB)**: 原始内核实现中的`((type*)0)->member`在标准C中属于未定义行为(对空指针解引用),但实际使用`offsetof`可避免[^1]: ```c // 标准C推荐使用<stddef.h>中的offsetof #define container_of(ptr, type, member) \ (type*)((char*)(ptr) - offsetof(type, member)) ``` ### 代码示例(标准C兼容版本) ```c #include <stddef.h> // 必需的头文件,提供offsetof #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type, member) ); \ }) ``` 说明: - 第一行`typeof`是GCC扩展,非标准C。若需完全符合标准,可省略类型检查: ```c // 完全符合标准C的简化版本(去除了类型检查) #define container_of(ptr, type, member) \ ((type*)((char*)(ptr) - offsetof(type, member))) ``` ### 总结支持情况 | C标准版本 | 是否支持 | 说明 | |-----------|------------------------|----------------------------------------| | C89/C90 | ✅ 支持 | 需包含`<stddef.h>`使用标准`offsetof` | | C99 | ✅ 支持 | 同C89 | | C11/C17 | ✅ 支持 | 基础特性不变 | | 标准C++ | ❌ 不支持(需改造) | C++的`offsetof`限制较多,且类型系统不同 | ### 相关问题 §§相关问题§§ 1. 如何在严格遵循C99标准的项目中安全使用`container_of`? 2. 使用`container_of`可能违反哪些C语言未定义行为? 3. 在C++中如何实现类似`container_of`的功能? 4. 为什么Linux内核的`container_of`实现要使用`typeof`扩展? 5. `offsetof`在不同编译器中的实现差异有哪些? </think> ### `container_of`对C标准版本的要求分析 #### 1. **支持的标准版本** `container_of`的实现主要依赖**指针运算**和**结构体偏移量计算**,这些特性在以下C标准中完全支持: | 标准版本 | 支持情况 | 关键特性说明 | |----------|----------|--------------| | **C89/C90** | ✅ 完全支持 | 支持基础指针运算和结构体地址计算[^1][^4] | | **C99** | ✅ 完全支持 | 保留C89特性,增加`inline`等新特性但不影响`container_of` | | **C11/C17** | ✅ 完全支持 | 兼容所有底层指针操作机制 | **代码实现基础**: ```c /* 标准C兼容版本 */ #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) #define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member))) ``` 此实现在**所有符合ANSI C标准的编译器**中均可工作(包括GCC、Clang、MSVC等)[^1][^4]。 #### 2. **不依赖特定标准的特性** 核心实现仅使用: 1. 空指针转换:`(TYPE *)0` 2. 结构体成员地址访问:`&((type*)0)->member` 3. 指针算术运算:`(char*)ptr - offset` 这些操作在**所有C标准**中均被明确定义[^3][^5]。 #### 3. **特定版本增强实现** 虽然基础版本在所有标准中可用,但Linux内核实现使用了**GCC扩展**来增强安全性: ```c /* Linux内核版本(含GCC扩展)*/ #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ // GCC的typeof (type *)( (char *)__mptr - offsetof(type, member) ); \ // 标准C部分 }) ``` 扩展特性依赖: - `typeof`:GCC扩展(非标准C) - `({...})`:语句表达式(GCC扩展) #### 4. **版本适配建议** | 使用场景 | 推荐实现方式 | 兼容性 | |-------------------|----------------------------------|----------------------| | 跨平台/严格标准C | 纯标准C版本(无GCC扩展) | C89及以上所有版本 | | Linux内核模块 | 含GCC扩展的增强版 | 需GCC编译器 | | 嵌入式系统 | 根据编译器支持选择 | 需验证编译器特性 | #### 5. **特殊注意事项** 1. **未定义行为警告**: 严格来说,`((type*)0)->member`属于**未定义行为**(解引用空指针)。但在实践中,由于仅取地址而不真正访问内存,所有主流编译器都支持此用法[^1][^4]。 2. **内存对齐问题**: 标准C保证结构体成员按声明顺序排列,但可能有内存填充。`offsetof`会计算包含填充的实际偏移量: ```c struct Example { char a; // 偏移0 // 可能3字节填充 int b; // 偏移4(假设4字节对齐) }; ``` 3. **C++兼容性**: C++中需要改造实现: ```cpp template <class P, class T, class M> P container_of(M T::*member, const M* ptr) { return reinterpret_cast<P>( reinterpret_cast<const char*>(ptr) - reinterpret_cast<size_t>(&(static_cast<T*>(nullptr)->*member)) ); } ``` ### 总结 - **所有C标准**(C89/C90及以上)都支持`container_of`的标准实现 - **Linux增强版**依赖GCC扩展,需特定编译器支持 - 在严格标准C项目中建议使用**纯标准C版本**(不含`typeof`和语句表达式) ---
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅学部落-王利涛

just for test

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值