Linux内核宏Container_Of的详细解释

 更新时间:2021年09月13日 11:56:55   作者:仲一  
我们在分析Linux内核链表的时候注意到内核在求解结构体偏移的时候巧妙的使用了container_of宏定义,本文我们就来详细剖析下内核到底是如何求解结构体成员变量的地址的

1. 结构体在内存中是如何存储的

int main() 
{ 
 
 Student stu; 
 stu.id = 123456; 
 strcpy(stu.name,"feizhufeifei"); 
 stu.math = 90; 
 stu.PE = 80; 
 printf("Student:%p\r\n",&stu); 
 printf("stu.ID:%p\r\n",&stu.ID); 
 printf("stu.name:%p\r\n",&stu.name); 
 printf("stu.math:%p\r\n",&stu.math); 
 return 0; 
} 

打印结果如下:

//结构体的地址 
Student:0xffffcbb0 
//结构体第一个成员的地址 
stu.ID:0xffffcbb0  //偏移地址 +0 
stu.name:0xffffcbb4//偏移地址 +4 
stu.math:0xffffcbd4//偏移地址 +24 


我们可以看到,结构体的地址和结构体第一个成员的地址是相同的。这也就是我们之前在拒绝造轮子!如何移植并使用Linux内核的通用链表(附完整代码实现)中提到的为什么在结构体中要把 struct list_head放在首位。

不太理解的再看下这两个例子:

  • struct A { int a; char b; int c; char d; };a 偏移为 0 , b 偏移为 4 , c 偏移为 8 (大于 4 + 1 的 4 的最小整数倍), d 偏移为 12 。A 对齐为 4 ,大小为 16 。
  • struct B { int a; char b; char c; long d; };a 偏移为 0 , b 偏移为 4 , c 偏移为 5 , d 偏移为 8 。B 对齐为 8 , 大小为 16 。

我们可以看到,结构体中成员变量在内存中存储的其实是偏移地址。也就是说结构体A的地址+成员变量的偏移地址 = 结构体成员变量的起始地址。

因此,我们也可以根据结构体变量的起始地址和成员变量的偏移地址来反推出结构体A的地址。

2. 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)); }) 


??首先看下三个参数, ptr是成员变量的指针, type是指结构体的类型, member是成员变量的名字。

??container_of宏的作用是通过结构体内某个成员变量的地址和该变量名,以及结构体类型,找到该结构体变量的地址。这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在结构中的偏移量,然后根据成员变量的地址反过来得出主结构变量的地址。下面具体分析下各个部分。

3. typeof

首先看下typeof,是用于返回一个变量的类型,这是GCC编译器的一个扩展功能,也就是说typeof是编译器相关的。既不是C语言规范的所要求,也不是某个标准的一部分。

int main() 
{ 
 int a = 5; 
 //这里定义一个和a类型相同的变量b 
 typeof(a) b  = 6; 
 printf("%d,%d\r\n",a,b);//5 6 
 return 0; 
} 

4. (((type *)0)->member)

((TYPE *)0) 将0转换为type类型的结构体指针,换句话说就是让编译器认为这个结构体是开始于程序段起始位置0,开始于0地址的话,我们得到的成员变量的地址就直接等于成员变量的偏移地址了。

(((type *)0)->member) 引用结构体中MEMBER成员。

typedef struct student{ 
 int id; 
 char name[30]; 
 int math; 
}Student; 
int main() 
{ 
 //这里时把结构体强制转换成0地址,然后打印name的地址。 
 printf("%d\r\n",&((Student *)0)->name);//4 
 return 0; 
} 

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

这句代码意思是用typeof()获取结构体里member成员属性的类型,然后定义一个该类型的临时指针变量__mptr,并将ptr所指向的member的地址赋给__mptr;

为什么不直接使用 ptr 而要多此一举呢?我想可能是为了避免对 ptr prt 指向的内容造成破坏。

6. offsetof(type, member))

((size_t) &((TYPE*)0)->MEMBER)

size_t是标准C库中定义的,在32位架构中被普遍定义为:

typedef unsigned int size_t;

而在64位架构中被定义为:

typedef unsigned long size_t;

可以从定义中看到,size_t是一个非负数,所以size_t通常用来计数(因为计数不需要负数区):

for(size_t i=0;i<300;i++)

为了使程序有很好的移植性,因此内核使用size_t,而不是int,unsigned。((size_t) &((TYPE*)0)->MEMBER) 结合之前的解释,我们可以知道这句话的意思就是求出MEMBER相对于0地址的一个偏移值。

7. (type * )((char * )__mptr - offsetof(type, member))

这句话的意思就是,把 __mptr 转换成 char* 类型。因为 offsetof 得到的偏移量是以字节为单位。两者相减得到结构体的起始位置, 再强制转换成 type 类型。

8. 举例

#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) );}) 
         
typedef struct student 
{ 
 int id; 
 char name[30]; 
 int math; 
}Student; 
 
int main() 
{ 
    Student stu; 
        Student *sptr = NULL; 
  stu.id = 123456; 
  strcpy(stu.name,"zhongyi"); 
  stu.math = 90; 
        sptr = container_of(&stu.id,Student,id); 
        printf("sptr=%p\n",sptr); 
        sptr = container_of(&stu.name,Student,name); 
        printf("sptr=%p\n",sptr); 
        sptr = container_of(&stu.math,Student,id); 
        printf("sptr=%p\n",sptr); 
        return 0;  
} 


运行结果如下:

sptr=0xffffcb90
sptr=0xffffcb90
sptr=0xffffcbb4

宏展开可能会看的更清楚一些

int main() 
{ 
    Student stu; 
        Student *sptr = NULL; 
  stu.id = 123456; 
  strcpy(stu.name,"zhongyi"); 
  stu.math = 90; 
  //展开替换 
        sptr = ({ const unsigned char  *__mptr = (&stu.id); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->id) );}); 
        printf("sptr=%p\n",sptr); 
        //展开替换 
        sptr = ({ const unsigned char  *__mptr = (&stu.name); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->name) );}); 
        printf("sptr=%p\n",sptr); 
        //展开替换 
        sptr = ({ const unsigned int *__mptr = (&stu.math); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->math) );}); 
        printf("sptr=%p\n",sptr); 
        return 0;  
} 

到此这篇关于Linux内核中Container_Of宏的详细解释的文章就介绍到这了,更多相关Linux内核中Container_Of宏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • linux epoll机制详解

    linux epoll机制详解

    这篇文章主要介绍了linux epoll机制详解,介绍了select()和poll() IO多路复用模型,epoll IO多路复用模型实现机制,epoll的接口和工作模式等相关内容,小编觉得还是挺不错的,这里分享给大家,需要的朋友可以参考下
    2018-01-01
  • 详解ubuntu 16.04 LTS 降级安装gcc 4.8

    详解ubuntu 16.04 LTS 降级安装gcc 4.8

    本篇文章主要介绍了ubuntu 16.04 LTS 降级安装gcc 4.8 ,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-04-04
  • CentOS 7 中firewall-cmd命令详细介绍

    CentOS 7 中firewall-cmd命令详细介绍

    这篇文章主要介绍了 CentOS 7 中firewall-cmd命令详细介绍的相关资料,这里对CentOS 7的firewall-cmd命令一一介绍,希望能帮助开始使用的朋友,需要的朋友可以参考下
    2016-11-11
  • ubuntu下没有中文输入法的解决办法

    ubuntu下没有中文输入法的解决办法

    今天小编就为大家分享一篇关于ubuntu下没有中文输入法的解决办法,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-03-03
  • git拉源码maven构建后部署脚本详解

    git拉源码maven构建后部署脚本详解

    这篇文章主要介绍了git拉源码maven构建后部署脚本详解的相关资料,需要的朋友可以参考下
    2017-01-01
  • CentOS 7如何快速开放端口

    CentOS 7如何快速开放端口

    这篇文章主要为大家详细介绍了CentOS 7如何快速开放端口,如何使用firewalld开放Linux端口,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-01-01
  • Linux中英文命令对应

    Linux中英文命令对应

    本文我们把Linux的中英文命令做了对应翻译,给需要的朋友参考一下。
    2017-12-12
  • linux tar压缩排除某个文件夹的方法

    linux tar压缩排除某个文件夹的方法

    下面小编就为大家带来一篇linux tar压缩排除某个文件夹的方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-12-12
  • Centos7/RHEL7双网卡绑定的方法

    Centos7/RHEL7双网卡绑定的方法

    本篇文章主要介绍了Centos7/RHEL7双网卡绑定的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • linux下的守护进程

    linux下的守护进程

    守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。同时,守护进程完成许多系统任务。
    2014-11-11

最新评论