Linux内核学习(八):linux内核配置与模块
内核的配置是开发的第一步,前面我kconfig那篇讲了内核这些配置怎么生成的。当时也提到了内核配置的一个图形化的界面,这里再来啰嗦几句。
本文内容全部来自《奔跑吧 linux内核》
1、内核配置工具
(1)make config
这是基于文本的一种传统的配置方式。它会为内核支持的每一个特性向用户提问,如果用户输入“y”,则把该特性编译进内核;如果输入“m”,则把该特性变成以模块;如果输入为“n”,则表示不编译该特性。
(2)make oldconfig
make oldconfig和make config很类似,也是基于文本的配置工具,只不过它是在现有的内核配置文件的基础上建立一个新的配置文件,在有新的配置选项时会向用户提问。
(3)make menuconfig
make menuconfig是一种基于文本模式的图形用户界面,用户可以通过移动光标来浏览内核支持的特性。
在那篇文章里写到了上述几种内核内置工具最终会在 Linux 内核源代码的根目录下生成一个隐藏文件,即.config文件,这个文件包含了内核的所有配置信息。下面是一个.config文件的例子。
.config 文件的每个配置选项都以“CONFIG_”字段开始,后面的 y 表示内核会把这个特性静态编译进内核,m表示这个特性会被编译成内核模块。
如果不需要编译到内核中,就要在前面使用“#”注释,并在后面用“is not set”来标识。
.config文件通常有几千行,每一行都通过手工输入显得不现实。
config太多,实际项目中如何生成这个.config文件呢?
(1)基于开发板
使用板级的配置文件一些芯片公司通常会提供基于某款 SoC 芯片的开发板,读者可以基于此开发板来快速开发产品原型。
芯片公司同时会提供板级开发板包,其中包含移植好的Linux内核。以ARM公司的Vexpress板子为例,该板子对应Linux内核的配置文件被放在arch/arm/configs目录中。如图3.3所示,arch/arm/configs目录下包含了众多的ARM板子的配置文件。
ARM Vexpress板子对应的config配置文件是vexpress_defconfig文件,可以通过下面命令来配置内核。
$ export ARCH=arm $ export CROSS_COMPILE=arm-linux-gnueabi $ make vexpress_defconfig
(2)使用系统的配置文件
当我们需要编译电脑中的Linux系统内核时,可以使用系统自带的config文件。
以优麒麟18.04系统为例,boot目录下面有一个config-4.15.0-22-generic文件。当我们想编译一个新内核(如 Linux 4.17 内核)时,可以通过如下命令来生成一个新的.config文件。
$ cd linux-4.17 $ cp /boot/config-4.15.0-22-generic ./.config
下面紧接着我们来看看内核模块
2、内核模块
Linux内核采用宏内核架构,即操作系统的大部分功能都在内核中实现,比如进程管理、内存管理、进程调度、设备管理等,并且都在特权模式下(内核空间)运行。
而与之相反的另一种流行的架构是微内核架构,它把操作系统最基本的功能放入内核中,而其他大部分的功能(如设备驱动等)都放到非特权模式下,这种架构有天生优越的动态扩展性。
Linux的这种宏内核可以理解为一个完全静态的内核,那如何实现运行时内核的动态扩展呢?
其实Linux内核在发展过程中很早就引入了内核模块这个机制,内核模块全称Loadable Kernel Module(LKM)。
在内核运行时加载一组目标代码来实现某个特定的功能,这样在实际使用Linux的过程中可以不需要重新编译内核代码来实现动态扩展。
Linux内核通过内核模块来实现动态添加和删除某个功能。
2.1、从一个内核模块开始
上面我们知道了模块的相关知识,现在我们来整一个模块注册进内核看看,体验一下这个流程。
a. 准备内核模块代码
0 #include <linux/init.h> 1 #include <linux/module.h> 2 3 static int __init my_test_init(void) 4 { 5 printk("my first kernel module init\n"); 6 return 0; 7 } 8 9 static void __exit my_test_exit(void) 10 { 11 printk("goodbye\n"); 12 } 13 14 module_init(my_test_init); 15 module_exit(my_test_exit); 16 17 MODULE_LICENSE("GPL"); 18 MODULE_AUTHOR("Ben Shushu"); 19 MODULE_DESCRIPTION("my test kernel module"); 20 MODULE_ALIAS("mytest");
这个简单的内核模块只有两个函数:
- 一个是 my_test_init()函数,输出一句话“my first kernel module init”;
- 另一个是my_test_exit()函数,输出“goodbye”。
麻雀虽小,五脏俱全,这是一个可以运行的内核模块。
第0行和第1行包含了两个Linux内核的头文件,其中<linux/init.h>头文件对应的是内核源代码的 include/linux/init.h 文件
在这个头文件中包含了第 14 行和第 15 行中的module_init()和 module_exit()函数的声明。
<linux/module.h>头文件对应的是内核源代码的include/linux/module.h文件,包含了第17~20行的MODULE_AUTHOR()这些宏的声明。
**第 14 行的 module_init()告诉内核这是该模块的入口。**内核在各个模块初始化时有一个优先级顺序。对于驱动模块来说,它的优先级不是特别高,而且内核把所有模块的初始化函数都存放在一个特别的段中来管理。
第15行的module_exit()宏告诉内核这个模块的退出函数是my_test_exit()。
第3~7行是该内核模块初始化函数,我们在这个例子中仅仅用printk输出函数往终端中输出一句话。printk是类似C语言库中的printf()输出函数,但是它增加了输出级别的支持。这个函数在内核模块被加载时运行.
(可以使用insmod命令来加载一个内核模块。)
第9~12行是该内核模块的退出函数,在这个例子中我们也仅仅用printk输出一句话,标记卸载了该模块
(可以使用rmmod命令卸载一个内核模块)
(insmod和rmmod两个的意思就是咱们可以不只是说说prink,可是直接干。)
第 17~20 行,MODULE_LICENSE()表示这个模块代码接受的软件许可协议。Linux 内核是一个使用GPL V2的开源项目,这要求所有使用和修改了Linux内核源代码的个人或者公司都有义务把修改后的源代码公开,也就是一个强制的开源协议,因此一般我们编写的驱动代码中都需要显式地申明和遵循这个协议。
MODULE_AUTHOR()用来描述该模块的作者信息,可以包括作者的姓名和邮箱等。
MODULE_DESCRIPTION()用来简单描述该模块的用途或者功能介绍。
**MODULE_ALIAS()为用户空间提供一个合适的别名。**下面我们来看如何编译这个内核模块。在优麒麟Linux上编译该内核模块,下面是编写内核模块的Makefile文件。
现在创建好了咱们的模块,下一步就是来编写内核模块的makefile文件
b. 内核模块的makefile文件
0 BASEINCLUDE ?= /lib/modules/`uname -r`/build 1 2 mytest-objs := my_test.o 3 obj-m := mytest.o 4 5 all : 6 $(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules; 7 8 clean: 9 $(MAKE) -C $(BASEINCLUDE) SUBDIRS=$(PWD) clean; 10 rm -f *.ko;
第0行的****BASEINCLUDE指向正在运行Linux的内核编译目录,对于编译优麒麟Linux中运行的内核模块,我们需要指定到当前系统对应的内核中。一般来说,Linux系统的内核模块都会安装到/lib/modules这个目录下,通过“uname -r”命令可以找到对应的内核版本。
首先通过“uname -r”来查看当前系统的内核,比如我的系统里面装了4.15.0-20-generic的内核版本,这个内核版本的头文件放在/usr/src/linux-headers-4.15.0-20-generic目录中。
第2行表示该内核模块需要哪些目标文件,格式是:<模块名>-objs := <目标文件>.o
第3行表示要生成的模块。注意,模块名字不能和目标文件名相同。格式是: obj-m :=<模块名>.o
第5~6行表示要编译执行的动作。
第8~10行表示执行make clean需要的动作。
然后在终端中输入make命令来执行编译。$ make编译完成之后会生成mytest.ko文件。
我们可以通过file命令检查编译的模块是否正确,可以看到变成x86-64架构的ELF文件,说明已经编译成功了。
另外,也可以通过modinfo命令进一步做检查。
c.验证
接下来就可以在优麒麟Linux机器上验证我们的内核模块了
$sudo insmod mytest.ko (安装模块)
你会发现没有输出,别着急,因为例子中的输出函数 printk()的默认输出等级,可以使用dmesg命令查看内核的打印信息。
$dmesg… [258.575353] my first kernel module init
另外,你可以通过lsmod命令查看当前mytest模块是否已经被加载到系统中,它会显示模块之间的依赖关系。
加载模块之后,系统会在/sys/modules目录下新建一个目录,比如对于mytest模块会建一个名为mytest的目录。
figo@figo-OptiPlex-9020:/sys/module/mytest$ tree -a
如果需要卸载模块,可以通过rmmod命令来实现。
d. linux模块小结
模块加载函数:加载模块时,该函数会被自动执行,通常做一些初始化工作。
模块卸载函数:卸载模块时,该函数也会被自动执行,做一些清理工作。
模块许可声明:内核模块必须声明许可证,否则内核会发出被污染的警告。
模块参数:根据需求来添加,为可选项。
模块作者和描述声明:一般都需要完善这些信息。
模块导出符号:根据需求来添加,为可选项。
2. 模块参数传递
内核模块作为一个可扩展的动态模块,为Linux内核提供了灵活性。但是有时我们需要根据不同的应用场景给内核模块传递不同的参数,Linux内核提供一个宏来实现模块的参数传递。
#define module_param(name, type, perm) \ module_param_named(name, name, type, perm) #define MODULE_PARM_DESC(_parm, desc) \ __MODULE_INFO(parm, _parm, #_parm ":" desc)
module_param()宏由3个参数组成,name表示参数名,type表示参数类型,perm表示参数的读写等权限。
MODULE_PARM_DESC()宏为这个参数的简单说明,参数类型可以是byte、short、ushort、int、uint、long、ulong、char和bool等类型。
perm指定在sysfs中相应文件的访问权限,如设置为0表示不会出现在sysfs文件系统中;如设置成S_IRUGO(0444)可以被所有人读取,但是不能修改;如设置成S_IRUGO|S_IWUSR(0644),说明可以让root权限的用户修改这个参数。
(这里你可能有点懵,没事我们接着看看下面的栗子会好点)
这个例子定义了一个模块参数debug,类型是int,初始化值为1,权限访问为0644。
也就是说root权限用户可以修改这个值,这个参数的用途是打开调试信息。(参数说明的意思)
其实这是一个比较常用的内核调试方法,可以通过模块参数使用调试功能。通过debug=1 ,打开了调试功能。
下面这个例子定义了两个内核参数,一个是debug,另一个是静态全局变量mytest。
当通过“insmod mymodule.ko mytest=200”命令来加载模块时,可以看到终端里输出为:
(这里也可以安装)
还可以通过调试参数来关闭和打开调试信息。
在/sys/module/mymodule/parameters目录下面可以看到新增的两个参数。
具体更多地想法和疑惑,去实际操作一下会有深的体会哦
3、符号共享
我们在为一个设备编写驱动程序时,会把驱动按照功能分成好几个内核模块,这些内核模块之间有一些接口函数需要相互调用,这怎么实现呢?(锁一个驱动是很多模块的组合)
Linux内核为我们提供两个宏来解决这个问题。
EXPORT_SYMBOL( ) EXPORT_SYMBOL_GPL( )
EXPORT_SYMBOL()把函数或者符号对全部内核代码公开,也就是将一个函数以符号的方式导出给内核中的其他模块使用。
EXPORT_SYMBOL_GPL()只能包含GPL许可的模块,内核核心的大部分模块导出来的符号都是使用GPL()这种形式的。如果要使用EXPORT_SYMBOL_GPL()导出函数,那么需要显式地通过模块申明为“GPL”,如MODULE_LICENSE(“GPL”)。
内核导出的符号表可以通过/proc/kallsyms来查看。
这些信息包含的内容:
- 第1列显示的是该符号在内核地址空间的地址;
- 第2列是符号属性,比如T表示该符号在text段中;
- 第3列表示符号的字符串,也就是EXPORT_SYMBOL()导出来的符号;
- 第4列显示哪些内核模块在使用这些符号。
相信到了这一步其实懵懵懂懂的应该具有一些能力去自己实现一些模块插到内核中了,想想自己竟然能改变内核了,有没有很兴奋。