服务器之家:专注于服务器技术及软件下载分享
分类导航

Linux|Centos|Ubuntu|系统进程|Fedora|注册表|Bios|Solaris|Windows7|Windows10|Windows11|windows server|

服务器之家 - 服务器系统 - Linux - Linux内核中Container_Of宏的详细解释

Linux内核中Container_Of宏的详细解释

2021-09-09 23:34嵌入式与Linux那些事仲一 Linux

我们在分析Linux内核链表的时候注意到内核在求解结构体偏移的时候巧妙的使用了container_of宏定义,今天我们来详细剖析下内核到底是如何求解结构体成员变量的地址的。

Linux内核中Container_Of宏的详细解释

我们在分析Linux内核链表的时候注意到内核在求解结构体偏移的时候巧妙的使用了container_of宏定义,今天我们来详细剖析下内核到底是如何求解结构体成员变量的地址的。

  • 1. 结构体在内存中是如何存储的
  • 2. container_of宏
  • 3. typeof
  • 4. (((type *)0)->member)
  • 5. const typeof(((type * )0) ->member)*__mptr = (ptr);
  • 6. offsetof(type, member))
  • 7. (type * )((char * )__mptr - offsetof(type, member))
  • 8. 举例

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

  1. int main() 
  2.  
  3.  Student stu; 
  4.  stu.id = 123456; 
  5.  strcpy(stu.name,"feizhufeifei"); 
  6.  stu.math = 90; 
  7.  stu.PE = 80; 
  8.  printf("Student:%p\r\n",&stu); 
  9.  printf("stu.ID:%p\r\n",&stu.ID); 
  10.  printf("stu.name:%p\r\n",&stu.name); 
  11.  printf("stu.math:%p\r\n",&stu.math); 
  12.  return 0; 

打印结果如下:

  1. //结构体的地址 
  2. Student:0xffffcbb0 
  3. //结构体第一个成员的地址 
  4. stu.ID:0xffffcbb0  //偏移地址 +0 
  5. stu.name:0xffffcbb4//偏移地址 +4 
  6. 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 。

Linux内核中Container_Of宏的详细解释

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

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

2. container_of宏

  1. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER) 
  2. #define container_of(ptr, type, member) ({          \ 
  3.         const typeof(((type *)0)->member)*__mptr = (ptr);    \ 
  4.     (type *)((char *)__mptr - offsetof(type, member)); }) 

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

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

3. typeof

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

typeof

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

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

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

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

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

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

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

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

6. offsetof(type, member))

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

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

  1. typedef unsigned int size_t; 

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

  1. typedef unsigned long size_t; 

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

  1. 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. 举例

  1. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 
  2. #define container_of(ptr, type, member) ({ \ 
  3.         const typeof( ((type *)0)->member ) *__mptr = (ptr); \ 
  4.         (type *)( (char *)__mptr - offsetof(type,member) );}) 
  5.          
  6. typedef struct student 
  7.  int id; 
  8.  char name[30]; 
  9.  int math; 
  10. }Student; 
  11.  
  12. int main() 
  13.     Student stu; 
  14.         Student *sptr = NULL
  15.   stu.id = 123456; 
  16.   strcpy(stu.name,"zhongyi"); 
  17.   stu.math = 90; 
  18.         sptr = container_of(&stu.id,Student,id); 
  19.         printf("sptr=%p\n",sptr); 
  20.         sptr = container_of(&stu.name,Student,name); 
  21.         printf("sptr=%p\n",sptr); 
  22.         sptr = container_of(&stu.math,Student,id); 
  23.         printf("sptr=%p\n",sptr); 
  24.         return 0;  

运行结果如下:

  1. sptr=0xffffcb90 
  2. sptr=0xffffcb90 
  3. sptr=0xffffcbb4 

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

  1. int main() 
  2.     Student stu; 
  3.         Student *sptr = NULL
  4.   stu.id = 123456; 
  5.   strcpy(stu.name,"zhongyi"); 
  6.   stu.math = 90; 
  7.   //展开替换 
  8.         sptr = ({ const unsigned char  *__mptr = (&stu.id); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->id) );}); 
  9.         printf("sptr=%p\n",sptr); 
  10.         //展开替换 
  11.         sptr = ({ const unsigned char  *__mptr = (&stu.name); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->name) );}); 
  12.         printf("sptr=%p\n",sptr); 
  13.         //展开替换 
  14.         sptr = ({ const unsigned int *__mptr = (&stu.math); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->math) );}); 
  15.         printf("sptr=%p\n",sptr); 
  16.         return 0;  

原文链接:https://mp.weixin.qq.com/s/hF7mP_J4BySYw0HCgk27IA

延伸 · 阅读

精彩推荐
  • LinuxLinux上设置用户通过SFTP访问目录的权限的方法

    Linux上设置用户通过SFTP访问目录的权限的方法

    这篇文章主要介绍了Linux上设置用户通过SFTP访问目录的权限的方法,SFTP可以理解为使用SSH协议进行FTP传输的协议,因而同时要对OpenSSH进行相关设置,需要的朋...

    OSChina10022019-06-19
  • LinuxLinux常用的日志文件和常用命令

    Linux常用的日志文件和常用命令

    成功地管理任何系统的关键之一,是要知道系统中正在发生什么事。 Linux 中提供了异常日志,并且日志的细节是可配置的。Linux 日志都以明文形式存储,所...

    Linux教程网2632020-04-18
  • LinuxLinux中环境变量配置的步骤详解

    Linux中环境变量配置的步骤详解

    Linux中环境变量包括系统级和用户级,系统级的环境变量是每个登录到系统的用户都要读取的系统变量,而用户级的环境变量则是该用户使用系统时加载的...

    Myths7882022-02-10
  • LinuxLinux lnmp下无法使用mail发邮件的两种解决方法

    Linux lnmp下无法使用mail发邮件的两种解决方法

    在配置了lnmp环境后,出现了mail函数不能发送邮件的问题,其实有两种方法,一是使用sendmail组件,而是使用postfix。下面为大家一一介绍下 ...

    Linux之家4042019-09-17
  • Linux确保Linux系统安全的前提条件 漏洞防护

    确保Linux系统安全的前提条件 漏洞防护

    Linux 作为开放式的操作系统受到很多程序员的喜爱,很多高级程序员都喜欢编写Linux操作系统的相关软件。这使得Linux操作系统有着丰富的软件支持,还有无...

    Linux之家2642020-04-11
  • Linuxlinux中rmdir命令使用详解(删除空目录)

    linux中rmdir命令使用详解(删除空目录)

    今天学习一下linux中命令: rmdir命令。rmdir是常用的命令,该命令的功能是删除空目录,一个目录被删除之前必须是空的 ...

    linux命令大全5372019-11-19
  • Linux详解Linux系统下PXE服务器的部署过程

    详解Linux系统下PXE服务器的部署过程

    这篇文章主要介绍了Linux系统下PXE服务器的部署过程,包括对PXE的API架构作了一个基本的简介,需要的朋友可以参考下...

    运维之道9812019-07-04
  • Linux手把手教您在 Linux 上使用 GPG 加解密文件

    手把手教您在 Linux 上使用 GPG 加解密文件

    在本教程中,我将告诉你如何用 GPG 加密和解密文件。这是一个简单的教程,你可以在你的 Linux 系统上尝试所有的练习。这将帮助你练习 GPG 命令,并在你...

    Linux中国6962021-12-15