【Linux内核】从小小的宏定义窥探Linux内核的精妙设计

描述

 Linux操作系统,可以说它就是程序猿的代码天堂;这不仅仅因为它是开源的,更多的是因为它的诞生,是由世界上无数的代码天才共同缔造而来;跑在它上面的Linux内核,经受了世界上各式各样的服务器压力测试,始终保持着高效、稳定、安全的特性,一如既往地服务全人类。甚至可以说Linux操作系统造福了人类,很难想象,当Linux操作系统消失了,这个世界会变得怎么样?

​ 作为Linux操作系统的忠实粉丝,笔者自大学时期就开始研究和使用Linux操作系统,出来工作了好几年,几乎每天都要跟Linux系统打交道,甚至毫不夸张的是,白天不在Linux系统命令行下敲几行命令,晚上都会失眠。

​ 学习和使用了Linux系统这么些年,一直想找个机会,对Linux的知识做一番梳理,无奈之前碍于各种时间因素和自我的惰性,迟迟未有实质性的进展。最近才开始狠狠地下定决心,必须迈出扎实的一步,争取做出更多的分享,充实自我的同时,也给同行带来更多的视野和思路,何乐而不为呢?


​ 本文打算从一个很小的代码设计,试图从中窥探一下Linux内核代码的精妙设计。它的名字就叫 max宏定义,请跟随笔者的思路一步步解开它神秘的面纱。

​ 先来一个它的全貌:

#define max(a, b) ({\
typeof(a) _max1 = (a);\
typeof(b) _max2 = (b);\
(void)(&_max1 == &_max2);\
_max1 > _max2 ? _max1 : _max2; })

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AQSFb6aB-1661923667090)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 我们先不一下子就把这段代码剖析彻底,换个思维,假设我们是Linux内核的设计者,要解决比较2个数的大小,代码应该怎么样入手。我想很多C语言工作者,甚至是初学C语言的码农也可以写出这样的如下代码:

#define max(a, b) a > b ? a : b

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jvjzDDsR-1661923667093)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 初看这个宏定义,似乎没有问题;细细一看,用个测试案例一测试就发现端倪了:

/* 假设有如下的调用代码 */
{    
    printf("result = %d\n", max(9!=9, 0==0));

    /* 宏定义展开后是  9!=9 > 0==0 ? 9!=9 : 0==0*/
    /* 输出结果是 0*/
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-59Bp6ggT-1661923667094)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 很明显正确答案应该是输出1,细心者就很快发现,给a和b加上括号试试看:

#define max(a, b) (a) > (b) ? (a) : (b)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7e0TQdq-1661923667097)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 调用代码测试下:

/* 假设有如下的调用代码 */
{
    printf("result = %d\n", max(9!=9, 0==0));

    /* 宏定义展开后是  (9!=9) > (0==0) ?(9!=9):(0==0)*/
    /* 输出正确结果 1*/

    printf("result = %d\n", 9 + max(9!=9, 0==0));

    /* 宏定义展开后是  9 + (9!=9) > (0==0) ?(9!=9 :(0==0)*/
    /* 输出结果是0, 正确的期望值输出,应该是10 (=9+1) */
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m705RxoN-1661923667114)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 于是又有了下面的改进:

#define max(a, b) ((a) > (b) ? (a) : (b))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xriR0pv8-1661923667116)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 这个版本,也是我们日常写代码最经常看到的版本,我们使用测试代码测试下看看:

/* 假设有如下的调用代码 */
{
    printf("result = %d\n", 9 + max(9!=9, 0==0));

    /* 宏定义展开后是  9 + ((9!=9) > (0==0) ?(9!=9):(0==0))*/
    /* 输出正确的期望值10 (=9+1) */

    int a = 8;
    int b = 9;

    printf("result = %d\n", max(a++, b++));

    /* 宏定义展开后是  ((a++) > (b++) ?(a++):(b++))*/
    /* 输出结果是10;而正确的期望值输出,应该是9 */
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2JAec1EG-1661923667122)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 很遗憾,经测试,这个版本依然有问题,这是因为宏定义中的++操作干扰了比较结果的输出,我们需要再次改进这个宏定义。应该怎么样改进呢?既然是++操作干扰了输出,那么我们使用2个中间变量来中转下不就ok了吗?于是有了下面的版本:

#define max(a, b) ({\
int _a = (a);\
int _b = (b);\
_a > _b ? _a : _b;\
})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t7VD8Szr-1661923667123)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 这样的写法,已经有点接近Linux内核定义的模样了。再次使用上面的测试代码执行测试:

/* 假设有如下的调用代码 */
{    
    int a = 8;
    int b = 9;

    printf("result = %d\n", max(a++, b++));

    /* 宏定义展开后是  ({int _a=a++; int _b=b++; _a > _b ? _a : _b;})*/
    /* 输出正确的期望值9 */
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dpinR0OT-1661923667125)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 虽然上面版本的定义解决了++操作符引起的输出结果错误的问题,但是由于宏定义内部使用了int型的_a和_b作为中间变量,这就是限制了max宏定义只能用于2个int型的数据做比较,这将大大限制了它的使用范围。于是,很容易想到一个解决办法,将int这个数据类型使用type变量传进去,于是有了下面的版本:

#define max(type, a, b) ({\
type _a = (a);\
type _b = (b);\
_a > _b ? _a : _b;\
})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WVFbRavF-1661923667126)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 这样的确是解决了如上数据类型问题的困惑,但是这样我们的宏定义是以多一个参数输入为牺牲代价的。那么,我们有没有什么办法,可以不将type输入,而直接从输入的a和b中获取它们的数据类型呢?答案是肯定有的!

GNU C作为C语言的扩展版本,增加了若干非常有用的扩展语法,其中typeof关键字就是其中的一个。比如定义一个变量int a; 则typeof(a)就可以取得a变量的类型,即int;比如直接使用typeof(unsigned char *),得到的输出就是数据类型unsigned char *,非常的实用。于是我们将typeof应用到max宏中,于是就有下面的优良版本:

#define max(a, b) ({\
typeof(a) _a = (a);\
typeof(a) _b = (b);\
_a > _b ? _a : _b;\
})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KTnbdo0C-1661923667127)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 这样的写法,虽然避免了我们传递a和b变量的数据类型进去,但是,如下的测试代码,结果会怎么样呢?

/* 假设有如下的调用代码 */
{    
    int a = 8;
    float b = 9.0;

    printf("result = %d\n", max(a, b));
    /* 这样能比较吗?*/

    int a = 8;
    float b = 9.0;
    float *p = &b;

    printf("result = %d\n", max(a, p));
    /* 这样又能比较吗?*/
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0v9A1VG-1661923667128)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 很明显,当a是int型,而b是float型,内核执行比较是可以的;但是如果拿一个int型的变量跟一个float *变量做比较,或者两个奇奇怪怪的struct类型变量做计较,这样肯定是不行的。所以,我们在设计max宏定义的时候,需要将这种可能出现的问题尽可能地在编译阶段就暴露出来,于是有了Linux内核max宏定义的最佳版本:

#define max(a, b) ({\
typeof(a) _a = (a);\
typeof(a) _b = (b);\
(void) &_a == &_b;\
_a > _b ? _a : _b;\
})

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YVJm8AAy-1661923667129)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]

​ 我们注意,宏定义的第4行,(void) &_a == &_b; 意思是对_a和_b的地址做比较,实际上从运行结果上,这个肯定是不等的,但是我们关心的并不是两者比较的结果,而是两者能不能用==比较的问题。当_a和_b的数据类型一致时,代码编译不会有任何警告;反之,当两者的数据类型不一致时,比如之前的a是int型,而b是float型,那么这条语句就会报出编译警告,如果在严格的编译选项下,这个警告还可以转换为错误,要求代码调用者去确认结果,是否对两个不同类型的数据执行max比较的动作,从而将隐患消除,提升代码质量。


​ 通过跟随笔者的思路,我们可以细细地体会到,内核设计者在设计这个max宏时,相信也是走了不少的弯路,从一开始最简版本,接着遇到各式各样的问题,然后一步步解决,一步步完善设计,最终才有最优秀的max宏呈现在我们面前。如此之类的代码设计,在Linux内核设计代码中比比皆是,今后笔者也会集中整理此类的优秀设计,致力于将更多的优秀内核代码分享给读者,敬请关注。文中提及的观点,均为笔者愚见,如有纰漏之处,还望诚心指正,谢谢。
 

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

全部0条评论

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

×
20
完善资料,
赚取积分