linux内核第一宏,Linux内核第一宏

Linux内核第一宏

Linux内核第一宏

作者简介:

贺东升,西安邮电大学2019级陈莉君教授研究生,初学内核的小白,热爱Linux内核,正在努力学习内核。

内核第一宏

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

整理分析的思路

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

de1aefddfd717c7f20a256d0963f5340.png

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

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

2c4fb3193a8226c3d1e3eeeba5f05b6e.png

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

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

7308879fcf5faecad86517f7c6b664c3.png

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

c36fcb3ed40421c03fe8c6d779ed3895.png

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

87e2568cb1175188e453477ed618c9a4.png

打印结果如下:

4dd69de0a5e478827cb622836cb0ea90.png

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

宏container_of

19df065c354d72ce4f1e6d68110620b8.png

在进入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编译器的语法扩展 ●({ })与逗号表达式类似,结果为最后一个语句的值举个例子:

3b53fa130e76cddc2e94919ec2584232.png

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

7a40def4b84219de75e0e7f190b9a1a1.png

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

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

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

ad4ea3a43c3915edc690b61e9f03d100.png

我们来看(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内核第一宏相关教程

Linux环境redis的安装和部署和断开连接和RedisDesktopManager使

Linux环境redis的安装和部署和断开连接和RedisDesktopManager使用 一、首先需要在Linux环境中下载安装redis 1、配置c语言环境 : yum install gcc-c++ //已经安装则跳过 ,若提示yum、install命令找不到,则下载对应命令即可(yum命令找不到:1)安装了build-

在linux系统上安装jdk和jmeter,以及如何在linux上使用jmeter

在linux系统上安装jdk和jmeter,以及如何在linux上使用jmeter 记录在服务器上利用jmeter进行压测的准备过程,我们需要在系统上安装jdk与jmeter。 我使用的jdk1.8,系统是centos 7,jmeter是 5.3 概述 安装jdk 安装jmeter 使用jmter 安装jdk jdk可以在官网自行

linux下怎么打开mysql数据库

linux下打开mysql数据库的方法:首先执行【service mysqld start】命令启动mysql服务;然后执行【mysql -u user -p passwd】命令连接数据库即可。 对于mysql服务的启动,我们通常使用命令service mysqld start,没问题的话就能启动mysql服务了。 (推荐教程

Cortex-M内核中的DWT计数器

Cortex-M内核中的DWT计数器 DWT跟踪组件 Cortex-M3 权威指南: 16.2 TRACE COMPONENTS: DWT The rest of the DWT counters are typically used for profiling the application codes. They can be programmed to emit events (in the form of trace packets)

9.配置静态ip以及ssh免密登陆-linux

9.配置静态ip以及ssh免密登陆-linux 1 配置静态ip 1 获取网关 route -n 2 修改配置文件 vim /etc/sysconfig/network-scripts/ifcfg-ens33 #修改该值为staticBOOTPROTO=static#添加如下的内容:ip,网关,域名解析服务器IPADDR=192.168.134.134NETMASK=255.255

linux内核协议栈 TCP选项之MSS

linux内核协议栈 TCP选项之MSS 目录 1MSS概述 2客户端三次握手 2.1 发送SYN段MSS选项值 2.1.1 tcp_advertise_mss() 2.1.2 tp-advmss的初始化 2.2 接收SYN+ACK段 3服务器端三次握手 3.1 接收SYN段 3.2 接收 ACK 报文 3.2.1初始化 advmss 3.3 发送SYN+ACK段 4

C语言系统学习日记01

C语言系统学习日记01 1、学习截图 第一步学习的是C++的输出打印语法 第二学习变量的声明要求以及常用的整型,浮点型,布尔型三种类型 第三学习C++的操作符,关系操作符,逻辑操作符和算术运算符(自增自减运算符)以及 运算符的优先级的问题 运算符 关系操作

SpringBoot简介以及创建第一个Demo

SpringBoot简介以及创建第一个Demo SpringBoot的特性 1 能快速创建感于spring的应用程序 2 能够直接使用java main方法启动内置的Tomcat,Jetty服务器运行SpringBoot程序,不需要部署war包; 3 提供预订的starter POM简化Maven配置,让Maven的配置变得简单; 4

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值