Linux 驱动开发->模块加载过程
Linux 内核加载模块的过程是一个复杂的系统操作,涉及多个步骤和子系统。内核模块加载的过程使内核能够在运行时动态加载代码(例如设备驱动程序),而无需重新编译或重启内核。模块加载通常由 insmod 或 modprobe 工具触发,模块加载的主要步骤包括从用户空间发出加载请求、模块文件的加载、符号解析、执行模块的初始化等。
内核加载模块的过程。
- 用户空间工具发出请求
insmod:通过手动指定模块文件路径加载模块。
sudo insmod my_module.ko
modprobe:更高级的工具,可以根据模块的名字加载,并处理模块的依赖关系。
sudo modprobe my_module
- 系统调用与内核交互
无论是 insmod 还是 modprobe,它们最终都会调用 init_module 系统调用,向内核发出加载模块的请求。init_module 会接收两个主要参数:
模块二进制数据:这是 .ko 文件的内容,通常由 ELF 格式表示。 模块的选项:可选的模块参数,用于配置模块。
系统调用流程如下:
int init_module(void *module_image, unsigned long len, const char *param_values);
module_image 是模块的二进制文件内容。 内核负责处理该文件内容,并将其加载到内存中。
- 内核处理模块加载请求
内核收到 init_module 系统调用后,通过一系列函数开始加载模块。其主要工作包括:
- a. 分配内存
内核首先为模块分配内存空间,将模块代码、数据段以及符号表映射到合适的内核地址空间。
- b. 解析模块头部
内核解析模块的 ELF 文件头部,检查模块的结构、符号表、依赖等信息。如果模块有其他依赖模块,内核会确保这些依赖模块已经加载。
- c. 符号解析
内核模块可能会引用其他模块或内核中的全局符号。符号解析的关键步骤是:
导入符号:解析模块中使用的符号,并检查这些符号是否已在内核或其他已加载模块中导出(即通过 EXPORT_SYMBOL 导出)。 导出符号:将模块中定义的全局符号导出,供其他模块使用。
- d. 依赖处理
如果模块依赖于其他模块(例如某个网络协议模块依赖于加密模块),内核会确保这些依赖模块已经加载。使用 modprobe 时,它会自动加载依赖模块,而 insmod 不会自动处理依赖。
- 模块初始化
当符号解析完成后,内核会执行模块的初始化过程,这通过模块内的 module_init 宏指定的初始化函数实现。
static int __init my_module_init(void) {
printk(KERN_INFO "My module is loaded.\n");
return 0;
}
module_init(my_module_init);
内核通过 .initcall 段找到模块的初始化函数,然后调用它。这个函数通常负责模块的核心初始化任务,例如:
注册设备驱动程序(如 register_chrdev 或 register_netdev)。 分配硬件资源。 启动必要的内核线程或定时器。
如果初始化函数返回 0,则表明模块初始化成功。否则,加载失败,内核会卸载该模块并释放已分配的资源。
- 模块注册
在模块初始化成功后,内核会将模块注册到内核模块表中,标记其状态为已加载。此时,模块的功能已经可以被内核或其他模块使用。
可以使用 lsmod 查看已加载的模块:
lsmod
- 加载日志
模块加载过程中的日志通过内核日志系统记录,用户可以通过 dmesg 命令查看这些信息。日志中会记录:
模块加载成功或失败的情况。 任何错误或警告信息。
例如,使用 dmesg | grep my_module 来查看特定模块的加载日志。
- 模块卸载
当不再需要该模块时,可以通过 rmmod 或 modprobe -r 命令卸载模块。内核会调用 module_exit 宏指定的退出函数来清理资源并注销模块。(在模块加载时就已经将卸载的函数注册到内核中了)
static void __exit my_module_exit(void) {
printk(KERN_INFO "My module is unloaded.\n");
}
module_exit(my_module_exit);
内存释放:内核会释放与该模块相关的内存,包括其代码段、数据段以及符号表。
注销模块:模块从内核的模块列表中被移除,不能再被使用。
-
模块加载流程总结
用户空间工具发出请求:通过 insmod 或 modprobe 触发加载。
系统调用:init_module 系统调用加载模块。
内存分配:内核为模块分配内存,解析模块头信息。
符号解析:解析模块的外部符号,并检查依赖关系。
执行初始化:调用模块的初始化函数,执行模块注册或资源分配等操作。
模块注册:模块成功加载,注册到内核模块表中。
模块卸载:通过 rmmod 或 modprobe -r 卸载模块,并调用退出函数释放资源。
总结
内核加载模块是一个动态过程,涉及用户空间发出请求、系统调用、内核符号解析、依赖处理、初始化等多个步骤。内核通过这些步骤确保模块在运行时被正确加载,并能根据需要动态添加或移除功能,提高了系统的灵活性。