00 Linux内核模块
Linux内核模块
Linux内核本身非常庞大,包含了很多东西,这带了一些问题:
- 许多功能我们可能并不需要,如果每次都把所有东西一起编译了,一是十分耗时,二是编译出来的镜像太大了
因此,Linux内核引入了“模块”机制,对于一些功能,它本身不直接编译进内核,而是以模块(.ko)的形式存在内核之外,一旦需要这些功能,可以动态将其加载进内核。一旦被加载,他们就和内核中其他部分没有区别
内核模块程序结构
1 |
|
一个Linux内核模块主要由如下几个部分组成:
模块加载函数:当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
模块卸载函数:当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。
模块许可证声明(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到 内核被污染(Kernel Tainted)的警告。
模块参数(可选):模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。
模块导出符号(可选):内核模块可以通过
EXPORT_SYMBOL()导出符号(这里的符号指的是函数或者变量),若导出,其他模块则可以使用本模块中的变量或函数。EXPORT_SYMBOL本质是把对应的符号添加到全局的符号表,这样我们单独编译某个.ko模块时,不必链接库文件也可以使用某函数了,这和应用层开发还是有点区别的
模块作者等信息声明(可选)
__init宏的作用
1 |
- 功能:标记初始化函数,告诉内核“此函数仅在模块加载时运行一次”
- 底层原理:
__init是一个宏,展开后是__attribute__((__section__(".init.text")))- 它将该函数编译到内核的
.init.text段(一个专门存放初始化代码的内存区域) - 模块加载完成后,内核会自动释放
.init.text段的内存(节省空间)
__exit宏的作用
- 功能:标记清理函数,告诉内核“此函数仅在模块卸载时运行”
- 底层原理:
__exit展开为__attribute__((__section__(".exit.text")))- 函数被编译到
.exit.text段,如果是可卸载模块(非内置),则保留;如果是内置驱动(编译进内核),则会被丢弃
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动态扩展
