title: Kernel之init相关
author: shell
categories:
- Linux
- Kernel
tags:
- init
date: 2021-09-06 21:19:20
Kernel之init相关
主要记录一些整体的概念、框架和简单介绍,不涉及具体的原理和实现细节
背景
在看驱动代码的时侯常常会听到module_init、subsys_initcall等xxx_init相关的代码,原先只晓得是该驱动最开始入口函数的地方,并没有考量到底层去,近来正好又见到,就想瞧瞧底层是哪些样的,于是就有了此文。
xxx_init相关初始化函数
这儿主要列出了module_init和subsys_initcall相关实现,其他类似
#ifndef MODULE
/*...*/
/* 不是模块时 */
#define subsys_initcall(fn) __define_initcall(fn, 4)
/*...*/
#endif
/* 为模块时 */
#define subsys_initcall(fn) module_init(fn)
#ifndef 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)));
/*...*/
#endif
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
从里面可以看出:
在驱动为模块时,subsys_initcall和module_init是一样的suse linux,都是module_init
在驱动不是模块时linux关机命令,编译进内核,最后都是到__define_initcall(fn,id),
xxx_init相关初始化函数在这两种形态下的实现是不一样的,主要跟对应模块的运行方法有关:
编译成可动态加载的模块,并通过insmod来动态加载,再进行初始化。
静态编译链接进内核的模块,在系统启动过程中进行初始化。
有些模块是必需要编译到内核,不能动态加载的,例如启动相关的模块,vfs等
旁边,我们就分这2块分别讨论:
非模块
在前面的讨论中,驱动不是模块时,最终init就会到__define_initcall(fn,id),只不过id不一样,module_init对应的id为6,subsys_initcall为4
这个id会有哪些影响呢?下边继续剖析__define_initcall
__define_initcall:
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec)
static initcall_t __initcall_##fn##id __used
__attribute__((__section__(#__sec ".init"))) = fn;
/*
* Used for initialization calls..
*/
typedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);
__define_initcall主要是定义初始化函数,并使用__attribute__和__section__将对应的初始化函数fn置于相应的.initcall##id段,如这儿对应的就是.initcall4和.initcall6
那.initcall##id段是些啥呢?虽然就是Kernel初始化init执行的次序,看右图就很明了了:
各个子区段之间的次序是确定的,即先调用.initcall1.init中的函数表针,再调用.initcall2.init中的函数表针,等等,这样就保证了初始化一定的调用次序
而在每位子区段中的函数表针的次序是和链接次序相关的,是不确定的。
Kernel启动相关流程:
模块
编译成module的模块就会手动形成一个*.mod.c的文件,上面有很重要的一段__section,用.gnu.linkonce.this_module标记:
...
__visible struct module __this_module
__section(.gnu.linkonce.this_module) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
...
定义了一个类型为module的全局变量__this_module,其成员init即为init_module
在insmod或modprobe模块时,最终会调用系统调用sys_init_module,
对应的内核函数(kernel/module.c):
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
load_module函数上面会加载模块的ko文件,并解释各个section,重定位,
其中,setup_load_info函数中会查找.gnu.linkonce.this_module段
info->index.mod = find_sec(info, ".gnu.linkonce.this_module");
if (!info->index.mod) {
pr_warn("%s: No module found in objectn",
info->name ?: "(missing .modinfo name field)");
return -ENOEXEC;
}
找到对应的module数据:
/* Module has been copied to its final place now: return it. */
mod = (void *)info->sechdrs[info->index.mod].sh_addr;
前面会调用do_init_module函数去进行初始化:
/* Start the module */
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
延展:Kernel中__init等宏定义
相关宏定义:
include/linux/init.h
include/linux/compiler_attributes.h
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)
#define __initconst __section(.init.rodata)
#define __aligned(x) __attribute__((aligned(x))) ///指定为x字节对齐. x是一个2的幂次方
#define __section(S) __attribute__((__section__(#S))) ///将放在#S段
#define __used __attribute__((__used__)) ///避免被链接器因为未用过而被优化掉
GNUC的一大特色就是__attribute__机制。__attribute__可以设置函数属性(FunctionAttribute)、变量属性(VariableAttribute)和类型属性(TypeAttribute)
__section主要告诉链接器应当把这个函数或则数据放置在那个位置,通常是指放置到内核镜像的那个位置上。
内核相当于一个特别大的可执行程序,上面的代码、数据等都是分段储存,一般编译器将函数置于.text段,变量放到.data或.bss段。具体段的储存规则是由vmlinux.lds文件定义,vmlinux.lds文件相关在前面章节有具体介绍linux lds文件,在代码中一般使用__section来申明属于那个段,如上面列出的这些宏定义
内核把段分的十分细致,是由于它会在运行过程中去定位相应的数据和代码,这样将愈加便捷处理。如同__init修饰的所有代码都置于.init.text段,它只在启动阶段会被内核调用到,当初始化结束后还会释放这部份显存,便于充分借助显存linux lds文件,这个就是属于显存管理的部份了。
延展:链接脚本
Kernel的链接脚本(“ldscript”):vmlinux.lds.S和vmlinux.lds
vmlinux.lds文件是由原始文件的汇编文件vmlinux.lds.S编译得到,所以未编译的内核源码里通常只有vmlinux.lds.S文件
同一构架下vmlinux.lds文件通常会有2个:
通常都在构架的对应的目录下,即arch/xxx/kernel/和arch/xxx/boot/compressed/
vmlinux.lds文件内容举例:
反汇编对比:
objdump --headers vmlinux
参考: