本文最后更新于 26 天前,其中的信息可能已经有所发展或是发生改变。
模块入口和出口注册API
在各个linux子模块中经常可以看到的module_init和module_exit两个宏,这分别注册了模块的初始化和结束部分需要调用的函数
- module_init(demo_init):
module_init宏的作用是告诉内核,当这个模块被加载时,应该执行名为demo_init的函数。demo_init函数通常包含模块启动时需要执行的所有初始化代码,比如分配资源、注册设备驱动、创建内核线程等。如果demo_init函数成功执行并返回0,模块就会成功加载;如果返回错误码,模块加载就会失败 - module_exit(demo_exit):
module_exit宏的作用是告诉内核,当这个模块被卸载时,应该执行名为demo_exit的函数。demo_exit函数负责执行模块退出时所需的所有清理工作,例如释放在demo_init中分配的资源、注销设备驱动、停止内核线程等。确保所有资源都被正确释放对于维持系统稳定性至关重要
从下文中可以看到module_init和module_exit宏有着两种不同的实现方式,使用了MODULE编译参数来进行区分,其主要的作用是控制最终编译产物是.KO文件还是完整的Linux镜像。不管是哪一种情况,二者最终展开都是一系列宏的嵌套,宏的最终展开是的inline函数和GCC编译参数。也就是说这一系列的逻辑实际上会在程序的编译阶段完成
module_init原理分析
查看Linux 5.4源码可以看到module_init宏对应源代码,可以看到module_init对应着两种不同的实现。MODULE 宏本身并不是直接定义某个值或函数,而是一个编译时开关。编译器通常会通过命令行参数(例如 -DMODULE)来定义 MODULE 宏
//linux-5.4/include/linux/module.h
#ifndef MODULE
#define module_init(x) __initcall(x);
#else
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
#endif
MODULE宏开启时的逻辑 源代码://linux-5.4/include/linux/module.h #define module_init(initfn) \ static inline initcall_t __maybe_unused __inittest(void) \ { return initfn; } \ int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));核心思想: 当文件被编译成内核模块也就是.KO文件时,会有明确的init_module函数作为加载时的入口点。这段宏代码的目的是将用户提供的初始化函数 (initfn) 设置为内核模块的官方入口点 (init_module)。它通过GCC的alias属性使得init_module直接指向initfn的代码MODULE宏关闭(默认情况)时的逻辑 源代码: 通过查看linux 5.4的对应源代码,展开各个宏可以看到最终的调用//linux-5.4/include/linux/init.h #define ___define_initcall(fn, id, __sec) \ static initcall_t __initcall_##fn##id __used \ __attribute__((__section__(#__sec ".init"))) = fn; #define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id) #define device_initcall(fn) __define_initcall(fn, 6) #define __initcall(fn) device_initcall(fn) #define module_init(x) __initcall(x);核心思想: 主要的目的是修改最终编译生成的ELF二进制文件中的部分,将需要执行的函数指针放在某个特定的位置___define_initcall宏的目的是获取一个函数fn,并创建一个指向该函数的静态函数指针。这个函数指针会被赋予一个基于fn和id的唯一名称,并被标记为“已使用”(__used),以防止被编译器优化掉。最关键的是,这个函数指针会被放置在一个特殊的内存段中,段名由__sec参数和.init后缀构成(例如".initcall0.init"、".initcall1.init"等)
module_exit原理分析
查看Linux 5.4源码可以看到module_exit宏对应源代码,可以看到module_exit对应着两种不同的实现。MODULE 宏本身并不是直接定义某个值或函数,而是一个编译时开关。编译器通常会通过命令行参数(例如 -DMODULE)来定义 MODULE 宏
//linux-5.4/include/linux/module.h
#ifndef MODULE
#define module_exit(x) __exitcall(x);
#else
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
#endif
- MODULE宏开启时的逻辑 源代码:
//linux-5.4/include/linux/module.h #define module_exit(exitfn) \ static inline exitcall_t __maybe_unused __exittest(void) \ { return exitfn; } \ void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));核心思想: 当文件被编译成内核模块也就是.KO文件时,会有明确的cleanup_module函数作为卸载时的入口点。将cleanup_module函数设置为exitfn函数的一个别名。#exitfn使用预处理器的字符串化操作符将exitfn转换为字符串字面量。因此,当内核调用cleanup_module时,实际上会直接执行开发者提供的exitfn函数的代码 - MODULE宏关闭时的逻辑 源代码:
//linux-5.4/include/linux/module.h #define __exit_call __used __section(.exitcall.exit) #define __exitcall(fn) \ static exitcall_t __exitcall_##fn __exit_call = fn #define module_exit(x) __exitcall(x)核心思想: 综上所述,__exitcall宏的目的是提供一种机制,用于注册在程序或模块退出时需要调用的函数。它通过创建一个静态的、具有唯一名称的函数指针,并将其初始化为指定的函数地址。通过.exitcall.exit段属性,所有这样定义的函数指针会被链接器收集到内存中的一个特定区域。之后,在退出流程中,系统可以遍历这个区域,并调用所有注册的退出函数。