Linux内核中max()宏的奥妙何在?(一)

Linux内核中max()宏的奥妙何在?(一)

1、max()宏那点事

在Linux内核中,有这样四个比较大小的函数,如下:

max(x,y) //两个数求最大值
min(x,y) //两个数求最小值
max3(x,y,z) //三个数求最大值
min3(x,y,z) //三个数求最小值

以Linux内核源码linux-3.18.34为例,它被定义在kernel.h中,它位于:

linux-3.18.34/include/linux/kernel.h

我们先来思考这样一个问题,如果让你写一个max(x,y)的宏,你会怎么写?
有同学说,不就是两个数比较大小求最小值的宏嘛,一行代码解决战斗,瞧瞧我的吧,于是乎出现了这样一个宏:

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

这样一个宏看似很精简,很完美,其实,问题很大,不信看下面的例子:

#include
#define max(a,b) ((a)>(b)?(a):(b))
int main()
{

        int x = 1, y = 2;
        printf("max=%d\n", max(x++, y++));
        printf("x = %d, y = %d\n", x, y);
}

执行完max(x++,y++),我们期望max的值为2,x的值为2,y的值为3。但实际的结果呢,有图有真相。
Linux内核中max()宏的奥妙何在?(一)_第1张图片
执行完max(x++,y++)后,max的值为3,x的值为2,y的值为4, 和我们想的不一样! 为什么会这样呢?我们又该如何解决这样的问题呢?
问题不大,GCC他来了,他来了,他带着GCC中的({statement list})和typeof()扩展走来了。

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

那么GCC的扩展在内核中是如何巧妙使用的呢,下面我们就结合内核中的max()宏的源码来一探究竟,先看下linux-3.18.34内核中有哪些常见的求最值的宏:

/*
 * min()/max()/clamp() macros that also do
 * strict type-checking.. See the
 * "unnecessary" pointer comparison.
 */
#define min(x, y) ({				\
	typeof(x) _min1 = (x);			\
	typeof(y) _min2 = (y);
	(void) (&_min1 == &_min2);		\
	_min1 < _min2 ? _min1 : _min2; })

#define max(x, y) ({				\
	typeof(x) _max1 = (x);			\
	typeof(y) _max2 = (y);			\
	(void) (&_max1 == &_max2);		\
	_max1 > _max2 ? _max1 : _max2; })
	
#define min3(x, y, z) min((typeof(x))min(x, y), z)
#define max3(x, y, z) max((typeof(x))max(x, y), z)

由此观之,大同小异,下面我们以max(x,y)宏为例,进行深入探究:

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

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

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

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

语句3(void) (&_max1 == &_max2);对于程序的执行是没有任何作用的,它的作用在于判断两个数的类型是否相同,如果类型不同,就会在编译过程中抛出一个警告。因为x和y的类型不一样,其指针类型也会不一样,两个不一样的指针类型进行比较操作,会抛出一个编译警告。如char * x; int * y, 然后x==y, 这个判断因为一个是char * 一个是int *,所以gcc在编译时会产生一个warning,这样可以避免一些潜在的错误发生。

语句4_max1 > _max2 ? _max1 : _max2;才是最终求最大值的核心语句,那为什么要大费周章这样写呢?

在这个宏定义中,先根据x和y的类型生成了两个局部变量_max1和_max2,之后判断其类型,比较其大小,返回较大的一个,这样就保证了宏参数只会被执行一次,避免了之前实验中出现的错误结果。要将x和y重新定义为_max1和_max2是为了避免输入参数和宏定义内部使用的局部变量重名,重名会导致在宏定义的语句块外层同名变量被内层变量作用而出现错误,这也就是前面提到的为什么会执行两次y++而出现错误的原因了。

那么接下来,我们就编写一个内核模块来实现求最大数,看看内核中的max(x,y)宏的使用效果。

2、内核模块代码

如下代码实现了在Linux内核模块中,两个数比较大小,并输出最大数。

/**********************************************************
 * Author        : 梁金荣
 * Email         : [email protected]
 * Last modified : 2019-09-20 7:00
 * Filename      : maxnum.c
 * Description   : 模块初始化函数中求最大数(使用内核中的代码)
 * *******************************************************/

/**
* 必要的头文件
*/
#include
#include
#include

/**
* 模块的初始化函数,模块的入口函数,加载模块,需超级用户权限
*/
static int __init lk_maxnum(void)
{
        int x = 1, y = 2;
        printf("max=%d\n", max(x++, y++));
        printf("x = %d, y = %d\n", x, y);
        return 0;
}

/**
* 出口函数,卸载模块,需超级用户权限
*/
static void __exit lk_exit(void)
{
        printk("The maxnum moudle has exited!\n");
}

module_init(lk_maxnum); //内核入口点,调用初始化函数,包含在module.h中
module_exit(lk_exit); //出口点
MODULE_LICENSE("GPL"); //许可证
MODULE_AUTHOR("ljr"); //作者(非必须)
MODULE_DESCRIPTION("max number"); //模块描述(非必须)

3、Makefile文件

如下代码为上述内核模块的Makefile文件:

#产生目标文件
obj-m:=maxnum.o
#定义当前路径
CURRENT_PATH:=$(shell pwd)
#定义内核版本号
LINUX_KERNEL:=$(shell uname -r)
#定义内核源码绝对路径
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#编译模块
all:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
#清理模块
clean:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

切记,在Makefile文件中,空格是不能乱加的

4、编译、加载、查看、卸载内核模块

(1)编译内核模块

命令如下:

make

Linux内核中max()宏的奥妙何在?(一)_第2张图片
如上图所示,生成的.ko文件就是内核模块文件(kernel object),该文件可在内核中加载或卸载。

(2)加载内核模块

加载内核模块时需要使用超级用户权限,且模块名后面必须加上.ko,命令如下:

sudo insmod maxnum.ko

(3)查看内核模块

查看内核模块命令如下:

lsmod

Linux内核中max()宏的奥妙何在?(一)_第3张图片
如图,maxnum模块已经加载到内核中了。

(4)卸载内核模块

卸载内核模块时依然需要超级用户权限,注意此时模块名后无需加上.ko,命令如下:

sudo rmmod maxnum

5、查看结果

我们在模块初始化函数中将输出结果打印在系统日志中,查看系统日志命令如下:

dmesg

Linux内核中max()宏的奥妙何在?(一)_第4张图片
如图,我们使用内核中的代码,在模块初始化函数中求出了最大数,并将其输出,同时也验证了GCC中({statement list})的扩展和typeof()解决了普通max(x,y)宏会出现结果错误的问题。

嗯?Linux内核中又更新max()宏相关代码了?新提交的代码是什么样子呢?其中又有什么奥妙之处呢?下一篇文章《Linux内核中max()宏的奥妙何在?(二)——大神Linus对这个宏怎么看?》让我们一探究竟!

先贴一张图片,感受一下:
Linux内核中max()宏的奥妙何在?(一)_第5张图片

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