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)

从里面可以看出:

文件linux压缩指令_文件linux命令_linux lds文件

在驱动为模块时,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执行的次序,看右图就很明了了:

文件linux压缩指令_linux lds文件_文件linux命令

各个子区段之间的次序是确定的,即先调用.initcall1.init中的函数表针,再调用.initcall2.init中的函数表针,等等,这样就保证了初始化一定的调用次序

而在每位子区段中的函数表针的次序是和链接次序相关的,是不确定的。

Kernel启动相关流程:

文件linux压缩指令_linux lds文件_文件linux命令

模块

文件linux命令_文件linux压缩指令_linux lds文件

编译成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;

文件linux命令_文件linux压缩指令_linux lds文件

前面会调用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文件,这个就是属于显存管理的部份了。

延展:链接脚本

linux lds文件_文件linux压缩指令_文件linux命令

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

参考:

Tagged:
Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复