Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?

最新max()宏

上回,我们在《Linux内核中max()宏的奥妙何在?(一)》一文中说到,在3.18.34版Linux内核源码中的max()宏,采用了GCC的扩展特性,可以避免一些错误。但是在Linux内核开源世界里,没有最好,只有更好,内核中的max()宏又被打补丁啦。让我们先来看看,Linus大神对于新补丁的看法:
Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?_第1张图片
是的,以前求最大值和最小值的宏还要定义各自的局部变量,而且变量名似乎并不是很贴切,后来又使用GCC的扩展__UNIQUE_ID()函数来进行唯一命名,这似乎还是有所欠缺。
拥抱变化,才会适应变化,那么最新的max()宏又变成什么样了呢?下面让我们来一探究竟。
在5.1.16版内核源码中,常见的比较大小的宏长这样:

/**
 * min - return minimum of two values of the same or compatible types
 * @x: first value
 * @y: second value
 */
#define min(x, y)	__careful_cmp(x, y, <)

/**
 * max - return maximum of two values of the same or compatible types
 * @x: first value
 * @y: second value
 */
#define max(x, y)	__careful_cmp(x, y, >)

/**
 * min3 - return minimum of three values
 * @x: first value
 * @y: second value
 * @z: third value
 */
#define min3(x, y, z) min((typeof(x))min(x, y), z)

/**
 * max3 - return maximum of three values
 * @x: first value
 * @y: second value
 * @z: third value
 */
#define max3(x, y, z) max((typeof(x))max(x, y), z)

我们拿#define max(x, y) __careful_cmp(x, y, >)宏定义来分析,那么__careful_cmp(x, y, >)又是什么呢?首先__careful_cmp(x, y, op)这个函数前面加了 “__”表示他是一个Linux的内部函数,他有三个参数,分别是x,y和op。到此我们就可以看出当op为>时,函数功能为x和y中求最大数,当op为<时,函数功能为x和y求最小数。我们再来看看他的定义:

#define __careful_cmp(x, y, op) \
	    __builtin_choose_expr(__safe_cmp(x, y), \
		__cmp(x, y, op), \
		__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op))

那么__careful_cmp(x, y, op)宏替换的函数

__builtin_choose_expr(__safe_cmp(x, y), \
		__cmp(x, y, op), \
		__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op))

又是何方神圣呢?

__builtin_choose_expr(__safe_cmp(x, y), \
		__cmp(x, y, op), \
		__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op))

这个函数是一个谓词函数,它属于编译时行为,而非运行时行为,跟sizeof和typeof一样,他的第一个参数必须是常量。

  • 如果__safe_cmp(x, y)的结果非0,则返回__cmp(x, y, op),且返回类型与__cmp(x, y, op)的类型一致。
  • 如果__safe_cmp(x, y)的结果为0,则返回__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op),且返回类型与__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op)类型一致。
  • 由于是编译时行为,因此__builtin_choose_expr()宏第二个和第三个参数所产生的目标代码是互斥的,生成了__cmp(x, y, op)就不会存在__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op)

现在我们现在已经知道了__builtin_choose_expr()函数的作用,其实就是就是根据第1个参数,选择返回第2个参数还是第3个参数。接下来,我们一起看看它的参数吧!

第一个参数__safe_cmp(x, y)

它在内核中是这样定义的:

#define __safe_cmp(x, y) \
		(__typecheck(x, y) && __no_side_effects(x, y))

嗯?又出现了两个陌生的函数!问题不大,我们继续看他们的定义:

#define __typecheck(x, y) \
		(!!(sizeof((typeof(x) *)1 == (typeof(y) *)1)))

嗯?看不懂,但,问题依旧不大。首先,宏观上,他是(a==b)型的,他不是一个函数,而是一个表达式。微观上,看内部,我先把他的括号一层一层扒开:

(typeof(x) *)
(typeof(y) *)
((typeof(x) *)1 == (typeof(y) *)1)
(sizeof((typeof(x) *)1 == (typeof(y) *)1))
(!!(sizeof((typeof(x) *)1 == (typeof(y) *)1)))
  • typeof()是GCC提供的,是在预编译时处理的,作用是获取变量的数据类型。(typeof(x) *)是获取x的数据类型,如果x是int 型,(typeof(x) *)的返回值就是int *,如果x是double型,(typeof(x) *)的返回值就是double *。
  • ((typeof(x) *)1 == (typeof(y) *)1)就等价于(int * 1 == double * 1)
  • (sizeof((typeof(x) *)1 == (typeof(y) *)1))实现了严格的类型检查
  • (!!(sizeof((typeof(x) *)1 == (typeof(y) *)1)))把表达式的结果置为1

到此,我们就知道了__typecheck(x, y)宏其实是用来做一个严格的类型检查,无论怎么样,表达式的结果都是1。具体功能就是判断y的类型是否与x相同,如果类型不同,编译器会跑出一个警告,有图有真相。

  • 如果他们是不同类型:有警告,表达式结果是1
    Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?_第2张图片
  • 如果他们是相同类型:一切风平浪静 ,表达式结果是1
    Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?_第3张图片

从上面两个例子中可以看出__typecheck(x, y)表达式是做类型检测,类型不同则给出警告,表达式的值永远是1,不说了,看图好理解

Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?_第4张图片

明白了表达式(__typecheck(x, y) && __no_side_effects(x, y))中的第1个函数__typecheck(x, y),我们再看第2个函数__is_constexpr(y),看完源码再说:

#define __no_side_effects(x, y) \
		(__is_constexpr(x) && __is_constexpr(y))

这又是啥啊,问题依旧不大,看了再说,先看看__is_constexpr()的定义:

#define __is_constexpr(x) \
	    (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

嗯,很复杂,别怕,我们先来看这个部分:

((void *)((long)(x) * 0l))
  • 如果x是一个整数常量表达式,那么((long)(x) * 0l)是一个值为0的整数常量表达式,((void *)((long)(x) * 0l))是一个空指针常量。
  • 如果x不是一个整数常量表达式,那么((void *)((long)(x) * 0l))不是一个空指针常量。
  • (long)(x)强制转换旨在允许x具有指针类型并避免u64在32位平台上对类型发出警告。
  • 值为0的整型常量表达式或类型为void *的表达式称为空指针常量。

我们接着看:

(8 ? ((void *)((long)(x) * 0l)) : (int *)8)

这个条件运算符会根据条件返回不同的类型:

  • 如果x不是一个整数常量表达式,那上面的表达式会返回int *
  • 如果x不是一个常量表达式,上面表达式会返回void *

最终的结果是这样的,不管其值,管其类型,我觉得大家都不想看了,我还是以图明意吧:
Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?_第5张图片

第二个参数__cmp(x, y, op)

在下面这个函数中:

__builtin_choose_expr(__safe_cmp(x, y), \
		__cmp(x, y, op), \
		__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op))

当第1个参数__safe_cmp(x, y)返回值为1的时候,__builtin_choose_expr()函数的返回值为__cmp(x, y, op)函数的返回值,直接看__cmp(x, y, op)函数源码:

#define __cmp(x, y, op)	((x) op (y) ? (x) : (y))

这个函数相对简单点,也是实现比较大小求最值的核心代码,需要注意的是这个函数第三个参数op,求最大值会被替换为>,求最小值会被替换为<。

第三个参数__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op)

看起来貌似又很复杂,但问题依旧不大,先看源码:

#define __cmp_once(x, y, unique_x, unique_y, op) ({	\
   	typeof(x) unique_x = (x);		\
   	typeof(y) unique_y = (y);		\
   	__cmp(unique_x, unique_y, op); })

首先,__cmp_once()这个函数有5个参数,分别是x、y、 unique_x、unique_y和op,x和y是要比较大小的两个数, unique_x和unique_y是唯一命名后的两个局部变量,op会被替换成>或小于,如果是求最大值,则op就是>,如果是求最小值,op就是<。
在这个函数要替换的表达式中,我们又看到GCC的扩展({statement list})和typeof()了,先回顾一下其功能:

  • ({statement list})是一个表达式,类似于逗号表达式,但是功能更强,({语句1;语句2;语句3;})中可以包含有多条语句(可以是变量定义、复杂的控制语句),该表达式的值为statement list中最后一条语句的值。
  • typeof()的功能是取变量类型。typeof(x)是获取x的类型,typeof(x) unique_x = (x);就是定义一个x的类型的变量 unique_x,并把x的数值赋值给它。如x是int型的5,那么typeof(x) unique_x = (x),就相当于int unique_x =5。与之前的命名_max1,_max2相比,这样的命名更加贴切。

首先,表达式的结构是这样({语句1;语句2;语句3;}),根据GCC的扩展特性,这个表达式最终的值应该是语句3的值。

语句1typeof(x) unique_x = (x); 是定义了一个同x的类型的唯一的局部变量unique_x,并把x的数值赋值给了unique_x。

语句2typeof(y) unique_y = (y);;是定义了一个同y的类型的唯一的局部变量unique_y,并把y的数值赋值给了unique_y。

与之前的max()宏源码的不同之处在于,这里的unique_x和unique_y是经过函数__UNIQUE_ID(__x)和 __UNIQUE_ID(__y)唯一命名来传入的。__UNIQUE_ID()可以生成一个唯一的名字,唯一性是由编译器提供的__COUNTER__宏保证的,这也是GCC的一个扩展,在GCC文档中对其的说明如下:
This macro expands to sequential integral values starting from 0. In conjunction with the ## operator, this provides a convenient means to generate unique identifiers.
简而言之,__COUNTER__会被展开为一个从0开始的整数,且每次调用后其值都会加一,这也就保证了其命名的唯一性。

语句3__cmp(unique_x, unique_y, op);又是如下宏的替换:

__cmp(x, y, op)	((x) op (y) ? (x) : (y))

用局部变量唯一命名的方式来进行比较大小,就会避免输入参数和宏定义内部使用的局部变量重名,而导致在宏定义的语句块外层同名变量被内层变量作用而出现错误,这也就避免了类似于x++和y++进行比较而出现错误了。

到此,我们就把5.1.16版max()宏的参数和作用都分析完了,好像有点复杂,但问题依旧不大,让我们宏观上来总结一下,且看5.1.16版内核max()宏的结构图:
Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?_第6张图片
当我们调用max(x,y)时,他被替换成__careful_cmp(x, y, >),进而执行函数的第1个参数__safe_cmp(x, y)

__builtin_choose_expr(__safe_cmp(x, y), \
		__cmp(x, y, op), \
		__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op))
  • 若x和y是同种类型,则__typecheck(x, y)返回1,无警告;若x和y是不同类型,则__typecheck(x, y)返回1,有编译警告。
  • 若x和y都是常量表达式,则__no_side_effects(x, y)返回1;若x和y其中有一个不是常量表达式,则__no_side_effects(x, y)返回0。
  • __no_side_effects(x, y)返回1,则__safe_cmp(x, y)返回1;若__no_side_effects(x, y)返回0,则__safe_cmp(x, y)返回0。
  • __safe_cmp(x, y)返回1,则__builtin_choose_expr函数返回__cmp(x, y, op)的值,若__safe_cmp(x, y)返回0,则__builtin_choose_expr函数返回__cmp_once(x, y, __UNIQUE_ID(__x), __UNIQUE_ID(__y), op)的值。
  • 最后,max(x,y)的返回值就是__careful_cmp(x, y, op)的返回值,也就是__builtin_choose_expr的返回值。

到此,我们已经把5.1.16版内核max()宏的参数和作用都分析完了,那么max()宏是如何从诞生成长到现在的呢?它到底经历过什么,下篇文章让我们继续探索!

你可能感兴趣的:(Linux内核)