内核第一宏

list_entry()有着内核第一宏的美称,它被设计用来通过结构体成员的指针来返回结构体的指针。现在就让我们通过一步步的分析,来揭开它的神秘面纱,感受内核第一宏设计的精妙之处。

整理分析的思路

list_entry()在内核源代码/include/linux目录下的list.h中被定义,如下:

在list_entry的定义中,我们看到出现了另外一个宏container_of。而list_entry这个宏正是通过container_of去实现的。所以我们要先进入container _of,来看看它做了什么。

container_of定义在/include/linux/kernel.h中,定义如下:

我们发现,在container_of的定义中,又出现一个新的宏offsetof。所以,在开始分析container_of之前,有必要先来搞清楚offsetof。

offsetof定义在/include/linux/stddef.h中,定义如下:

我们可以看到,在offsetof的定义中,已经没有再引入新的宏,所以,我们就以offsetof为突破口,进行分析。

正式分析

宏offsetof

单词offset的意思是偏移量,所以我们可以顾名思义一下,宏offsetof的作用可能和偏移量有关。那么,它要求谁的偏移量呢?

offsetof用于计算TYPE结构体中成员MEMBER的偏移量。

从offsetof的定义中可以看到,在&((TYPE *)0)->MEMBER中,有一个明显的强制类型转换((TYPE *)0)。在C语言中,强制类型转换有两种语法:

 1.(TYPE)var_name; //变量名形式,如(int)i; 2.(TYPE)varlue;   //值形式,如(type*)0;

定义中使用了第二种语法,将0值强制类型转换成一个TYPE结构体的指针。通过这种强制类型转换后,TYPE结构体的地址变成了0,那么为什么要做这种转换?它的作用是什么?

其实这么做的目的只有一个,就是为了更容易拿到成员的偏移量。我们知道,结构体类型在预编译的时候,为了使CPU能够对数据快速访问和有效节省存储空间,有一个内存对齐的问题,就是结构体的每个成员在内存中的存储都要按照一定的偏移量来存储。所以会由于成员类型的不同,导致每个成员的偏移量也不尽相同,所以我们就不能一劳永逸的来给所有成员设定一个固定的偏移值。那我们想要拿到一个成员的偏移量怎么办呢?我们就把这个重任交给了编译器。我们可以指挥编译器,让它“交出”成员的偏移量。有一点我们必须清楚,编译器在预编译的时候,对每个成员的偏移量是心知肚明的,所以编译器如果想要知道某个成员的地址,它只需要用结构体的地址+成员的偏移量就可以得到该成员的地址。

举个简单的例子:以上面的图为例,如果上面结构体的地址p=1000,,成员C的偏移量(offset)是4,那成员C的地址pc就是1000+4=1004;

这个时候得到的1004是成员C的地址pc,但是我们想要的不是它的地址,而是它的偏移量,这个时候怎么办呢?最简单的办法就是,直接将结构体的地址变成0不就可以了吗?0加一个数就等于这个数本身,这样相加的结果正好就是成员的偏移量了。这就是为什么定义中要通过强制类型转换将结构体的地址变成0,举个例子:现在将结构体的地址p=0,成员C的偏移量(offset)还是4,0+4=4,得到的结果正好就是该成员的偏移量了。

所以我们让编译器执行&((TYPE *)0)->MEMBER这句话的时候,它做的就是这样一个事情,它先将type类型结构体的地址变成0,然后再去加上成员MEMBER的偏移量,0+偏移量=偏移量,所以最后得到的结果就是成员的偏移量了。内核的设计者们,正是通过这种巧妙的设计,来指挥编译器交出偏移量。

所以,当我们调用offsetof(TYPE, MEMBER)之后,就会得到成员MEMBER在TYPE结构体中的偏移量了

这里有一点值得思考的是:&((TYPE *)0)->MEMBER中,结构体的地址通过强制类型转换变成了0,我们知道0地址是留给操作系统来使用的,这里面的内容是不允许普通的程序来访问的。但是这里却将结构体地址变成了0,那直接使用0地址不会导致程序崩溃吗?

答案是程序是不会崩溃的,编译器在执行&((TYPE *)0)->MEMBER的时候,并没有真正去访问0地址中的内容,而只是将这个0值当作加法运算中的一个加数来处理。形象的说,就是编译器只是摘掉了你房间的门牌号拿来作计算,并没有开门去取放在屋子里的任何东西。它在做完加法后就走人了,屋子里的东西是完整无缺的。而之所以编译器没有进屋子取东西,是因为有“&”的存在,编译器看到有“&”,就会明白我只需要拿到地址就可以了。下面通过一个简单的例子来说明:

打印结果如下:

1.根据打印结果可以看到:pst->j与&(pst->j)效果是不一样的
  2.▶pst->j 没有“&”,会访问变量中的内容,打印结果为成员变量中的内容
  3.▶&(pst->j)有“&”,不会访问变量中的内容,只拿地址,打印结果为成员的地址

至此,offsetof的作用我们已经知道了。在container_of的定义中,使用了offsetof,也就是说,在container _of的实现中,它需要用到offsetof来得到结构体某个成员的偏移量,那container _of的作用是什么?它要偏移量有什么用呢?接下来就让我们一起进入container _of的世界吧。

宏container_of

在进入container _of的世界后,我们发现这里有两个“熟悉的陌生人”,分别是typeof和“({ })”。这两个小伙伴,我们在C语言中是见不到它们的,这是因为他们都只“生活”在GNU C编译器中。为了能让我们在认识container _of的旅程更加轻松,我们有必要花些时间来和typeof和“({ })”这两个杰出的小伙伴交个朋友,认识一下他们。

typeof

●typeof是GNU C编译器的特有关键字
        ●typeof只在编译期生效,用于得到变量的类型
举个例子:

  int i = 100;
  typeof(i) j = i; <=> int j = i; //这两个语句的作用是等价的,变量i的类型是int,typeof(i)就相当于拿到变量i的类型

({     }) 
    ●({ })是GNU C编译器的语法扩展
    ●({ })与逗号表达式类似,结果为最后一个语句的值
举个例子:

现在,我们已经认识了typeof和“({ })”两个小伙伴,这对我们认识container _of会有很大帮助。现在,我们可以来正式的分析container _of宏了。让我们再一次把container _of的定义搬到这里:

定义中使用了扩展语法“({ })”,前面已经说过,它的结果就是最后一个语句的值,既然这样,我们就可以直接来看最后一个语句。

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

这里面有一个指针__mptr,它在第二行中被定义,类型由typeof来获得。指针 __mptr和指针ptr的值是一样的,而ptr又是宏container _of的一个参数,它是指向type结构体中成员member的一个指针,所以 __mptr也指向type结构体中成员member。为了清晰的表示这种关系,我们用一个图来表示,它们的关系如下图:

我们来看(char *)__mptr - offsetof(type,member)这句话是什么意思。通过offsetof(type,member)可以得到成员member的偏移量,也就是上图中的offset,然后用  __mptr减去offset,得到一个地址,如上图所示P,而这个地址就是结构体的地址,这样就实现了通过成员找到结构体的起始地址。__mptr前面的char*是为了进行指针运算的,以实现逐字节相减。最后通过(type *)强制类型转换为指向结构体的指针。到这里,宏container_of就真相大白了。

这里有一点值得思考的是:既然__mptr = (ptr),那为什么不直接使用传入的参数ptr去减,而是看似“多此一举”的在第二行将ptr的值赋给 __mptr,然后用 __mptr去减呢?

答案是为了对传入的参数进行一次类型安全检查。宏是在编译的时候由预处理器来进行处理的。预处理器做的是单纯的文本替换,不会进行任何的类型检查,这就有可能导致我们在编写代码的时候,由于粗心大意而造成错误。举例来说,container _of(ptr, type, member)有三个参数,如果传入ptr的时候,我们由于粗心大意,将一个错误的ptr指针传入,发现程序可能会正常运行,但是结果是错误的。这个时候为了增加代码的安全性,为了能够有一点点的类型安全的检查,所以内核的设计者们在定义container _of的时候,在定义的第二行添加了一行用于类型安全检查的代码,它会在你传入错误的指针时,弹出一个警告,这个警告告诉我们,在这个地方存在着类型不兼容的情况,这样我们在运行之前就可以再次去检查一下参数,从而避免一次BUG。

结语
至此,我们已经清楚的知道了container_of的作用了。现在我们回到最初的出发点———list _entry(),也就明白了为什么它被称作内核第一宏了。

linux内核第一宏 container_of的更多相关文章

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

    4.1 typeof 关键字 ANSI C 定义了 sizeof 关键字,用来获取一个变量或数据类型在内存中所占的存储字节数.GNU C 扩展了一个关键字 typeof,用来获取一个变量或表达式的类型 ...

  2. linux内核中宏container_of是干什么的?

    Linux Kernel Version 4.14 1. container_of是干什么的? 已知一个结构体中某个成员的首指针,那么就可以通过宏container_of来获得此结构体的首指针 2 先 ...

  3. 转载 linux内核 asmlinkage宏

    转载http://blog.chinaunix.net/uid-7390305-id-2057287.html 看一下/usr/include/asm/linkage.h里面的定义:#define a ...

  4. linux内核第一二章总结

    1 Linux内核简介 1 Unix的历史 1.Unix演化版实现了任务管理.换页机制.TCP/IP等新的特性. 2.Unix的特点: Unix很简洁,仅仅提供几百个系统调用并且有一个非常明确的设计目 ...

  5. Linux 内核常见宏定义

    我们在阅读Linux内核是,常见到这些宏 __init, __initdata, __initfunc(), asmlinkage, ENTRY(), FASTCALL()等等.它们定义在 /incl ...

  6. Linux内核第一节

    存储程序计算机工作模型 存储程序计算机——冯诺依曼体系结构 IP:寄存器,总是指向内存的代码段.IP(16位) 32位(EIP) 64位(RIP). 内存:保存数据和指令. CPU:CPU从IP指向的 ...

  7. 由linux内核某个片段(container_of)引发的对于C语言的深入理解

    /usr/src/linux-source-3.8.0/drivers/gpu/drm/radeon 这个文件夹以下 去找到这个文件 mkregtable.c  打开,就能够看到了. #define ...

  8. linux内核中宏likely和unlikely到底做了些什么?

    1. 先看看它们长啥样吧!(它们有两种定义,第一种是使能了程序trace功能的宏定义,第二种是普通的宏定义,咱们分析普通宏定义吧) # define likely(x) __builtin_expec ...

  9. Linux 内核 MODULEDEVICETABLE 宏

    这个 pci_device_id 结构需要被输出到用户空间, 来允许热插拔和模块加载系统知道什 么模块使用什么硬件设备. 宏 MODULE_DEVICE_TABLE 完成这个. 例如: MODULE_ ...

随机推荐

  1. coding++:解决Not allowed to load local resource错误-SpringBoot配置虚拟路径

    1.在SpringBoot里上传图片后返回了绝对路径,发现本地读取的环节上面出现了错误(Not allowed to load local resource),一开始用的是直接本地路径. 但是在页面上 ...

  2. 实战级Stand-Alone Self-Attention in CV,快加入到你的trick包吧 | NeurIPS 2019

    论文提出stand-alone self-attention layer,并且构建了full attention model,验证了content-based的相互关系能够作为视觉模型特征提取的主要基 ...

  3. 实验十一 MySQLl备份与恢复1

    实验十一 MySQL备份与恢复 一.  实验内容: 1. 使用SQL语句导入和导出表数据 2. 使用客户端工具备份还原数据库 3. 使用日志文件恢复数据库 二.  实验项目:学生成绩数据库 创建用于学 ...

  4. A - 你能数的清吗 51Nod - 1770(找规律)

    A - 你能数的清吗 51Nod - 1770(找规律) 演演是个厉害的数学家,他最近又迷上了数字谜.... 他很好奇 xxx...xxx(n个x)*y 的答案中 有多少个z,x,y,z均为位数只有一 ...

  5. 使用xlsxwriter模块和xlrd模块操作Excel表格

    1.xlsxwriter模块介绍:主要用来生成excel表格,插入数据.插入图标等表格操作 如下代码:目的是往demo01.xlsx插入数据和图片 # 导入xlsxwriter模块:主要用来修改表格的 ...

  6. 1048 Find Coins (25分)

    Eva loves to collect coins from all over the universe, including some other planets like Mars. One d ...

  7. vs整合MySQL和QT

    23:37:23 2019-08-12 尝试用vs写一个程序整合MySQL和QT 参考资料:https://blog.csdn.net/qq_35987486/article/details/8406 ...

  8. 使用 Spring data redis 结合 Spring cache 缓存数据配置

    使用 JavaConfig 方式配置 依赖 jar 包: jedis.spring-data-redis 首先需要进行 Redis 相关配置 @Configuration public class R ...

  9. CSS制作小旗子与小箭头

    CSS制作小旗子与小箭头 效果图如下: 小旗子效果图 小箭头效果图 小旗子效果 以下是具体实现代码: <div class="container"> <div c ...

  10. 给listview添加长时间点击事件(完成删除操作)

    出现的问题是:当长时间点击listview的时候,触发长时间点击事件的同时也会触发点击事件,处理的办法是在长时间点击事件中加上return true:这样就可以很好的解决了. 给listview天机长 ...