从微观角度来看Linux内核设计

描述

◆◆从微观角度来看Linux内核设计◆◆

余生皆欢喜

最近总结出来学习内核有两个大的角度,一种就是从宏观角度来看,总的来说就是顺着抽象,管理,操作来看,这种角度更多的是内核中应用层面的内容,用来理解内核中是怎么运转起来的。第二种就是从内核的最细节部分出发,深入到一个个具体的宏,看看内核设计者在细节部分有着怎么样的巧妙之处,这样也有助于我们夯实C语言基础,也可以学习到GNU C的用法。

最近学习了如下的GNU C的内容:

指定初始化

语句表达式

typeof关键字

内核第一宏

我们来看看这些内容是怎么设计的,GNU C就是打辅助的,专门为了OS而存在,(为什么全世界不统一使用GNU C呢?)它带来了太多的方便,换句话说,它帮助内核设计人员解决了很多内核设计者在设计内核时所遇到的问题,我这样认为,GNU C中每一条功能,就是内核设计者在实际设计中遇到的问题。

这里再次分析总结gitbook中的两个宏,一个是max/min宏,一个是内核第一宏container_of。

max/min宏

内核中的样子:

代码

这里的max宏可以让我们学会语句表达式,typeof关键字;基础方面可以巩固运算符优先级。

这个宏是怎么得到的呢?

我们来写一个宏,用来比较两个变量的大小,我一定会这么写:

代码

那么我们来比较一下4!=4和2!=3,结果是错误的,原因是运算符优先级出了问题。那么我们来解决,使用括号是最简单的方法:

代码

我们来运行一条语句:printf("max = %d\n",3 + MAX(4,5));,结果是7,这里是因为+的运算优先级大于>了,换句话说,是因为外部的语句,影响到了宏,那么我就把自己隔离起来:

代码

再来运行一下printf("max=%d\n",MAX(2++,3++));,输出的会是4,但我们只想要比较2和3的值,这里是因为自增自减运算符导致的问题,那么怎么解决呢?和交换两个数字的想法一样,通过一个中转值来存放,就可以隔离影响了

代码

这里就有一些内核代码中的味道了,注意一个细节,这里的第四行没有括号了,为什么?这里就是因为语句表达式了,不存在上面的影响了。这里我们回顾一下代码,再看看目前这个宏的第二三行,是int,也就是我们这个宏只能比较int类型的变量,而在内核中需要比较大小的变量有很多,那么我们来提高一下:

代码

这个宏就可以用来比较任意类型的变量了,再来看一下代码,我们需要替换的变量有type,x,y三个,如果有了typeof关键字,我们还可以减少一个:

代码

接着来,如果我们使用了一次宏,是MAX(i,j),其中i是int类型,j是float类型,这样比较是可以的,但是在内核的设计过程之中,很有可能有些地方会出现问题,所以还需要改造:

代码

这就是究极形态了,我们添加了第四行的代码,来看&_min1,它的意思是取_min1的地址,而&_min2的意思是取_min2的地址,我们也知道,这两个地址肯定不可能是一样的,那为什么还要这样写呢?这里就很巧妙了,当两个变量的类型不同时,对应的地址,也就是指针类型也不相同,比如一个是int类型,一个是char类型,那么指向他们的指针就是int *和char *,这两个指针在比较的时候,就比较的是类型了。如果比较的类型不一样,gcc会警告的。

我们来看这一系列改进,我相信内核设计人员也想把代码写成# define MAX(x,y) x > y? x : y的样子,但是现实是残酷的,我们为了代码的健壮性,就必须这样一步一步来改进,所以,内核代码看起来很复杂,又很巧妙,是因为我们直接看到的是究极形态的代码,它是向现实妥协了多次以后的产物,也就是健壮性+GNU C。但是,内核设计者的初衷,或者说最初的想法和我们都是一样的。

有内核源码在旁边,巩固基础知识就不用像以前的学习模式了,可以在源码中代入学习,增添一份趣味性,并且可以很快理解。

在以后处理因为运算符而导致的问题的时候,使用括号是最方便的,内核就这么干了。

在写程序的时候,要巧用中转变量,虽然只是简单的存入另一个变量之中,但是代码的健壮性提高了很多。

两个地址在进行比较的时候,我们可以得知这两个指针类型是否一致。

内核第一宏

gitchat中把container_of宏叫做内核第一宏,我也很喜欢这个称号,因为学内核两个月里见这个宏的次数太多了。在陈老师讲list.h的时候,就学习过这个宏,但是并没有完完全全地剖析开。

高能预警:

代码

这个宏的作用我们已经很清楚了,根据结构体中某一成员的地址,就可以获得这个结构体的首地址,再说的明白一点,假如你是内核设计人员,前面也说道了,我们已经对数据进行了多次封装,我们一定会遇到这种情况:传给某个函数的参数是某个结构体成员变量,但是我们在这个函数中还想使用这个结构体的其它成员变量,这个时候就需要想办法,于是才有了我们现在看到的这个内核第一宏。

它的三个参数是:

ptr:此结构体内成员member的地址

type:此结构体类型

member:此结构体内的成员

我们直接看代码,这个宏的最后的值,就是最后一条语句,(type *)( (char *)__mptr - offsetof(type,member) );}),这条语句也是这个宏的中心思想拿结构体成员的地址减去此成员的偏移,这里也体现了指针做减法是很有意义的。成员的地址好说,我们直接传进来了,偏移是通过offsetof来实现的,来看看这个offsetof:将0强制类型转换成这个结构体的指针类型,然后访问这个成员,加上&得到它的偏移,返回。这里要注意一下,那就是为什么只通过TYPE和MEMBER就可以得到偏移,我一开始认为的是内核中这个类型的结构体多了,到底用的是哪一个结构体来得到的,最后发现,并没有关系,因为我们需要的是字节数,与实际这个字段赋什么样的值并没有关系,因为所有这个类型的结构体中,各成员的字节大小是一样的。

再来看(char *)__mptr,这个通过第四行代码可以很容易得出它是成员的地址,为什么要强制转换成char *呢?转换成int *不行吗?这里又可以学习一下C指针的基础知识,通过代码可以很容易知道有什么区别:

代码

打印出来的值,p(int *)类型,增加了4字节,而q(char *)增加了1字节,回到宏中,我们的偏移是按照字节来算的,所以不能使用(int *),必须使用(char *)。在最后,再次强制类型转换成指向这个结构体的指针类型。

回过头来看第四行代码,const typeof( ((type *)0)->member ) *__mptr = (ptr);,这里和max宏之中类似,使用了中转变量来存放,这里为什么要使用中转变量?max宏中是为了防止自增自减的影响(当然只是原因之一了),但我们在使用的时候总不至于发过来成员的地址再加一个++运算符吧。我们可以从const的用法来思考,const int * p //p可变,p指向的内容不可变,所以,使用了const,我们就可以保证ptr指向的内容在这里只是可读的,这也许就是为什么使用中转变量的原因,为了防止我们通过指针改变了原有的成员的值,毕竟指针虽然强大,但也是很危险的,所以,这里的中转要配合const来使用。既然是中转,那么类型就必须要求一致了,所以我们要得到和这个成员一致的类型,就通过typeof来得到了,将0强制类型转换成这个这个结构体的指针类型,然后访问这个变量,(注意仔细看代码,这里的代码和offsetof非常类似)这里没有使用&,所以只是访问到变量了,没有得到偏移。另外根据const的用法,第四行的代码也可以写成typeof( ((type *)0)->member ) const *__mptr = (ptr);也就是把const放到后面。

我们再来注意一个细节,就是offsetof里的size_t,这个是什么,这里在敲代码的过程中偶然学到一个小技巧,就是这个size_t绝对是封装,就是C语言中那几种变量类型,我们可以typedef int size_t;然后运行,gcc就会报错,并且会给你显示:以前已经定义过:typedef __SIZE_TYPE__ size_t,并且会指定这个值在哪个文件,我们就可以知道它的真面目了。换句话说,gcc这么强大,我们当然可以把它当做一个学习工具来使用。

另外还可以通过sublime,可以很快找到它的真面目(3.10版本):

代码

最后,为了更深入理解这些知识的使用方法,还是需要自己动手来敲代码的,尤其是内核第一宏,将代码写到用户态下,然后疯狂改造,这样才会真正理解这个宏。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分