说明:
- 所有代码均在Ubuntu 18.04(内核版本
linux 5.4.0-52-generic
)上编译运行通过- 本文同步发表于
www.linuxrevead.com
一、什么是内核模块
内核模块实际上是实现能够为内核扩展某个具体功能的、高聚合的代码。它可以引用内核的API,但一般不对外提供API。之所以称其为模块的原因是因为它的代码可以独立于内核之外,编译结果也可以独立于内核镜像vmlinux之外。在使用时,通过某些方法将预先编译好的模块文件放入到内核的文件系统中,通过内核提供的模块相关工具在内核运行时,将该模块从内核中加载或者卸载,从而实现给内核扩展某种功能。
使用内核模块最显著的特点是Linux kernel运行时即可动态的完成模块的安装和卸载,而并不需要关闭或者重启kernel,这样节省了功能升级成本和服务器切换的时间成本。
二、如何编写内核模块
在学习编写Linux模块之前,有必要说明一下,初学者只需要记住模块编写的格式,重要函数的作用以及它们被调用的时机(实际上就是Linux内核模块的框架)即可,而并不需要完全理解代码中每一行的意思。等真正能够灵活使用模块,或者对于内核有一定了解,再去理解“Linux内核模块框架为什么是这样的”、“背后的实现逻辑是什么”就会简单很多。这就类似于初学者学习C语言的时候,最开始只是机械的记住“在代码第一行写include
2.1 编写"Hello world"内核模块
按照国际惯例,我们首先学习一下"Hello world"内核模块代码。新建一个hello.c
文件,在其中写入下列代码:
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk(KERN_ALERT "Hello world!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_ALERT "Goodbye world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
我们简单介绍一下这个模块的组成。
首先,包含了内核其他模块提供的两个头文件,用于声明后面我们要使用到的“module_init
”、 “module_exit
” 和 “MODULE_LICENSE
”。
其次,声明了“hello_init
”函数,在其中输出了“Hello world!”;声明了“hello_exit
”函数,在其中输出了“Goodbye world!”。
最后,“hello_init
”函数被“module_init
”修饰,表示模块加载的时候被调用。“hello_exit
”函数被“module_exit
”修饰,表示模块卸载的时候被调用。
上述模块实现了以下两个功能:
- 在模块被加载到内核中的时候,
hello_init
函数会被调用,从而在内核的log中输出“Hello world!
” - 在模块从内核中卸载的时候,
hello_exit
函数会被调用,从而在内核的log中输出“Goodbye world!
”
2.2 将模块添加到kernel代码中
由于后面还要基于这个模块编写字符设备驱动,所以,我们现在在drivers/char
下新建hello
这个文件夹,并把hello.c
这个模块放置于drivers/char/hello
目录下。Kernel使用Makefile来管理所有的文件,因此如果想在编译kernel的时候,把我们新加入的drivers/char/hello/hello.c
一起进去,那么需要修改两个Makefile。
修改Makefile——编译drivers/char/hello
目录
在drivers/char/Makefile
的最后一行添加以下代码:
obj-y += hello/
表示drivers/char/hello
需要进行编译。
修改Makefile——编译drivers/char/hello/hello.c
在drivers/char/hello
目录下新增Makefile,并加入以下内容:
obj-m += hello.o
表示drivers/char/hello/hello.c
需要进行编译,并将其编译模块。其中,
m
表示将hello.c
编译为模块;m
可以修改为y
,表示将hello.c
默认编入Linux内核镜像(即vmlinux
)中;m
可以修改为n
,表示不编译hello.c
。
对于一个模块,我们想配置它时,可以直接修改Makefile,但是kernel中有数百个模块,而且有些模块之间依赖关系,那么用修改Makefile的方法工作量就太大了。Kernel中提供了另外一种机制去配置这些模块是否编译,或者是否编译成模块,即Kconfig。
增加hello模块的Kconfig
如果使用Kconfig来配置hello.c,需要以下步骤:
1)将drivers/char/hello/Makefile
中的m
替换成一个变量,该变量的命名规则,我们沿用Linux的规则,定义为CONFIG_HELLO
。
2)在drivers/char/hello/Kconfog
中增加以下内容来定义及描述该变量
config HELLO
tristate "Device driver sample code, named hello"
default m
help
Configure the device driver sample
- 第一行表示,定义变量
CONFIG_HELLO
- 第二行的
tristate
表示CONFIG_HELLO
变量可以取三个值y
m
和n
。 - 第二行的“Device driver sample code, named hello” 表示对该变量简要的解释
- 第三行的"default m"表示
CONFIG_HELLO
变量的默认值 - 第四行“help”及其后的内容表示对
CONFIG_HELLO
变量的详细解释。
3)在drivers/char/Kconfog
增加下面的代码,以包含drivers/char/hello/Kconfog
source "drivers/char/hello/Kconfig"
增加上述代码之后,我们就可以使用Linux提供的menuconfig这个图形化工具来配置kernel了。在代码根目录下执行"make menuconfig
",即可看到下面的界面:
在该界面下,使用上下键可以导航不同的条目,使用回车进入条目的子条目,使用空格可以切换当前选中的条目的配置(即在y
、m
和n
之间循环切换)。依次进入“Device Drivers --->
” -> "Character devices --->
" 就可以看到我们刚刚新增加的条目"<M> Device driver sample code, named hello
",默认被配置为m
,如下图所示。
2.3 编译kernel代码
按照本博客《实验二》树莓派基本开发描述的方法编译完成后,就可以在代码目录的out/drivers/char/hello
目录下生成hello.ko
三、模块的加载和卸载
3.1 加载内核模块
内核模块运行于内核空间,因此需要sudo
权限来执行。可以使用insmod
来加载内核模块。根据上述对hello模块的解释,其作用是在加载的时候,将“Hello world!
”输出到内核log中。
树莓派进入Home界面后,使用U盘,将Ubuntu上编译好的hello.ko
复制到树莓派上来。在终端中输入sudo insmod hello.ko
,即可将上述编译好的hello.ko
动态加载到内核中。
$ sudo insmod hello.ko
然后在终端中输入dmesg
查看kernel log,在输出的log最后一行,我们就可以看到预期的现象 —— 模块加载的时候,打印出“Hello world!
”。从而证明我们自己写的hello.ko模块已成功加载到内核中。
另外,我们已可以使用lsmod
命令来查看已加载到内核的所有模块:
$ lsmod
Module Size Used by
hello 16384 0 <-- 我们刚刚加载的内核模块
thunderbolt 167936 0
cmac 16384 1
...
3.2 卸载内核模块
卸载内核模块的操作环境同加载模块一样,在终端中执行sudo rmmod hello
,即可完成内核模块的卸载。如下所示:
$ sudo rmmod hello
执行完毕之后,在终端中执行dmesg
,查看kernel log:
[ 4011.853307] Goodbye world!
表示刚刚加载的hello.ko
已成功卸载,并输出了我们预期的log —— 模块卸载的时候,打印出“Goodbye world!
”
好的,目前为止,我们就学会了如何编写一个最简单的内核模块,并将其添加到kernel的代码中了。