Linux内核模块

Linux内核本身非常庞大,包含了很多东西,这带了一些问题:

  • 许多功能我们可能并不需要,如果每次都把所有东西一起编译了,一是十分耗时,二是编译出来的镜像太大了

因此,Linux内核引入了“模块”机制,对于一些功能,它本身不直接编译进内核,而是以模块(.ko)的形式存在内核之外,一旦需要这些功能,可以动态将其加载进内核。一旦被加载,他们就和内核中其他部分没有区别

内核模块程序结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <linux/module.h>
#include <linux/fs.h>

static int __init mydriver_init(void) {
printk(KERN_INFO "Driver loaded\n");
return 0;
}

static void __exit mydriver_exit(void) {
printk(KERN_INFO "Driver unloaded\n");
}

module_init(mydriver_init);//注册模块加载函数
module_exit(mydriver_exit);//注册模块卸载函数
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

一个Linux内核模块主要由如下几个部分组成:

  • 模块加载函数:当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
  • 模块卸载函数:当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功 能。
  • 模块许可证声明 许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到 内核被污染(Kernel Tainted)的警告。
  • 模块参数(可选):模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。
  • 模块导出符号(可选):内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块则可以使用本模块中的 变量或函数。
  • 模块作者等信息声明(可选)

__init宏的作用

1
#define __init		__section(.init.text) __cold notrace

此宏定义会把被修饰的函数放到.init.text段内,且在.initcall.init中还保存了一份函数指针

在内核初始化时,会通过这些函数指针调用这些__init函数,并且在初始化完成后,释放init区段(包括.init.text、.initcall.init等)的内存

module_init详解

对于内核的各个功能,他们既可以被编译成模块的形式(.ko),也可以直接被编译进内核(built-in)。而module_initmodule_exit 等 API 既用于模块,也用于内置组件,但它们的实际行为会因编译方式而不同。

module_init 是一个宏,它的行为取决于代码是编译为模块还是内置:

  • 对于模块(\*.ko):

    1
    2
    3
    4
    5
    #define module_init(initfn) \
    static inline initcall_t __inittest(void) \
    { return initfn; } \
    // 在符号表创建个叫init_module的符号,指向initfn的地址
    int init_module(void) __attribute__((alias(#initfn)));
    • 将初始化函数 initfn 绑定到 init_module,供 insmod 调用
  • 对于内置功能:

    1
    2
    3
    #define module_init(initfn) \
    static initcall_t __initcall_##fn __used \
    __attribute__((__section__(".initcall6.init"))) = initfn
    • 将初始化函数指针放入 .initcall6.init 节区,内核在链接阶段会收集所有标记为此节区的函数指针,形成一个初始化函数数组
    • 内核启动时(start_kernel()):会按优先级顺序(如 .initcall1.init.initcall7.init)依次执行这些函数
1
2
3
4
5
6
7
8
9
10
11
12
13
//init/main.c

extern initcall_t __initcall_start[]; // .initcall0.init 的起始地址
extern initcall_t __initcall6_start[]; // .initcall6.init 的起始地址

static void __init do_initcalls(void) {
initcall_t *fn;

// 遍历所有优先级段
for (fn = __initcall_start; fn < __initcall6_start; fn++) {
(*fn)(); // 执行初始化函数
}
}
  • module_init会把该模块放到优先级6,如果要控制驱动的加载顺序,就得换个宏,放到别的优先级里面!

模块/内置的选择

控制某个功能到底编译成模块还是直接内置到内核里面,通过将待生成的模块添加到obj-m或者obj-y来控制(他们再内核的顶层Makfile创建):

1
2
3
4
# 编译为*.ko
obj-m := my_driver.o
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
  • -C $(KERNELDIR):进入到内核的源码目录进行编译,解析顶层的Makefile,编译完成时返回
  • M=$(CURRENT_PATH):表示在内核中需要被编译的目录,在示例中也就是当前目录
  • modules:内核的编译目标,表示只编译模块(对应 obj-m 指定的文件)而忽略obj-y指定的文件
1
2
3
4
# 直接链接进内核
obj-y := my_driver.o
kernel_builtin:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH)

make kernel_builtin其实不会直接得到新的Linux内核镜像,只会得到my_driver.o,需要在内核drivers/下的随便某个Makefile加入obj-y += xxxx/my_driver.o,然后重新编译整个内核

这个其实不是标准做法,标准做法是结合Kconfig,并且要把驱动放到内核源码路径下,而不是外部

参考链接:

内核模块的makefile写法

1.我们在看到内核模块的Makefile时,会发现一个奇怪的东西:

1
2
obj-m := my_driver.o
obj-y := my_driver.o

不管是编译成模块还是内置到内核内,假设该模块依赖的是my_driver.o,但是makefile却没有my_driver.o的生成规则,这是因为makefile的隐晦规则预定义了.o文件的依赖为同名的.c文件

但这仅适合单个文件,如果要链接多文件的话,需要用下面的方法

1
2
obj-m := complex_module.o
complex_module-objs := file1.o file2.o # complex_module-y是一样的
  • 多文件或非默认命名时,需通过 <module>-objs或者<module>-y 显式声明依赖

2.有的时候会发现makefile还会包含一个目录

1
2
obj-y += st_lsm6dsox/
obj-y += st_lsm6dsrx/

这其实是递归编译,如果target是目录的话,会自动执行目录里makefile的内容

加载模块原理

我们通常使用用户层程序insmod或者modprobe将内核模块加载进内核,现在我们来看一下它的底层原理:

对于模块的动态加载,主要起作用的是sys_init_module这个系统调用,其调用链如下:

1
2
3
4
5
6
7
8
9
用户命令(insmod / modprobe)

sys_init_module (系统调用)

load_module (解析 ELF、分配内存、解析符号)

do_init_module (调用 mod->init)

module_init 注册的函数(如 my_init)

在系统调用中,内核首先通过load_module解析一个模块,并创建一个模块实例

1
2
3
4
5
6
7
8
9
10
11
struct module {
enum module_state state; // 模块状态(LIVE, COMING, GOING)
struct list_head list; // 内核模块链表节点
char name[MODULE_NAME_LEN]; // 模块名(如 "mydriver")
const struct kernel_symbol *syms; // 模块导出的符号表
unsigned int num_syms; // 符号数量
int (*init)(void); // 初始化函数(即 module_init 注册的函数)
void *module_init; // 初始化代码段地址
void *module_core; // 核心代码段地址
// ...
};

接着就是要初始化该实例,其中最重要的成员变量是init,它指向读入模块的ELF文件的init_module这个符号。而这个符号,在模块被编译的时候,被替换成用module_init()宏注册的那个函数了

之所以要替换,是因为内核把模块加载函数名写死成init_module了,而具体的模块代码里,可能不想用这个函数名来命名模块加载函数(比如上面用的是mydriver_init)

符号解析

之前有这样一个问题:模块作为单独编译的一个东西,为什么能直接使用Linux内核提供的其他功能

我们知道各个函数、变量的地址其实都存在ELF文件的符号表里面,只要知道了某个符号的地址,就可以用它了。而内核维护了一个全局的符号表struct kernel_symbol __start___ksymtab,包含所有导出的函数和变量(如 printk, kmalloc

注册的模块可以动态的导出变量/函数到内核全局的符号表,也可以使用该符号表中的别的变量/函数。从而使得单独编译的模块可以使用内核已有的功能以及对Linux动态扩展