Home 内核模块概要
Post
Cancel

内核模块概要

内核模块

linux内核模块可以在系统运行期间动态扩展功能,无须重启或重新编译内核。不过内核模块通常要和内核版本匹配,否则可能出现不匹配无法加载。 linux驱动程序最常见的形式就是编译为一个内核模块来加载使用,其他形式,也可以编译进内核或将主要IO映射到用户空间,由用户空间来完成驱动主体部分。 不过,编译为ko模块还是linux驱动最常见的形式。ko文件就是elf可重定位目标文件的内部格式。

EXPORT_SYMBOL

EXPORT_SYMBOL 是内核代码中常见的宏定义,用于所谓的符号导出。因为模块是动态加载的,编译时确定的符号是有限的,如果整个linux内核静态编译,那么 符号的引用在静态链接阶段就可以全部完成,可以不需要EXPORT_SYMBOL机制。而模块是在外部编译的,再链接阶段是需要解决符号引用的问题。手段就是这个 EXPORT_SYMBOL 机制,以便外部模块能够完成链接时的符号引用问题。

该机制的实现主要通过三部分,源文件代码中EXPORT_SYMBOL宏定义;链接脚本链接器部分;使用导出符号部分。对于用户编写驱动模块而言,主要需要使用到第一部分 和第三部分。大致的实现原理是通过几个额外的section来保存这些个导出的符号信息,并储存在内核或模块的对象文件中。详细细节可以参考《深入Linux设备驱动内核机制》。

相关的宏定义包括:

1
2
3
EXPORT_SYMBOL
EXPORT_SYMBOL_GPL
EXPORT_SYMBOL_GPL_FUTURE

生成的符号表文件:.symvers ,里面记录符号地址和符号名对应关系,对于内核的符号表文件,由于编译模块时会进入内核源码目录,所以可以找到,不需要额外操作。 对于用于自定义的导出符号,使用时需要把符号表文件复制到需要调用的模块(使之能找到自定义的导出符号)。适用于在用户有多个模块分开编译,且模块间需要引用 其他自定义模块的导出符号的情况,而且加载时也要注意先后加载顺序,因为有依赖。

查看运行中的内核的所有导出符号
可以查看内核所有符号的导出情况,要注意的是,有些符号是宏定义实现的,所以查找时会出现不完全匹配的情况。

1
cat /proc/kallsyms

模块加载及简要过程

用户通常使用modprobeinsmod工具加载模块。

模块文件通过文件系统接口读入内存,在内核态执行该文件,由于是ko模块,内核会先load_module模块到内核中, 并进而调用关键的sys_init_module函数进行初始化等操作。装载过程类似动态共享库的加载过程,有些符号解析,重定位的工作,不过是在内核环境中,有些差异。

模块在内核中使用 struct module 结构体描述(具体定义在 /include/linux/module.h中)。该结构体较大,有一些比较典型的成员,如链表的组织结构,模块的状态, sysfs相关的属性,导出的符号,模块的加载参数,init/exit回调函数指针(配合模块代码的module_init实现实例化),模块之间的依赖,引用关系属性等。

sys_init_module的初始化操作,会进行配置module结构体,执行module_init回调函数,释放 .init 段等等相关工作。

模块版本控制

内核模块和内核之间的一个主要问题是,二者是独自编译的,如果二者的编译版本有差异,导致模块编译时使用旧版本的符号,而内核升级后,对应的符号发生变化,那么模块就可能 会加载失败,即使能加载也不能确定没有潜在风险。即一个ko文件总是需要基于某个特定的内核源码树来构建。

为此,内核提供了一个简陋的方法。就是CONFIG_MODVERSIONS宏,编译内核时开启该宏,在对应版本上编译的内核模块也要开启该宏定义,这个方式其实是需要双方共同协作的。 否则,模块加载时会被拒绝。该机制的实现原理大致如下:开启该宏定义后,针对导出的符号,在编译后的对象文件中又额外多几个段(section),主要用来保存crc校验信息,针对每一个 导出的符号,都会生成一个crc校验码,并保存在对象文件里面。目标内核的有该符号的crc信息,而待加载的模块文件里面也有该符号的crc信息,加载时会比较二者的crc信息,不一致 就会拒绝加载,典型的就是导出符号的api接口发生了一点变化。该机制可以解决潜在风险的问题,更加安全。不过本质还是要重新编译模块。 不过,如果驱动是以源码形式提供的,就不会有版本控制问题,用户只需要在对应的内核版本上重新编译即可。

查看该选项是否开启:

1
cat /boot/config-`uname -r`  | grep CONFIG_MODVERSIONS

模块信息

在模块的对象文件中,保存有一个 .modinfo 的section段,用来保存模块的各种信息。linux下可以使用命令modinfo来查看一个模块的信息。如查看ssd 的nvme驱动模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ modinfo /lib/modules/4.19.0-23-amd64/kernel/drivers/nvme/host/nvme.ko
filename:       /lib/modules/4.19.0-23-amd64/kernel/drivers/nvme/host/nvme.ko
version:        1.0
license:        GPL
author:         Matthew Wilcox <willy@linux.intel.com>
srcversion:     E8F75AD99D3656EA308DB6F
alias:          pci:v0000106Bd00002003sv*sd*bc*sc*i*
...
...
alias:          pci:v00008086d00000953sv*sd*bc*sc*i*
depends:        nvme-core
retpoline:      Y
intree:         Y
name:           nvme
vermagic:       4.19.0-23-amd64 SMP mod_unload modversions 
sig_id:         PKCS#7
signer:         Debian Secure Boot CA
sig_key:        32:A0:28:7F:84:1A:03:6F:A3:93:C1:E0:65:C4:3A:E6:B2:42:26:43
sig_hashalgo:   sha256
signature:      21:CA:AC:40:EE:07:3B:10:F4:CE:75:71:B8:2A:1C:01:18:E6:E2:EE:
                ...
                ...
                BC:C9:06:5D:50:9A:0E:28:F0:66:90:20:89:2C:54:EC
parm:           use_threaded_interrupts:int
parm:           use_cmb_sqes:use controller's memory buffer for I/O SQes (bool)
parm:           max_host_mem_size_mb:Maximum Host Memory Buffer (HMB) size per controller (in MiB) (uint)
parm:           sgl_threshold:Use SGLs when average request segment size is larger or equal to this size. Use 0 to disable SGLs. (uint)
parm:           io_queue_depth:set io queue depth, should >= 2

其基本原理是将信息保存在对象文件的.modinfo段中。在模块代码中使用时,可以使用宏定义 MODULE_INFO,(定义在/include/linux/module.h中)。一般使用时, 可以直接使用,但更多使用进一步的扩展宏定义封装,不过本质都是调用的该宏定义,常用的扩展封装:

1
2
3
4
5
MODULE_AUTHOR
MODULE_DESCRIPTION
MODULE_LICENSE
MODULE_VERSION
MODULE_ALIAS

只要是通过modinfo命令显示出来的,就使用了该技术,不过并不都是MODULE_INFO宏完成的,有一些是单独指定的,如模块参数。

模块卸载

使用命令 rmmodmodprobe -r

内核会调用 sys_delete_module 函数进行模块卸载,会进行模块查找,依赖关系检查,执行.exit回调函数,空间释放等,过程大致和初始化相反。

模块参数

可以带过模块参数机制,对模块内部代码的一些参数进行特定初始化。可以在模块加载时(如insmod)跟在后面,也可以在运行中通过 /sys/module/ 目录里面查看修改。 使用模块参数需要指定权限。 参考代码

1
2
3
4
5
6
7
8
//module parameter
static int int_para = 123;
module_param(int_para, int, S_IRUGO);   //S_IRUGO == 0444
MODULE_PARM_DESC(int_para,"example for int parameter.\n");

static char *str_para = "initial string";
module_param(str_para, charp, S_IRUGO); //S_IRUGO == 0444
MODULE_PARM_DESC(str_para,"example for string parameter.\n");

使用示例

1
2
3
$ sudo insmod ./simple_module.ko int_para=9999  dyndbg=+pt 
$ cat /sys/module/simple_module/parameters/int_para 
9999

如上,通过加载时参数重新初始化了模块参数,如果要在运行中修改,需要加上写权限

使用模块参数时,如timeout=10,注意=后是不能带空格的

模块相关命令工具

在debian10发现版上,模块相关的操作命令主要在 kmod 包中,可以使用命令 dpkg -L kmod 查看所有安装的工具和文档。

查看系统中的已加载模块和依赖关系

1
2
3
4
5
lsmod
## 或者
cat /proc/modules
## 或者,/sys/module/下一个文件夹对应一个模块
ls /sys/module/

参考代码

安装模块编译环境

环境:debian10 (amd64)

因为驱动模块编译需要和内核版本匹配的源码树或部分编译过的二进制形式,所以安装当前版本内核的一些头文件。主要在makefile中指定内核构建路径。嵌入式arm-linux 环境需要额外指定交叉编译器。

1
sudo apt install linux-headers-`uanme -r`

模块基本框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/timer.h>
#include <linux/jiffies.h>

int __init simple_module_init(void);
void __exit simple_module_exit(void);

//module parameter
static int int_para = 123;
module_param(int_para, int, S_IRUGO);   //S_IRUGO == 0444
MODULE_PARM_DESC(int_para,"example for int parameter.\n");

static char *str_para = "initial string";
module_param(str_para, charp, S_IRUGO); //S_IRUGO == 0444
MODULE_PARM_DESC(str_para,"example for string parameter.\n");


int __init simple_module_init(void)
{
    pr_debug("in %s\n",__func__);
    return 0;
}

void __exit simple_module_exit(void)
{
    pr_debug("in %s\n",__func__);
}

module_init(simple_module_init);
module_exit(simple_module_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("prejoy");
MODULE_DESCRIPTION("this is a simple kernel module templete");
MODULE_VERSION("1.0.0");
MODULE_ALIAS("spmod");

扩展

记录一些模块的进一步的使用细节,包括禁用模块的自动加载,自动加载参数设置,模块间依赖。

禁用内核模块的自动加载 blacklist

模块有在内核源码树内编译为模块的,(menuconfig中可以配置为M,则表示可以编译为模块),也可以在内核源码树外部编译。 内核编译的模块通常会自动加载,即系统引导时自动加载,可以通过一些方式禁用某个某块的自动加载。

安装在 /lib/modules/xxx/kernel/drivers/ 目录下的模块会被自动加载。可以在 /etc/modprobe.d/blacklist.conf 文件中填写如下行来屏蔽模块自动加载

1
blacklist modulename  

自动加载的模块参数设置

默认自动加载的模块是不带参数,可以用一些方法设置参数。在 /etc/modprobe.d/目录下,创建自定义文件 xxx.conf,名称可以任意起。 在文件中添加行

1
options modulename param1=value1 param2=value2

以上行,就表示,为modulename设置参数,param1=value1 param2=value2

如果是自己单独编译的模块
先要让自己的模块自动加载:

  1. 将模块安装到 /lib/modules/xxx/kernel/drivers 目录下。
  2. 然后编辑文件 /etc/modules,将需要自动加载的用户自定义模块名写入该文件。
  3. 最后执行 depmod -a重新分析和记录依赖。

模块依赖

编译时,首先要解决编译的依赖问题,多个模块可以放一起编译,或者使用生成的符号文件。 运行前,可以使用depmod命令分析并自动记录。

另外,加载模块时,使用modprobe可以自动加载所有依赖的模块,这点比insmod更方便。

模块依赖关系查看

在运行depmod后,生成模块依赖的一个记录文件,可以方便查看依赖关系。

1
2
depmod -a
cat /lib/modules/内核版本/modules.dep
This post is licensed under CC BY 4.0 by the author.

VSCode+GDB远程调试

NUMA内存模型