Kernel之init相关

[TOC]

主要记录一些整体的概念、框架和简单介绍,不涉及具体的原理和实现细节

背景

在看驱动代码的时候经常会看到module_initsubsys_initcall等xxx_init相关的代码,以前只知道是该驱动最开始入口函数的地方,并没有深究到底层去,最近刚好又碰到,就想看看底层是什么样的,于是就有了此文。

xxx_init相关初始化函数

这里主要列举了module_initsubsys_initcall相关实现,其他类似

  • subsys_initcall:
    #ifndef MODULE
    /*...*/
    /* 不是模块时 */
    #define subsys_initcall(fn)		__define_initcall(fn, 4)
    /*...*/
    #endif
    
    /* 为模块时 */
    #define subsys_initcall(fn)		module_init(fn)
  • module_init:
    #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_initcallmodule_init是一样的,都是module_init
    在驱动不是模块时,编译进内核,最后都是到__define_initcall(fn, id)

xxx_init相关初始化函数在这两种形态下的实现是不一样的,主要跟对应模块的运行方式有关:

  1. 编译成可动态加载的模块,并通过insmod来动态加载,再进行初始化。
  2. 静态编译链接进内核的模块,在系统启动过程中进行初始化。

有些模块是必须要编译到内核,不能动态加载的,比如启动相关的模块,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

insmodmodprobe模块时,最终会调用系统调用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 object\n",
        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__))        ///避免被链接器因为未用过而被优化掉

GNU C 的一大特色就是__attribute__ 机制。__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )

__section主要告诉链接器应该把这个函数或者数据放置在哪个位置,一般是指放置到内核镜像的哪个位置上。
内核相当于一个非常大的可执行程序,里面的代码、数据等都是分段存放,通常编译器将函数放在.text 段,变量放在.data.bss 段。具体段的存放规则是由vmlinux.lds文件定义,vmlinux.lds文件相关在后面章节有具体介绍,在代码中通常使用__section来声明属于哪个段,如前面列举的那些宏定义

内核把段分的非常细致,是因为它会在运行过程中去定位相应的数据和代码,这样将更加方便处理。就像__init 修饰的所有代码都放在.init.text段,它只在启动阶段会被内核调用到,当初始化结束后就会释放这部分内存,以便充分利用内存,这个就是属于内存管理的部分了。

延伸:链接脚本

Kernel的链接脚本("ld script"): vmlinux.lds.Svmlinux.lds

vmlinux.lds文件是由原始文件的汇编文件vmlinux.lds.S编译得到,所以未编译的内核源码里一般只有vmlinux.lds.S文件

同一架构下vmlinux.lds文件一般会有2个:

  • ./arch/x86/boot/compressed/vmlinux.lds: -用于生成压缩的内核image
  • ./arch/x86/kernel/vmlinux.lds: -用于生成未压缩的内核image

一般都在架构的对应的目录下,即arch/xxx/kernel/arch/xxx/boot/compressed/

vmlinux.lds文件内容举例:

```

反汇编对比:
```shell
objdump --headers vmlinux

参考: