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值当作加法运算中的一个加数来处理。形象的说,就是编译器只是摘掉了你房间的门牌号拿来作计算,并没有开门去取放在屋子里的任何东西。它在做完加法后就走人了,屋子里的东西是完整无缺的。而之所以编译器没有进屋子取东西,是因为有“&”的存在,编译器看到有“&”,就会明白我只需要拿到地址就可以了。

下面通过一个简单的例子来说明:

打印结果如下:

根据打印结果可以看到:pst->j与&(pst->j)效果是不一样的▶pst->j 没有“&”,会访问变量中的内容,打印结果为成员变量中的内容▶&(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 内核第一宏相关推荐

  1. Linux内核第一宏:container_of

    static void device_release(struct device *dev) {struct device *rd = to_device(dev);devm_kfree(dev, r ...

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

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

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

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

  4. 转载 linux内核 asmlinkage宏

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

  5. linux内核第一个函数,通过内核源码看函数调用之前世今生 - 极光 - CSDN博客

    通过内核源码看函数调用之前世今生 作者:杨小华 栈(Stack):一个有序的积累或堆积 韦氏词典 对每一位孜孜不倦的程序员来说,栈已深深的烙在其脑海中,甚至已经发生变异.栈可以用来传递函数参数.存储局 ...

  6. LINUX内核第一霸

    近日有同行十万火急找到我,说遇到个极其古怪的问题,请求救援.我说什么问题,他说是一个诡异的编译错误. 我心中暗笑,编译错误也需要找老雷么.在软件世界的诸般错误中,编译错误按说是最简单的啊. 我说,发个 ...

  7. linux内核current宏介绍

    1.概述 本文主要介绍linux current宏在arm和arm64上的实现 内核版本:Linux 5.3 2.current在arm和arm64上的实现 在linux 内核中,有一个current ...

  8. 【Linux 内核】宏内核与微内核架构 ( 操作系统需要满足的要素 | 宏内核 | 微内核 | Linux 内核动态加载机制 )

    文章目录 一.操作系统需要满足的要素 二.宏内核 三.微内核 四.Linux 内核动态加载机制 一.操作系统需要满足的要素 电脑上运行的 操作系统 , 是一个 软件 ; 设备管理 : 操作系统需要 为 ...

  9. linux内核系统调用宏SYSCALL_DEFINE

    注:源码来自内核版本3.13 在Linux的系统中,系统调用在内核的入口函数都是以 sys_xxx 命名的(如:sys_read) ,但是如果在内核源码去搜索相关函数时,很遗憾,搜索不到 sys_xx ...

  10. linux 内核 THIS_MODULE宏定义详解

    结构体struct module在内核中代表一个内核模块,通过insmod(实际执行init_module系统调用)把自己编写的内核模块插入内核时,模块便与一个 struct module结构体相关联 ...

最新文章

  1. vue post请求后台django接口Forbidden (CSRF token missing or incorrect.)
  2. SAP制造业解决方案
  3. 你的行为合理吗?看看社会心理学给我们的启示。
  4. php java c_当PHP、Java、C、C++ 这几种编程语言变成汽车是什么样的场景?
  5. vue项目引入CNZZ数据专家(方法汇总篇)
  6. java 修改最大nio连接数_携程基于Quasar协程的NIO实践
  7. linux怎么安装scp服务,linux下ssh安装与scp命令使用详解
  8. AVL平衡树的插入例程
  9. python基础之列表、元组
  10. 计算机仿真软件的论文,【计算机仿真论文】计算机仿真软件模拟物流系统研究(共5366字)...
  11. 自制全铝CNC雕刻机全过程(完工篇)
  12. 【Android】dp-sp-屏幕像素密度
  13. 【兴趣书签】让人深陷其中的科幻小说
  14. 武汉新时标文化传媒有限公司短视频创作者实现突围?
  15. 【转载】FPGA功耗的那些事儿
  16. C#编程_实现简易的任务管理器
  17. 微信小程序选项卡数组列表单项选择切换效果
  18. c语言 scanf( 停止,一个c程序,一运行到Scanf就程序停止
  19. Parametric Contrastive Learning:长尾问题中的对比学习
  20. 没有那个文件或目录 误无法以读模式打开文件 No such file or directory

热门文章

  1. 浅谈FMA与SMA(test)
  2. java拷贝字符文件
  3. 找到多个与名为“Login”的控制器匹配的类型
  4. HDU1176:免费馅饼(dp,数字三角形的应用)
  5. NHibernate代码解析 - SqlCommand - SqlString 参数名后绑定
  6. python 框架是什么意思_Python框架有哪些?区别是什么?
  7. 基于etcd+confd通过nginx对docker服务混合注册发现详解
  8. css 3 制作水波状进度条
  9. 分布式文件系统(FastDFS+Tengine+fastdfs-nginx-module)
  10. 一份招聘需求的分析 (转载)