内核第一宏

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. 使用node.js中遇到的一些小bug

    1.BUG Cannot set headers after they are sent to the client 解决:即发出一次请求得到两次或以上的回应时会出现此警告,此时注意查看再在一些条件下 ...

  2. iOS UITableView优化

    一.Cell 复用 在可见的页面会重复绘制页面,每次刷新显示都会去创建新的 Cell,非常耗费性能.  解决方案:创建一个静态变量 reuseID,防止重复创建(提高性能),使用系统的缓存池功能. s ...

  3. JS函数详解

    什么是函数呢? 对于JS来说,我们可以把函数理解为任意一段代码放在一个盒子里,在我们想要让这段代码执行的时候,直接执行这个盒子里的代码就行.专业一点来讲:js函数就是由事件驱动的可执行课重复只用的代码 ...

  4. 记一次Task抛异常,调用线程处理而引发的一些随想

    记一次Task抛异常,调用线程处理而引发的一些随想 多线程调用,任务线程抛出异常如何在另一个线程(调用线程)中捕获并进行处理的问题. 1.任务线程在任务线程执行语句上抛出异常. 例如: private ...

  5. java 第六周上机练习 04.09

    1.编写一个简单程序,要求数组长度为5,静态赋值10,20,30,40,50,在控制台输出该数组的值. int [] arr= {10,20,30,40,50}; for(int i=0;i<a ...

  6. WDK驱动开发点滴

    老程序员做新方向,老树发新芽,作为菜鸟的我,写点心得,用以记录并与同行交流 1对一些概念的理解: KMDF与UMDF.两者的框架,及使用VS生成的初始代码基本相同,只有所包含的头文件不同,链接的系统库 ...

  7. 深入解读ES6系列(四)

    来自老曾es6的前言: 哈喽小伙伴们,爱说'废'话的Z又回来了,欢迎来到Super IT曾的博客时间,上一节说了字符串,面向对象以及json的知识,这一节我们继续我们知识的海洋,一起奋斗不秃头!不足的 ...

  8. vue 刮刮乐功能实现

    <template> <!--游玩区域--> <div class="panel"> <canvas id="canvas&qu ...

  9. 浅谈RPC与Http

    什么是RPC,RPC原理是什么? 什么是RPC? RPC(Remote Procedure Call)远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.比如两个 ...

  10. 家庭版记账本app进度之对于按钮的点击事件以及线性布局以及(alertdialog)等相关内容的应用测试

    通过线性布局,制作出连个按钮还有文本输入框以及嘴上放的标题文本进行信息的相关显示,完后最后的信息的输入,之后在屏幕的的下方进行显示 当点击第一个按钮的时候,在下方就会简单的出现你自己刚刚输入的相关信息 ...