module_init加载设备驱动 并最终调用到xxx_init函数

头像
96 编辑
04-30 电脑
  我们知道在写设备驱动的时候通常要为某个设备实现xxx_init函数,并将该函数传入module_init(xxx_init), 当kernel启动之后该设备驱动就可以被内核加载,这一章节将以倒叙的方式详细介绍了内核是如何加载module_init()函数,并最终调用到xxx_init函数的。
module_init()定义在include/linux/module.h中,
#ifndef MODULE
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(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) __attribute__((alias(#initfn)));
 
/* 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) __attribute__((alias(#exitfn)));
#endif
其中有两部分定义,设备驱动的加载有两种方式,一种是编译进内核,一种是以模块的方式加载,加载方式不同定义的形式也略有不同。#ifndef MODULE表明当设备驱动编译进内核时, module_init的定义形式。
#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)
#define __initcall(fn) device_initcall(fn)
从__inicall定义可以,module_init()-->__initcall(fn)--->__define_initcall(fn)---> __define_initcall(fn, 6),可以看到在initcall段中启动的部分最终都是通过__define_initcall设置的。这部分的代码可以在include/linux/int.h中找到。
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn;
下面分析一下该宏的具体细节,比如,__define_initcall传递过来的参数为(xxx_init, 6), 经过##和#的作用之后,就将xxx_init连接到之前的字符串中,为__initcall_xxx_init6,除此之外还有一个section的定义".initcall" #id ".init",由于前后两部分都是字符串,所以#id的作用就是字符串化,组合成.initcall6.init.
 
说了这么多,貌似我们再跟踪代码就跟不下去了,因为你再也找不到代码的下一步调用在哪里了,我们在代码中搜索一下发现在vmlinux.lds.S中找到了线索,我们本文是以arm64为前提的所以文件的路径为(./arch/arm64/kernel/vmlinux.lds.S). 在这个链接器脚本中我们发现了很多段,其中也包含上文提到的initcall段,现在以一张图片来展示各个段的全貌.....

#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)
#define __initconst __section(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
除此之外发现,以__init标记的函数或者变量都是放置在.init.text段中,__initdata标记的函数或者变量都是放置在.init.data段中....,所以驱动的xxx_init函数都是放置在init.text段中,module_init函数将xxx_init的函数指针放置到了initcall6.init段中,kernel启动过程中先加载到initcall6.init段中的函数指针然后加载到init.text段中的函数实体。
接下来分析kernel启动过程时如何加载到initcall6段中的内容的,
start_kernel---->rest_init---->kernel_thread(kernel_init, NULL, CLONE_FS)---->kernel_init_freeable---->do_basic_setup---->do_initcalls-->do_initcall_level---->do_one_initcall
由上述的流程,开机过程会调用start_kernel进而会调用到do_initcalls,
static void __init do_initcalls(void)
{
int level;
 
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
该函数中会计算initcall_levels数组的大小,并循环调用do_initcall_level。你会发现,initcall_levels的定义就是本.c文件中,并且定义为__initdata,即放置在init.data段中。
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
initcall_levels定义为指针数组,即数组中每个元素都是__initcallx_start的起始地址。跟踪一下代码发现extern initcall_t __initcall_start[]都有类似的地定义,如果这个时候你再看下vmlinux.lds.S便会豁然开朗。最后开始让我们好好看看do_one_initcall函数吧,这里我把不必要的内容都去掉了,留下的都是精华,很明显fn就是 __initcall6_start地址了,通过循环initcall6中的所有内容都会被依次加载。
int __init_or_module do_one_initcall(initcall_t fn)
{ .........
if (initcall_debug)
ret = do_one_initcall_debug(fn);
else
ret = fn();
........
}
这里的initcall_debug故名思意就是开启debug相关的功能,具体的功能是每个驱动模块加载的时间,做系统优化的朋友对这个变量一定时非常熟悉的。
7
3
云主机的10大特点及9大优势 Excel Mid函数怎么用 Midb函数反向取值及使用方法
热门文章
随便看看
随便看看