前言
module_init、module_exit是Linux驱动框架里用于设置驱动初始化函数/卸载函数,在Linux里驱动模式分为两种:
第一种是以模块形式也就是.ko形式
第二种是直接被编译进内核,成为内核的一部分
两者之间的区别在于.ko的模块可以被动态加载和卸载就像动态库一样,而另外一个则是像静态库一样是内核的一部分。
静态模式下的驱动初始化/卸载函数是由内核在启动时主动调用,而动态模式下则是通过装载工具(如insmod/modprobe)让内核被动去调用。
静态与动态模块
静态模式
在静态模式下Linux内核采用段初始化的方式,简单的来说就是将驱动的init/exit函数放入到指定的Link段里,在启动或退出时依次调用段里的函数。
动态模式
动态模式下驱动是.ko文件,是一种特殊的文件格式,不是ELF也不是lib文件,但它可以理解为是一个特殊的lib文件,它是由内核解析并加载的,通常是由insmod/modprobe通过sys_init_module 系统调用来完成驱动加载,内核也提供了API:request_module来方便开发者们动态式的加载驱动与卸载驱动。
module_init实现原理
module_init函数定义在kernel/include/linux/module.h里,它是一个宏函数,其原型如下:
Tips
它具有两种定义,通过宏判断来决定使用哪种定义
#ifndef MODULE
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
#else /* MODULE */
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
通过上面的定义可以发现它首先判断是否存在MODULE这个宏,这个宏是由内核的KBUILD系统来传递的,只有在模块化构建时才会被定义,如果是直接构建进内核的情况下它是不会被定义出来的,上面的表达式是如果MODULE宏没有被定义则使用第一个宏定义,否则第二个。
先说第一个:
#define module_init(x) __initcall(x);
它内部调用了__initcall宏来完成这个功能,__initcall是一个注册函数,属于内核较底层的功能,它的定义在kernel/include/linux/init/h
#define __initcall(fn) device_initcall(fn)
而device_initcall内部调用的是__define_initcall
#define define_initcall(fn) __define_initcall(fn, 6)
而__define_initcall内部调用的是__define_initcall(函数前缀多了一个_),它定义如下:
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
___define_initcall的定义如下:
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
Tips
linux内核设计采用分层思想所以接口一层套一层,外层都是face interface,用于不同层之间交互的。
__define_initcall会先定义一个静态函数指针的变量,名为__initcall_##fn##id,使用了__used属性来修饰它,因为编译内核使用的是最高等级的检查条件,任何警告都会被视为错误,而这个变量不会被内核显示调用,编译器会报变量定义但未被使用的警告,__used是告诉编译器不要对这个变量报出这样的警告。
最后使用__attribute__((__section__(#__sec ".init")))属性将这个变量放入指定段最后由内核在初始化时通过initcall_t指向这个段的首地址依次调用就可以了。
initcall_t的定义如下:
typedef int (*initcall_t)(void);
id是它的优先级,内核会根据不同的优先级来顺序初始化每个段,也就是说不止一个段,你可以在init.h文件里找到它的定义:
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
Tips
sync意味着这个初始化段是同步的,在初始化时不能被打断,只能依次初始化
非sync意味着这个段初始化时采用的是异步机制,它们可以同时进行初始化,采用内核线程的方式初始化
通过上述分析,我们可以明白module_init的初始化优先级为6,传递给__sec参数是.initcall##id,最终它们会被编译器拼接#__sec ".init",所以最终放入的段是.initcall6.init
那么内核是如何初始化它们的呢?
在内核的"kernel/include/asm-generic/vmlinux.lds.h"这个文件中定义了给ld脚本使用的信息,这个文件只能给GCC LD工具使用,它会在link脚本被包含并执行,在大约782行可以看到如下定义:
#define INIT_CALLS \
__initcall_start = .; \
KEEP(*(.initcallearly.init)) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
__initcall_end = .;
内核使用INIT_CALLS_LEVEL来定义段,同时使用__initcall_start、__initcall_end变量来指向段的开始与结束。
Tips
在GCC LD里.代表当前地址
KEEP代表这个段即便不被显示使用仍然保留,防止被优化
INIT_CALLS_LEVEL定义如下:
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
__initcall##level##s_start = .; \
KEEP(*(.initcall##level##s.init)) \
它其实就说定义两个段并用一个变量保留它们的首地址
在内核kernel/init/main.c里大约950行可以看到在C语言里将它们使用extern声明出来了:
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall8_start[];
extern initcall_entry_t __initcall_end[];
这里的initcall_entry_t类型是一个int,下面是它的定义:
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
typedef int initcall_entry_t;
#else
typedef initcall_t initcall_entry_t;
如果使用了CONFIG_HAVE_ARCH_PREL32_RELOCATIONS则为int类型, PREL32是一个地址重定向功能,通常,内核里的模块都是绝对地址,如果不是在这个地址上面内核就会出现问题,当使用PREL32时它会使用相对地址而非绝对地址,这样可以有效提升灵活性,通常它的定义一般是initcall_t。
sync的段被定义在987行
extern initcall_entry_t __initcall0s_start[];
extern initcall_entry_t __initcall1s_start[];
extern initcall_entry_t __initcall2s_start[];
extern initcall_entry_t __initcall3s_start[];
extern initcall_entry_t __initcall4s_start[];
extern initcall_entry_t __initcall5s_start[];
extern initcall_entry_t __initcall6s_start[];
extern initcall_entry_t __initcall7s_start[];
extern initcall_entry_t __initcall8s_start[];
Linux采用initcall_entry_t的表来存储它们:
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
这里的__initdata是将initcall_levels放入.init.data段里,这个段里的所有数据会在内核初始化完成之后被清空释放掉,在这个段里的变量意味着仅在初始化阶段有效。
内核会依次调用do_initcall_level来初始化每个level
static void __init do_initcall_level(int level) {
#ifdef CONFIG_INITCALL_ASYNC
if (initcall_nr_workers)
if (do_initcall_level_threaded(level) == 0)
return;
#endif
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
Tips
__init会将函数放入.init.txt中,在内核初始化结束之后会将这块区域初始化并释放掉,节省内存
从上面的代码可以看出它会去判断是否开启CONFIG_INITCALL_ASYNC宏,如果开启调用do_initcall_level_threaded去初始化initcall_level, 以下代码摘自do_initcall_level_threaded
for (fn = initcall_levels[level]; fn < initcall_sync_levels[level];
fn++, i++) {
iwork = &iworks[i];
iwork->call = initcall_from_entry(fn);
kthread_init_work(&iwork->work, initcall_work_func);
initcall_queue_work(&initcall_workers[w], iwork);
if (++w >= initcall_nr_workers)
w = 0;
}
这意味着每个驱动初始化函数可以同时被执行,虽然也有线程创建先后顺序,但是从时间片上来看就好像是同时进行的,所以这也是为什么内核在启动时日志有时是乱序的。
在来看看同步的,也就是sync的,它是依次调用的:
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
从上面可以发现它是判断fn<下一个level地址+1,将下一个地址的位置作为end这样的方法可以省去在声明一个end变量,结尾地址使用的是__initcall_end,可以在initcall_levels的声明里看到。
从代码可以分析出,是先创建异步初始化线程,然后在依次执行同步初始化段。
至此初始化就结束了。
说完了静态构建的,在来说说动态构建的,静态构建的它的定义如下:
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
首先先说它第一部分:它首先定义了一个__inittest函数,返回类型是initcall_t,使用__maybe_unused属性表明这个函数可能不会被显示调用,不要报警告,你可以看到它是一个内联函数,但是又没有使用它,仅仅只是定义了它,在C语言里内联函数不会像普通函数那样单独分配一块内存地址存放它们,这点非常重要,如果你定义了,但不去使用它们编译器会将它们直接优化掉,但是在语法检查、语义分析阶段不会被忽略掉,Linux是利用它来做语法检查,检查传递进来的initfn是否是initcall_t类型的,如果不是在编译初期就会报错,与它的内联函数名称一样叫init test。
第二部分它声明了一个函数变量init_module,仅仅是声明了它,通过__copy(initfn)属性将initfn的所有构建属性copy给了init_module,随后又使用alias将它指向initfn,这里解释一下copy属性,这是GCC9.1版本以上引进的属性,它的作用是将一个函数的所有__attribute__属性原封不动的copy过来,它只能用于copy函数,你可以在GNU官网找到关于它的说明:
官网(GNU)

中文翻译:
copy (函数)
copy 属性将声明函数的属性集合应用于应用此属性的函数声明。该属性旨在用于定义别名或函数解析器的库,这些库期望指定与其目标相同的属性集。copy 属性可以用于函数、变量或类型。然而,应用属性的符号类型(函数或变量)必须与参数引用的符号类型匹配。copy 属性仅复制语法和语义属性,但不复制影响符号链接或可见性的属性,例如别名、可见性或弱属性。已弃用的和 target_clones 属性也不会被复制。参见通用类型属性和通用变量属性。
例如,下面的 StrongAlias 宏利用别名和 copy 属性为具有 alloc_size、malloc 和 nothrow 属性的函数 allocate 定义了一个名为 alloc 的别名。借助 typeof 操作符,别名与目标函数具有相同的类型。由于 copy 属性,别名也共享与目标相同的属性。
#define StrongAlias(TargetFunc, AliasDecl) \
extern __typeof__ (TargetFunc) AliasDecl \
__attribute__ ((alias (#TargetFunc), copy (TargetFunc)));
extern __attribute__ ((alloc_size (1), malloc, nothrow))
void* allocate (size_t);
StrongAlias (allocate, alloc);
Tips
GCC有许多非常棒的特性,我认为语言是灵魂而编译器是被灵魂注入的工具,我们要善于利用它的特性来提升我们的开发效率与程序性能,例如LTO优化等等。
这里来解释一下为什么定义了别名还需要使用copy属性,别名不应该直接指向这个函数吗?这里涉及到一个GCC优化的原理,当A函数是B函数的别名时它不会继承B函数的所有属性,在编译阶段属性与一个函数绑定,但不会与另外一个函数共享,即便这个函数变量是别名,那么此时B变量有malloc属性,当调用B函数时GCC会检查这个B函数传递进来的指针参数是否为NULL,如果是会报警告错误,但如果使用B函数的别名A函数来访问时虽然会访问到B函数,但是A函数没有Malloc属性,它不会去检查传递进来的指针是否为空。
但是函数内部优化是不受别名影响的。
最后init_module这个函数别名变量就会被定义出来,它是全局的,所以它具备export属性(不需要使用extern来显示导出),在符号表内可见。
当编译完成之后它就是一个.ko文件,那么就来说说insmod是如何加载它的吧,虽然后缀是.ko文件实则上它是一种特殊的elf文件格式,insmod源码可以在busybox项目里找到insmod.c,我们直接看关键部分:
image_size = INT_MAX - 4095;
mmaped = 0;
image = try_to_mmap_module(filename, &image_size);
if (image) {
mmaped = 1;
} else {
errno = ENOMEM; /* may be changed by e.g. open errors below */
image = xmalloc_open_zipped_read_close(filename, &image_size);
if (!image)
return -errno;
}
errno = 0;
// 这里是关键,调用系统内核功能
init_module(image, image_size, options);
首先它分配了一个大约2G-4096(减去4096是为了防止超出内存最大限制)的内存(通过这点也可以确认内核最大可以加载2G的模块),然后使用try_to_mmap_module函数将模块映射到内存里以便内核能够对它进行访问,最后通过init_module来调用syscall_init_module,以下是它的定义:
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
init_module函数里内核会在内核里分配一块内存,将共享内存里的文件拷贝到内核空间里,将驱动变为内核的一部分。
可以在init_module函数内部看到它调用copy_module_from_user函数来进行copy,并且在copy时候会对模块进行elf文件解析,并将信息放入info变量里
struct load_info info = { };
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
然后通过load_module来加载函数
return load_module(&info, uargs, 0);
在进入内核后,内核会去解析它的ELF文件并遍历寻找init_module然后调用它,以下是ELF文件的格式组成:

在进入内核之后内核会先对ELF文件格式进行检查, 确保它是一个正确的elf文件。
err = elf_header_check(info);
if (err)
goto free_copy;
err = setup_load_info(info, flags);
if (err)
goto free_copy;
if (blacklisted(info->name)) {
err = -EPERM;
goto free_copy;
}
err = module_sig_check(info, flags);
if (err)
goto free_copy;
err = rewrite_section_headers(info, flags);
if (err)
goto free_copy;
/* Check module struct version now, before we try to use module. */
if (!check_modstruct_version(info, info->mod)) {
err = -ENOEXEC;
goto free_copy;
}
做完检查之后会调用layout_and_allocate在module区域分配一块内存,这块区域使用红黑树链表维护
/* Figure out module layout, and allocate all the memory. */
mod = layout_and_allocate(info, flags);
if (IS_ERR(mod)) {
err = PTR_ERR(mod);
goto free_copy;
}
然后去查找符号段,在这个过程中会解析所有符号,同时将init_module也解析过来,将符号信息解析到mod变量里
err = find_module_sections(mod, info);
if (err)
goto free_unload;
调用setup_modinfo将模块添加到内核符号表syms
Tips
内核syms表保存了内核所有符号的信息,内核可以借助kallsyms访问它们
/* Set up MODINFO_ATTR fields */
setup_modinfo(mod, info);
/* Fix up syms, so that st_value is a pointer to location. */
err = simplify_symbols(mod, info);
它还会去copy用户空间传递进来的参数,供使用module_param传递参数使用:
mod->args = strndup_user(uargs, ~0UL >> 1);
最后调用do_init_module去调用内部的init_module这个符号,就完成了内核运行时加载模块的工作。
return do_init_module(mod);
在find_module_sections阶段已经完成对符号的解析,mod里init变量指向init_module,在do_init_module会直接调用do_one_initcall来执行它:
/* Start the module */
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
if (ret < 0) {
goto fail_free_freeinit;
}
简单来说内核会去遍历section/符号段找到init_module的位置并执行它,对于内核来说,模块就是一个使用ELF文件格式的动态库。

通过st_name来获取符号名字,最后通过sh_offset找到它的偏移然后使用call跳转过去执行它。
module_exit
说完module_init之后module_exit就非常容易理解了,静态模式下它的定义如下:
#define module_exit(x) __exitcall(x);
动态模式下它的定义如下:
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
与module_init一样的方式,只是一个是初始化一个是退出,原理是一样的。
先拆开静态模式下__exitcall的定义:
#define __exitcall(fn) \
static exitcall_t __exitcall_##fn __exit_call = fn
与init不同的是它没有level,它使用了__exit_call属性,属性定义如下
#define __exit_call __used __section(.exitcall.exit)
它会将其放入.exitcall.exit段里,module_exit方法就比较直接了,就是把这些话函数放入exit段里内核结束时依次执行,在vmlinux.lds.h文件可以看到它在link脚本里的定义:
#define EXIT_CALL \
*(.exitcall.exit)
#define DISCARDS \
/DISCARD/ : { \
EXIT_TEXT \
EXIT_DATA \
EXIT_CALL \
*(.discard) \
*(.discard.*) \
}
它定义了一个DISCARD段,段的布局是EXIT_TEXT、EXIT_DATA、EXIT_CALL…
值得注意的是它使用了//来定义这个段,这样的定义方法会告诉LD,所有被Link到这个段里的内容都会被丢弃,也就意味着exit这个函数在链接阶段就已经被丢弃了,对于内核来说静态模式下它是内核的一部分,所以不存在exit函数,它应随着内核一起结束,内核会在关机或者重启时执行内存处理操作,这一切不需要模块去关心,因为它已经是内核的一部分了。
这里需要值得注意的一点是动态模式下,它不使用vmlinux.lds.h里的脚本,所以这些段不会被丢弃。
接下来我们来看看动态模式下的module_exit,它的定义原理在module_init已经说过了,这里就不在叙述了,在卸载模块时候我们通常使用rmmod命令, rmmod卸载时候调用的是delete_modulesyscall,如果想追踪可以通过strace命令跟一下,在kernel/module.c大约955行可以看到它的定义:
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
unsigned int, flags)
{
struct module *mod;
char name[MODULE_NAME_LEN];
int ret, forced = 0;
if (!capable(CAP_SYS_MODULE) || modules_disabled)
return -EPERM;
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
return -EFAULT;
name[MODULE_NAME_LEN-1] = '\0';
audit_log_kern_module(name);
if (mutex_lock_interruptible(&module_mutex) != 0)
return -EINTR;
mod = find_module(name);
if (!mod) {
ret = -ENOENT;
goto out;
}
if (!list_empty(&mod->source_list)) {
/* Other modules depend on us: get rid of them first. */
ret = -EWOULDBLOCK;
goto out;
}
/* Doing init or already dying? */
if (mod->state != MODULE_STATE_LIVE) {
/* FIXME: if (force), slam module count damn the torpedoes */
pr_debug("%s already dying\n", mod->name);
ret = -EBUSY;
goto out;
}
/* If it has an init func, it must have an exit func to unload */
if (mod->init && !mod->exit) {
forced = try_force_unload(flags);
if (!forced) {
/* This module can't be removed */
ret = -EBUSY;
goto out;
}
}
/* Stop the machine so refcounts can't move and disable module. */
ret = try_stop_module(mod, flags, &forced);
if (ret != 0)
goto out;
mutex_unlock(&module_mutex);
/* Final destruction now no one is using it. */
if (mod->exit != NULL)
mod->exit();
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_GOING, mod);
klp_module_going(mod);
ftrace_release_mod(mod);
async_synchronize_full();
/* Store the name of the last unloaded module for diagnostic purposes */
strlcpy(last_unloaded_module, mod->name, sizeof(last_unloaded_module));
free_module(mod);
/* someone could wait for the module in add_unformed_module() */
wake_up_all(&module_wq);
return 0;
out:
mutex_unlock(&module_mutex);
return ret;
}
在说module_init的时候说过Linux内部使用红黑树来管理,它在卸载时使用find_module查找树结构并找到对应模块
mod = find_module(name);
if (!mod) {
ret = -ENOENT;
goto out;
}
并且会去检查这个模块是否有init和exit函数
/* If it has an init func, it must have an exit func to unload */
if (mod->init && !mod->exit) {
forced = try_force_unload(flags);
if (!forced) {
/* This module can't be removed */
ret = -EBUSY;
goto out;
}
}
随后调用try_stop_module函数来停止模块
/* Stop the machine so refcounts can't move and disable module. */
ret = try_stop_module(mod, flags, &forced);
if (ret != 0)
goto out;
在Linux内部模块维护着一个计数器,当有进程在使用它时计数器就会增加try_stop_module会检查这个模块的计数器是否为0,如果为0则会将模型状态设置为MODULE_STATE_GOING,禁止其它进程对它的使用
/* Mark it as dying. */
mod->state = MODULE_STATE_GOING;
然后就会去调用它的exit函数
if (mod->exit != NULL)
mod->exit();
随后调用blocking_notifier_call_chain函数来对notify进行通知,通知MODULE_STATE_GOING事件的发生并做对应处理,所有在notify链上面注册的,对MODULE_STATE_GOING感兴趣的回调都会被调用
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_GOING, mod);
最后将模块从模块表里移除
free_module(mod);
527

被折叠的 条评论
为什么被折叠?



