通过这个章节可以学习如何编写内核模块。
保持内核代码风格的一致性
这是这个教程中没有,但是我认为也比较重要的东西。编写内核代码应该慎重,因为需要对整个系统负责。写出来的内核代码也应尽量”标准化”,不然大家都在提交内核代码,那最后内核里不同模块的代码风格迥异,看起来就乱了,乱了就容易出问题。在Linux内核代码顶级目录下有一个*.clang-format*配置文件,里面描述了用clang-format工具格式化内核代码时所使用的配置。对于习惯用vim的人来说,按照这个插件配置一下就可以方便的用clang-format格式化代码了。
内核模块代码的基本要素
内核模块代码模板
从模仿、copy开始学习写内核代码,
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
MODULE_DESCRIPTION("My kernel module");
MODULE_AUTHOR("Me");
MODULE_LICENSE("GPL");
static int dummy_init(void)
{
pr_debug("Hi\n");
return 0;
}
static void dummy_exit(void)
{
pr_debug("Bye\n");
}
module_init(dummy_init);
module_exit(dummy_exit);
从上面的实例代码可以看到,内核模块代码的基本组成部分
- 以*MODULE_*开头的声明
- 模块描述声明;
- 作者声明(有多个作者就多写几行);
- 协议声明;
- 版本声明;
- 模块注册函数、卸载函数
分别配合module_init和module_exit使用。- 模块加载函数的实现在加载成功时返回0,否则返回其他值;模块卸载函数没有返回值;
- 模块加载函数和卸载函数都声明为static,防止被其他文件/模块调用;
编译内核模块
- 编译前确认编译内核/内核模块代码的工具的版本都符合要求
在linux中的Documentation/Changes有说明对编译工具版本的需求 - 编写makefile
以下是来自Linux Device Driver 3rd edition的makefile示例:
# Comment/uncomment the following line to disable/enable debugging
#DEBUG = y
# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
DEBFLAGS = -O -g # "-O" is needed to expand inlines
else
DEBFLAGS = -O2
endif
CFLAGS += $(DEBFLAGS) -I$(LDDINCDIR)
ifneq ($(KERNELRELEASE),)
# call from kernel build system
obj-m := simple.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) LDDINCDIR=$(PWD)/../include modules
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
depend .depend dep:
$(CC) $(CFLAGS) -M *.c > .depend
ifeq (.depend,$(wildcard .depend))
include .depend
endif
内核模块代码的makefile并不是普通的makefile,而是内核构建系统Kbuild的一部分。这里只对内核模块代码makefile做一些必要的解释,有空再去系统学习Kbuild:
- 需要相应版本的源代码支持
$(MAKE) -C $(KERNELDIR) M=$(PWD) LDDINCDIR=$(PWD)/../include modules
这个命令的含义是:- 先通过
-C
选项进入到内核源码所在目录,读取linux源码顶级目录下的makefile文件; - 再通过
M=$(PWD)
返回当前目录; - 开始构建目标modules
modules目标即obj-m
这个变量包含的所有内核模块;
- 先通过
obj-m
的含义
表示要通过simple.o
文件构建内核模块simple.ko- 如果要构建的内核模块包含多个源文件怎么办
modulename-y = file1.o file2.o
或者modulename-objs = file1.o file2.o
内核模块的安装与卸载
- 安装
insmod modulename.ko
或者modprobe modulename.ko
两者区别在于对于modulename.ko中未定义的引用,modprobe会搜寻其搜索路径中的其他模块是否有定义这些符号,如果有,那么modprobe会先安装好这些模块(这是个递归的过程)。而insmod则是直接安装失败。 - 卸载
rmmode module.ko
或者modprobe -r modulename.ko
只有内核模块的使用计数为0时才允许卸载该内核模块 - 查看已经安装的内核模块
lsmod
最后一列会显示该内核模块的使用计数
或者考虑cat /proc/modules
开机启动时安装内核模块:
boot时会运行/etc/rcS.d/S01kmod这个脚本安装需要在启动时运行的内核模块,
可以通过修改文件/etc/modules-load.d/modules.conf添加需要开机启动安装的内核模块,或者将ko文件放到目录/etc/modules-load.d/里。
内核模块的调试
两种内核错误
- kernel oops
出现kernel oops时表明内核检测到出现错误,但是内核不会崩溃,还可以(勉强)继续运行。 - kernel panic
出现kernel panic后,内核不能继续安全运行。
一些辅助调试的二进制工具
minicom与netconsole
printk
简而言之,printk就是内核里的printf,只不过:
- printk需要指定该日志的级别;
- printk的输出内容需要用dmesg查看,或者查看相关文件(如*/var/log/syslog*);
printk的日志级别:
宏 | 代表的数字 |
---|---|
KERN_EMERG | 0 |
KERN_ALERT | 1 |
KERN_CRIT | 2 |
KERN_ERR | 3 |
KERN_WARNING | 4 |
KERN_NOTICE | 5 |
KERN_INFO | 6 |
KERN_DEBUG | 7 |
近期评论