从一个简单的宏定义看linux内核的严谨

来源:互联网 发布:两台mac屏幕共享 编辑:程序博客网 时间:2024/06/06 14:22

最近闲来无事,分析下linux kernel里面一些函数都是怎样定义使用的,它们都是怎样避免风险的,从include/linux/kernel.h中挑出一个经典的min宏进行分析一下

(例子一)

从我们最先认识的min函数开始看看,这个函数的作用就是求两个数中小一点的那个

#define min(x, y) ({                                \

typeof(x) _min1 = (x);                        \

typeof(y) _min2 = (y);                        \

(void) (&_min1 == &_min2);                \

_min1 < _min2 ? _min1 : _min2; })

 

1)首先我们可以看到这个宏的定义外面由一个({}),为什么要这样写呢?其实{}很好理解,因为我们的宏是多个语句,假如没有了这个{},在一些语句中就会出现歧义,比如 ifaminx,y),这样就会出问题,后面的语句它就不会执行了。那么这里为什么在外面还要加一个()呢,我们来看这个函数的作用,它是用来返回一个较小的数,对,是返回,一般来说对于返回值,我们怎么用?最简单的当然是return了,所以,一般来讲,我们会写return min(x,y);(当然还有很多常用的方法,比如说赋值a=min(x,y)等等)。这个时候你就不难理解我们为什么要加个()了吧,不过正规的来讲,这是GCC扩展中的一个用法,在GCC扩展中允许吧一个()内的复合语句看成是一个表达式,称为语句表达式,它可以出现在任何语句表达式可以出现的地方。

2)继续看这个宏,我们知道一般我们使用这个函数的时候,x,y是什么类型的并不清楚,我们是否需要所有的类型都写一个min函数呢?其实不需要,在GCC的扩展中,我们提供了一个宏,称为typeof,它可以获得相应参数的类型,这样就可以不需要因为类型而重写宏了,小方便啊。

再看这两句话:

typeof(x) _min1 = (x);                        \

typeof(y) _min2 = (y);                        \

为什么要把xy赋值给_min1_min2然后返回_min1_min2,而不直接写成(x) < (y)? (x) :(y)呢?这个就是最经典的C语言课本中所说的那个所谓的++的问题了,还需要多说吗?好吧,我说一下吧,因为我也是菜鸟,也记不清当时是在什么地方提到这个东西了,呵呵,不过印象中记得好像是这样说的:假如我写成min(a++,b++),展开成宏就变成了(a++ < (b++)? (a++) :(b++)了,究竟ab各自++了多少次,大概可以算出来吧,所以我们一定要先把它们赋值给两个局部变量,这样就没有这个问题了。

3)下面再来看一下那个(void) (&_min1 == &_min2);这句话是为了在比较不同类型的时候能够打印一个警告信息。好吧,我试试看,写一个简单测试程序

#include <stdio.h>

 

#define min(x, y) ({                                \

typeof(x) _min1 = (x);                        \

typeof(y) _min2 = (y);                        \

(void) (&_min1 == &_min2);                \

_min1 < _min2 ? _min1 : _min2; })

 

main(){

unsigned long a = 4;

in b = 5;

 

printf("min is %d\n", min(a, b));

}

gcc编译了一下,果然报下面警告了:

min.c: In function ‘main’:

min.c:14: 警告:比较不相关的指针时缺少类型转换

没有这句话,就不会报这个警告,为什么会这样呢?原来,在比较地址的时候,会发现地址所存值的类型,假如不同的话就会报警告,而在直接比较值的时候,比如上面的_min1 < _min2 ?的时候,会做强制类型转换,而且不会报警告,好吧,我真的很佩服写这段代码的人,太牛了!简单的4,5行代码,竟然有这么多的东西在里面,这叫老夫如何是好啊,继续努力。


(例子二)

在Linux内核中,经常会看到do{}while(0)这样的语句,许多人开始都会疑惑,认为do{}while(0)毫无意义,因为他只会执行一次,加不加do{}while(0)效果是完全一样的,其实do{}while(0)主要用于宏定义中。
这里用一个简单点的宏来演示:
# define SAFE_FREE( p) do { free ( p) ; p = NULL ; } while ( 0)
假设这里去掉do...while(0),即定义SAFE_FREE为:
# define SAFE_FREE( p) free ( p) ; p = NULL ;
那么以下代码:
if ( NULL ! = p) 
   SAFE_FREE( p) 
else 
  . . .
会被展开为:
if ( NULL ! = p) 
  free ( p) ; p = NULL ; 
else 
  . . .
展开的代码中存在两个问题:
if 分支后面有两个语句,导致else分支没有对应的if,编译失败;
假设没有else分支,则SAFE_FREE中的第二个语句无论if测试是否通过都会执行。


将SAFE_FREE的定义加上{}就可以解决上诉问题了,即:
# define SAFE_FREE( p) { free ( p) ; p = NULL ; }
这样,代码:
if ( NULL ! = p) 
   SAFE_FREE( p) 
else 
  . . .
会被展开为:
if ( NULL ! = p) 
  { free ( p) ; p = NULL ; } 
else 
  . . .
  
但是,在C程序中,每个语句后面加分号是一种约定俗成的习惯,那么,如下代码:
if ( NULL ! = p) 
   SAFE_FREE( p); 
else 
  . . .
将为扩展为:
if ( NULL ! = p) 
  { free ( p) ; p = NULL ; } ; 
else 
  . . .
这样,else分支就有没有对应的if了,编译将无法通过。假设用了do{}while(0),情况就不一样那个了,同样的代码会被扩展为:
if ( NULL ! = p) 
  do { free ( p) ; p = NULL ; } while ( 0) ; 
else 
  . . .
不会再出现编译问题。do{}while(0)的使用完全是为了保证宏定义的使用者能无编译出错的使用宏,它不对其使用者做任何假设。

0 0