linux内核链接脚本vmlinux.lds分析续篇之 --- initcall机制(十三)
说明: 此篇initcall机制讲解对应的内核版本是4.20.13:这时可能有人会提出疑问,为什么前面的都是用老的内核2.6.38来讲,这个机制用新的内核来讲?主要是因为这里有一定的改动,我还没有研究过新内核这块的流程,顺便一起学习一下。
一. initcall机制的由来
我们都知道,linux对驱动程序提供静态编译进内核和动态加载两种方式,当我们试图将一个驱动程序编译进内核时,开发者通常提供一个xxx_init()函数接口以启动这个驱动程序同时提供某些服务。
那么,根据常识来说,这个xxx_init()函数肯定是要在系统启动的某个时候被调用,才能初始化并启动这个驱动程序。
最简单直观地做法就是:开发者试图添加一个驱动程序时,在内核启动init程序的某个地方直接添加调用自己驱动程序的xxx_init()函数,在内核启动时自然会调用到这个程序。
但是,回头一想,这种做法在单人开发的小系统中或许可以,但是在linux中,如果驱动程序是这么个添加法,那就是一场灾难,这个道理我想不用我多说。
不难想到另一种方式,就是集中提供一个地方,如果你要添加你的驱动程序,你就将你的初始化函数在这个地方进行添加,在内核启动的时候统一扫描这个地方,再执行这一部分的所有被添加的驱动程序。
那到底怎么添加呢?直接在C文件中作一个列表,在里面添加初始化函数?我想随着驱动程序数量的增加,这个列表会让人头昏眼花。
然而,对于linux大神而言,这些都不是事,linux的做法是:
底层实现上,在内核镜像文件中,自定义一个段,这个段里面专门用来存放这些初始化函数的地址,内核启动时,只需要在这个段地址处取出函数指针,一个个执行即可。
对上层而言,linux内核提供xxx_init(init_func)宏定义接口,驱动开发者只需要将驱动程序的init_func使用xxx_init()来修饰,这个函数就被自动添加到了对应level的段中,开发者完全不需要关心实现细节。
对于各种各样的驱动而言,可能存在一定的依赖关系,需要遵循先后顺序来进行初始化,考虑到这个,linux也对这一部分做了分级处理。
二. initcall的源码
在平台对应的include/linux/init.h文件中,可以找到xxx_initcall的定义:
# include/linux/init.h
/*
* Early initcalls run before initializing SMP.
*
* Only for built-in code, not modules.
*/
#define early_initcall(fn) __define_initcall(fn, early)
/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#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)
#define __exitcall(fn) \
static exitcall_t __exitcall_##fn __exit_call = fn
#define console_initcall(fn) ___define_initcall(fn,, .con_initcall)
xxx_init_call(fn)的原型其实是__define_initcall(fn, n),n是一个数字或者是数字+s,这个数字代表这个fn执行的优先级,数字越小,优先级越高,带s的fn优先级低于不带s的fn优先级。
继续跟踪代码,看看__define_initcall(fn,n):
#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)
值得注意的是:
(1)attribute()是gnu C中的扩展语法,它可以用来实现很多灵活的定义行为,这里不细究。我们只需知道: attribute((section(".initcall" #id “.init”)))表示编译时将目标符号放置在括号指定的段中。
(2)#在宏定义中的作用是将目标字符串化,##在宏定义中的作用是将多个符号连接成一个符号,并不将其字符串化。
(3)used是一个宏定义,使用前提是在编译器编译过程中,如果定义的符号没有被引用,编译器就会对其进行优化,不保留这个符号,而__attribute((used))的作用是告诉编译器这个静态符号在编译的时候即使没有使用到也要保留这个符号。具体定义如下:
#define __used __attribute__((__used__))
为了更方便地理解,我们拿举个例子来说明,开发者声明了这样一个函数:core_initcall(example_init);
所以core_initcall(example_init)的解读就是:
- 首先宏展开成:__define_initcall(example_init,1)
- 接着又被宏定义为:___define_initcall(example_init, 1, .initcall1)
- 然后展开:static initcall_t __initcall_example_init1 = example_init,这就是一个简单的变量定义。
- 同时声明 __initcall_example_init1这个变量即使没被引用也保留符号,并且将其放置在内核镜像的.initcall1.init段处。
需要注意的是,根据官方注释可以看到early_initcall(fn)只针对内置的核心代码,不能用来描述模块。
三. xxx_initcall修饰函数的调用
既然我们知道了xxx_initcall是怎么定义而且目标函数的放置位置,那么使用xxx_initcall()修饰的函数是怎么被调用的呢?
我们知道Linux内核启动准备阶段准备完成后(这一点将放在内核启动系列篇进行相应的讲解),就从内核C函数起始部分也就是start_kernel开始往下继续运行,这里只列出我们关注的调用顺序:
# init/main.c
|--- start_kernel
|--- arch_call_rest_init
|--- rest_init
|--- kernel_thread(kernel_init, NULL, CLONE_FS)
|--- kernel_init_freeable
|--- do_basic_setup
|--- do_initcalls
这个do_initcalls()就是我们需要寻找的函数了,在这个函数中执行所有使用xxx_initcall()声明的函数,接下来我们再来看看它是怎么执行的:
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,
};
int __init_or_module do_one_initcall(initcall_t fn)
{
...
do_trace_initcall_start(fn);
ret = fn();
do_trace_initcall_finish(fn, ret);
...
return ret;
}
static void __init do_initcall_level(int level)
{
...
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
在上述代码中,定义了一个静态的initcall_levels数组,这是一个指针数组,数组的每个元素都是一个指针。
(1)do_initcalls()循环调用do_initcall_level(level),level就是initcall的优先级数字,由for循环的终止条件ARRAY_SIZE(initcall_levels) - 1可知,总共会调用do_initcall_level(0)~do_initcall_level(7),一共八次。
(2)而do_initcall_level(level)中则会遍历initcall_levels[level] ~ initcall_levels[level +1] 中的每个函数指针,(initcall_levels[level]实际上是对应的__initcall##level##_start指针变量),并调用do_one_initcall(initcall_from_entry(fn))。
注意:
- fn为函数指针,fn++相当于函数指针+1,相当于:内存地址+sizeof(fn),sizeof(fn)根据平台不同而不同,一般来说,32位机上是4字节,64位机则是8字节。
- initcall_levels[level]指向当前".initcall##level.init"段,initcall_levels[level+1]指向".initcall##(level+1).init"段,两个段之间的内存就是存放所有添加的具有相同段属性的函数指针。
也就是从".initcall##level.init"段开始,每次取一个函数出来执行,并累加指针,直到取完。
(3)do_one_initcall(initcall_from_entry(fn))的执行:initcall_from_entry(fn)是得到当前fn在这个段中的偏移。然后调用fn。
在initcall机制的由来中有提到,在开发者添加xxx_initcall(fn)时,事实上是将fn放置到了".initcall##level##.init"的段中,但是在do_initcall()的源码部分,却是从initcall_levels取出,initcall_levels[level]是怎么关联到".initcall##level##.init"段的呢?
答案在include/asm-generic/vmlinux.lds.h中:
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \
#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 = .;
在这里首先定义了__initcall_start,将其关联到".initcallearly.init"段。
然后对每个level定义了INIT_CALLS_LEVEL(level),将INIT_CALLS_LEVEL(level)展开之后的结果是定义__initcall##level##_start,并将
__initcall##level##_start关联到".initcall##level##.init"段和".initcall##level##s.init"段。
到这里,__initcall##level##_start和".initcall##level##.init"段的对应就比较清晰了,所以,从initcall_levels[level]部分一个个取出函数指针,并执行每一个函数,其中有一个fn就是执行xxx_initcall()定义的函数。
附注: 关于linux内核initcall的机制,我本来想自己从头开始写一篇的,奈何有一篇写得非常全面的博文,实在难以超越。我就根据自己的理解修改了原文中的一些说法欠严瑾的地方。
参考博文地址:linux的initcall机制(针对编译进内核的驱动)
本文来自博客园,作者:BSP-路人甲,转载请注明原文链接:https://www.cnblogs.com/jianhua1992/p/16852793.html,并保留此段声明,否则保留追究法律责任的权利。