本篇是《Linux内核学习笔记》系列的第九篇。
从现在开始,我们(其实是我)已经看够了源码,迫不及待地希望能够利用已经学到的东西大展身手。一边学一边敲代码其实是很重要的。本文会介绍模块这一概念,比较添加模块和直接向内核修改代码的异同,并介绍常用的调试技巧。
1. 模块
一个非常好的模块编程教程:https://sysprog21.github.io/lkmpg
1.1. 要不要用模块?
模块显然是有必要存在的。例如,有些代码有用,但注定不是通用代码。例如声卡、显卡等,它们有各种各样的硬件型号,其驱动程序也相当多样,若是将它们全部加入内核源码树中,其会变得极其庞大。但是,大部分计算机可能只用其中的一种或几种。这种情况下,设备驱动程序往往以模块的形式呈现。
还有一种情况,即像我这样的内核菜鸟想要写点小demo来训练基本的API、数据结构等。这样的代码没必要放在源码树,只是作为一个模块就够了。
不过有时的需求,模块注定不能完成。例如静态链接的代码,它已经位于RAM,这时就不能通过外部模块来将其替换。有不少核心组件是静态链接到内核的,例如伙伴系统等。
1.2. Hello模块
接下来的内容就是系统编程的Hello, world。
#include <linux/module.h> // header for all modules
#include <linux/kernel.h> // pr_info
#include <linux/init.h> // about macros of init and exit
static int start(void)
{
pr_info("Hello, world\n");
return 0;
}
static void finish(void)
{
pr_info("Goodbye, world\n");
}
module_init(start);
module_exit(finish);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("SuHaitao");
MODULE_DESCRIPTION("A Hello, World Module");
首先,需要包括一些头文件。这些文件的用途在上面给出。
然后,模块需要定义入口和出口,它们用module_init和module_exit宏进行注册。初始化函数必须满足int func(void)的形式,退出函数必须满足void func(void)的形式,少一个void是不行的!初始化函数返回0表示模块正常加载,否则内核会给出报错信息(尽管初始化函数确实已经执行完了,模块已经被加载到了内核)。
最后三行用于给出模块的一些信息。特别要注意的是,MODULE_LICENSE给出了模块的许可证信息。如果一个模块有GPL许可证,它就是开源的,意味着不是一个二进制黑盒模块。反之它有可能是闭源的、不易调试的黑盒模块,内核开发者对其缺乏信任,认为这种模块会污染内核。因此,非GPL许可证的模块加入内核后,内核会设置一个污染标识。
下面写Makefile。
obj-m += hello.o
PWD := $(CURDIR)
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
首先,我们要向obj-m列表加入hello.o,与源代码文件同名。
make语句的含义是,先跳转至-C选项指示的路径下,然后make modules,最后移动到M参数的值指示的路径下。PWD一般来说就是当前目录,但这里给它赋以CURDIR的值,是防止使用sudo执行命令时PWD不能正确地继承环境变量值,详见这里的解释。
-C指示的是什么路径?其实就是内核源代码的位置,此处是一个软链接。当内核编译好后,使用sudo make modules_install就可以在/lib/modules建立一系列内核模块信息,该路径也就可以使用了。
写好Makefile之后,先不着急编译安装,我们来监听一些内核信息:
$ cat /dev/kmsg
好了,再打开一个Shell,然后编译安装:
$ make && sudo insmod hello.ko
编译会产生.ko文件,然后用insmod就能安装它。这时就会看到内核的输出:
6,1088,23460050244,-;Hello, world
6是信息等级,这里是info级,和pr_info对应。1088是信息的序列号。最后的一串数字是以微秒为单位的时间戳。
然后使用如下命令卸载模块:
$ sudo rmmod hello.ko
内核输出如下(使用cat /dev/kmsg查看):
6,1089,24988083413,-;Goodbye, world
2. 更复杂一点的模块
接下来我们会运用定时器实现一个热情的模块,它每秒钟都要说一次hello。一个很简单的思路是,在模块初始化函数中启动一个定时器,在定时器的回调函数中输出hello,然后重新启动定时器,从而形成无限循环。
第一步是声明定时器。我们把它定义成全局变量,让整个模块,包括回调函数的多次调用,都共用一个计时器。计时器起码得存活到回调函数结束为止,如果草率地定义在模块初始化函数中,add_timer之后就返回掉,那么真到执行回调函数的时候计时器已经不在了,内核就会崩溃。
static struct timer_list timer;
接下来在模块初始化函数中进行计时器初始化,并添加计时器:
timer_setup(&timer, hello, 0);
timer.expires = jiffies + 100;
add_timer(&timer);
hello是回调函数,每次调用它时,它会输出hello,并给内置的计数器加1(这里偷懒了,没有使用锁保护!它不是可重入的函数!)。输出完之后,重新添加计时器,这样一段时间之后hello又会被调用。
static void hello(struct timer_list* _timer)
{
static int cnt = 0;
printk(KERN_INFO "Hello %d times\n", cnt++);
timer.expires = jiffies + 100;
add_timer(&timer);
}
还不能忘了在模块清理函数中进行定时器的注销。模块可能在任何时候被卸载,此时定时器的指针还保留在队列中,如果没有将其注销,定时器和回调函数的指针都会悬空,等执行回调函数的时候就寄了。
del_timer_sync(&timer);
然后编译、安装、卸载模块,观察输出。
6,565,727893501,-;Hello, world 2
6,566,728962333,-;Hello 0 times
6,567,730002324,-;Hello 1 times
6,568,731042331,-;Hello 2 times
6,569,732082310,-;Hello 3 times
6,570,733122305,-;Hello 4 times
6,571,734162700,-;Hello 5 times
6,572,735202459,-;Hello 6 times
6,573,736242368,-;Hello 7 times
6,574,737282300,-;Hello 8 times
6,575,737935404,-;Goodbye, world 2