计算机 · 2021年12月19日 0

Linux Kernel Labs Kernel Modules

通过这个章节可以学习如何编写内核模块。

保持内核代码风格的一致性

这是这个教程中没有,但是我认为也比较重要的东西。编写内核代码应该慎重,因为需要对整个系统负责。写出来的内核代码也应尽量”标准化”,不然大家都在提交内核代码,那最后内核里不同模块的代码风格迥异,看起来就乱了,乱了就容易出问题。在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_*开头的声明
    1. 模块描述声明;
    2. 作者声明(有多个作者就多写几行);
    3. 协议声明;
    4. 版本声明;
  • 模块注册函数、卸载函数
    分别配合module_initmodule_exit使用。
    1. 模块加载函数的实现在加载成功时返回0,否则返回其他值;模块卸载函数没有返回值;
    2. 模块加载函数和卸载函数都声明为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这个命令的含义是:
    1. 先通过-C选项进入到内核源码所在目录,读取linux源码顶级目录下的makefile文件;
    2. 再通过M=$(PWD)返回当前目录;
    3. 开始构建目标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,只不过:

  1. printk需要指定该日志的级别;
  2. printk的输出内容需要用dmesg查看,或者查看相关文件(如*/var/log/syslog*);

printk的日志级别:

代表的数字
KERN_EMERG0
KERN_ALERT1
KERN_CRIT2
KERN_ERR3
KERN_WARNING4
KERN_NOTICE5
KERN_INFO6
KERN_DEBUG7

练习