上一节拒绝造轮子!如何移植并使用Linux内核的通用链表(附完整代码实现)我们在分析Linux内核链表的时候注意到内核在求解结构体偏移的时候巧妙的使用了container_of宏定义,今天我们来详细剖析下内核到底是如何求解结构体成员变量的地址的。

@

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

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的地址。

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

typeof

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

typeof

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

(((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;
}

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

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

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

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地址的一个偏移值。

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

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

举例

#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,"feizhufeifei");
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,"feizhufeifei");
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那些事,领取秋招笔试面试大礼包(华为小米等大厂面经,嵌入式知识点总结,笔试题目,简历模版等)和2000G学习资料。公众号主要分享Linux驱动开发,数据结构与算法,计算机基础,C/C++等相关知识,有任何问题均可以加我微信,欢迎骚扰!

Linux内核中container_of宏的详细解释的更多相关文章

  1. 内核中container_of宏的详细分析【转】

    转自:http://blog.chinaunix.net/uid-30254565-id-5637597.html 内核中container_of宏的详细分析 16年2月28日09:00:37 内核中 ...

  2. 剖析linux内核中的宏---------container_of

    #define container_of(ptr, type, member) ({ \ const typeof(((type *)0)->member) * __mptr = (ptr); ...

  3. linux内核中的宏ffs(x)

    linux内核中ffs(x)宏是平台相关的宏,在arm平台,该宏定义在 arch/arm/include/asm/bitops.h #define ffs(x) ({ unsigned long __ ...

  4. Linux内核中的宏:__init and __exit

    ZZ FROM: http://blog.csdn.net/musein/article/details/742609 ======================================== ...

  5. 剖析linux内核中的宏-----------offsetof

    offsetof用于计算TYPE结构体中MEMBER成员的偏移位置. #ifndef offsetof#define offsetof(TYPE, MEMBER) ((size_t) &((T ...

  6. Linux内核中container_of函数详解

    http://www.linuxidc.com/Linux/2016-08/134481.htm

  7. linux内核中链表代码分析---list.h头文件分析(一)【转】

    转自:http://blog.chinaunix.net/uid-30254565-id-5637596.html linux内核中链表代码分析---list.h头文件分析(一) 16年2月27日17 ...

  8. (十)Linux内核中的常用宏container_of

    Container_of在Linux内核中是一个常用的宏,用于从包含在某个结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址. Containe ...

  9. Linux内核中的常用宏container_of

    Container_of在Linux内核中是一个常用的宏,用于从包含在某个结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址. Containe ...

随机推荐

  1. 面试阿里,字节跳动90%会被问到的Java异常面试题集,史上最全系列!

    Java异常架构与异常关键字 Java异常简介 Java异常是Java提供的一种识别及响应错误的一致性机制. Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程 ...

  2. Fruity Granulizer合成器功能简介

    本章节采用图文结合的方式给大家介绍电音编曲软件-FL Studio的插件Fruity Granulizer合成器,感兴趣的朋友可以一起沟通交流. Fruity Granulizer合成器是一个使用了粒 ...

  3. Markdown特点

    前言 以前经常在 gitHub 中看到 .md 格式的文件,一直没有注意,也不明白为什么文本文档的后缀不是 .txt ,后来无意中看到了 Markdown,看到了用这个东西写得一些web界面等特别的规 ...

  4. C3PO数据库连接池

    1 <?xml version="1.0" encoding="UTF-8"?> 2 3 -<c3p0-config> 4 5 6 -& ...

  5. 关于redis在cluster模式化下的 分布式锁的探索

    背景 redis作为一个内存数据库,在分布式的服务的大环境下,占的比重越来越大啦,下面我们和大家一起探讨一下如何使用redis实现一个分布式锁  说明       一个分布式锁至少要满足下面几个条件 ...

  6. Java-Netty前菜-NIO

    NIO NIO主要有三个核心部分组成: buffer缓冲区 Channel管道 Selector选择器 在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道配合使用来处 ...

  7. JQuery案例:购物车加减

    购物车加减 <head> <meta charset="UTF-8"> <title>加减购物车</title> <style ...

  8. Spring Framework 5.0简述

    从Spring框架5.0开始,Spring需要JDK 8+ (Java SE 8+),并且已经为JDK 9提供了现成的支持. Spring框架还支持依赖注入(JSR 330)和通用注释(JSR 250 ...

  9. 在 Spring Boot 配置 Kafka 安全认证

    spring: kafka: bootstrap-servers: IP:端口 listener: missing-topics-fatal: false properties: sasl: mech ...

  10. 20200513_安装windows sql server 2012 _ ws功能 NetFx3时出错,错误代码:-2146498298

    这是没有安装.net Framework 3.5造成的 1. 下载个.net Framework 3.5, 放到任意目录下, 比如C: 2. 打开添加windows 功能 3. 直接下一步: 4. 勾 ...