00 Linux内核模块
Linux内核模块
Linux内核本身非常庞大,包含了很多东西,这带了一些问题:
- 许多功能我们可能并不需要,如果每次都把所有东西一起编译了,一是十分耗时,二是编译出来的镜像太大了
因此,Linux内核引入了“模块”机制,对于一些功能,它本身不直接编译进内核,而是以模块(.ko
)的形式存在内核之外,一旦需要这些功能,可以动态将其加载进内核。一旦被加载,他们就和内核中其他部分没有区别
内核模块程序结构
1 |
|
一个Linux内核模块主要由如下几个部分组成:
- 模块加载函数:当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
- 模块卸载函数:当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功 能。
- 模块许可证声明 许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到 内核被污染(Kernel Tainted)的警告。
- 模块参数(可选):模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。
- 模块导出符号(可选):内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块则可以使用本模块中的 变量或函数。
- 模块作者等信息声明(可选)
__init宏的作用
1 |
此宏定义会把被修饰的函数放到.init.text
段内,且在.initcall.init
中还保存了一份函数指针
在内核初始化时,会通过这些函数指针调用这些__init函数,并且在初始化完成后,释放init区段(包括.init.text、.initcall.init等)的内存
module_init详解
对于内核的各个功能,他们既可以被编译成模块的形式(.ko
),也可以直接被编译进内核(built-in)。而module_init
和 module_exit
等 API 既用于模块,也用于内置组件,但它们的实际行为会因编译方式而不同。
module_init
是一个宏,它的行为取决于代码是编译为模块还是内置:
对于模块(
\*.ko
):1
2
3
4
5
int init_module(void) __attribute__((alias(#initfn)));- 将初始化函数
initfn
绑定到init_module
,供insmod
调用
- 将初始化函数
对于内置功能:
1
2
3- 将初始化函数指针放入
.initcall6.init
节区,内核在链接阶段会收集所有标记为此节区的函数指针,形成一个初始化函数数组 - 内核启动时(
start_kernel()
):会按优先级顺序(如.initcall1.init
→.initcall7.init
)依次执行这些函数
- 将初始化函数指针放入
1 | //init/main.c |
module_init
会把该模块放到优先级6,如果要控制驱动的加载顺序,就得换个宏,放到别的优先级里面!
模块/内置的选择
控制某个功能到底编译成模块还是直接内置到内核里面,通过将待生成的模块添加到obj-m
或者obj-y
来控制(他们再内核的顶层Makfile创建):
1 | # 编译为*.ko |
-C $(KERNELDIR)
:进入到内核的源码目录进行编译,解析顶层的Makefile,编译完成时返回M=$(CURRENT_PATH)
:表示在内核中需要被编译的目录,在示例中也就是当前目录modules
:内核的编译目标,表示只编译模块(对应obj-m
指定的文件)而忽略obj-y
指定的文件
1 | # 直接链接进内核 |
make kernel_builtin
其实不会直接得到新的Linux内核镜像,只会得到my_driver.o
,需要在内核drivers/
下的随便某个Makefile加入obj-y += xxxx/my_driver.o
,然后重新编译整个内核
这个其实不是标准做法,标准做法是结合Kconfig,并且要把驱动放到内核源码路径下,而不是外部
参考链接:
内核模块的makefile写法
1.我们在看到内核模块的Makefile时,会发现一个奇怪的东西:
1 | obj-m := my_driver.o |
不管是编译成模块还是内置到内核内,假设该模块依赖的是my_driver.o
,但是makefile却没有my_driver.o
的生成规则,这是因为makefile的隐晦规则预定义了.o
文件的依赖为同名的.c
文件
但这仅适合单个文件,如果要链接多文件的话,需要用下面的方法
1 | obj-m := complex_module.o |
- 多文件或非默认命名时,需通过
<module>-objs
或者<module>-y
显式声明依赖
2.有的时候会发现makefile还会包含一个目录
1 | obj-y += st_lsm6dsox/ |
这其实是递归编译,如果target是目录的话,会自动执行目录里makefile的内容
加载模块原理
我们通常使用用户层程序insmod
或者modprobe
将内核模块加载进内核,现在我们来看一下它的底层原理:
对于模块的动态加载,主要起作用的是sys_init_module
这个系统调用,其调用链如下:
1 | 用户命令(insmod / modprobe) |
在系统调用中,内核首先通过load_module
解析一个模块,并创建一个模块实例
1 | struct module { |
接着就是要初始化该实例,其中最重要的成员变量是init
,它指向读入模块的ELF文件的init_module
这个符号。而这个符号,在模块被编译的时候,被替换成用module_init()
宏注册的那个函数了
之所以要替换,是因为内核把模块加载函数名写死成
init_module
了,而具体的模块代码里,可能不想用这个函数名来命名模块加载函数(比如上面用的是mydriver_init
)
符号解析
之前有这样一个问题:模块作为单独编译的一个东西,为什么能直接使用Linux内核提供的其他功能
我们知道各个函数、变量的地址其实都存在ELF文件的符号表里面,只要知道了某个符号的地址,就可以用它了。而内核维护了一个全局的符号表struct kernel_symbol __start___ksymtab
,包含所有导出的函数和变量(如 printk
, kmalloc
)
注册的模块可以动态的导出变量/函数到内核全局的符号表,也可以使用该符号表中的别的变量/函数。从而使得单独编译的模块可以使用内核已有的功能以及对Linux动态扩展