Linux内核的初始化流程,很多文章都有分析,自己也学习了很多次,总是感觉记忆不深刻。突发奇想,觉得整个初始化流程与中国神话传说盘古开天地特别的契合,所以尝试着更生动形象的分析一下内核初始化化流程。
天和地还没有分开的时候,宇宙混沌一片,从外表看像一个大鸡蛋。这个蛋形混沌宇宙可以用“0”来非常形象的表示。Linux内核的初始化是从0开始的,Documentation/arm64/booting.txt里面写明了,此时中断是完全关闭的,一切似乎都是静止的。
宇宙混沌中,开始孕育代号为0的巨人盘古,孕育的顺序是从头(head.S)开始,然后是身体(stext)。什么是宇宙?宇者,上下四方,空间为宇;宙者,古往今来,时间为宙。盘古为了开天辟地,必须分开空间和时间。在空间维度,要对内存空间进行切分,即基于MMU的空分复用。在时间维度,要对CPU的运行时间进行切分,即基于进程调度的时分复用。盘古作为0号进程,在head.S汇编代码中,沿着_head(头)->stext(身)->__primary_switch(MMU)->__primary_switched(进程)完成了上述4个步骤,之后就可以跳转到C语言函数start_kernel开始开天辟地。
盘古开天辟地不仅仅是简单的分开天地,还要创造世间万物,就像start_kernel要进行各个子系统的初始化一样。当所有的子系统都初始化完毕,开天辟地的最后一步,就是分出天和地,start_kernel的最后一步是rest_init,也有异曲同工之妙。在rest_init中,创建”地”进程即1号进程init,它将孕育所有用户进程,是所有的用户进程的祖先;创建”天”进程为2号线程kthreadd,它将孕育所有内核线程,是所有的内核线程的祖先。
盘古(0号进程)在完成开天辟地之后,就事了拂衣去,闲云野鹤地idle了,也被称为idle进程。只有在天地之间没有任何进程运行的时候,才被调度运行。
Linux内核代码的执行真的是从头(Head)开始!为啥这么说?
内核入口所在的文件名是head.S,启动代码链接时的代码段名字叫做__HEAD(即.head.text头代码),启动代码的标签是_head,全部都是用单词Head(头),司马昭之心路人皆知哈。接下来分析具体是怎么做到的。
根据Linux内核源码目录下的Makefile,vmlinux的链接文件是vmlinux.lds。
export KBUILD_LDS := arch/$(SRCARCH)/kernel/vmlinux.lds
链接文件vmlinux.lds是由vmlinux.lds.S文件生成的。以ARM64为例,vmlinux.lds.S位于目录arch/arm64/kernel/。vmlinux.lds生成的过程记录在.vmlinux.lds.cmd文件中。
在vmlinux.lds.S文件中,与Linux内核运行入口相关的主体内容如下:
1 |
ENTRY(_text) |
|||
2 |
||||
3 |
SECTIONS |
|||
4 |
{ |
|||
5 |
…… |
|||
6 |
. = KIMAGE_VADDR + TEXT_OFFSET; |
|||
7 |
||||
8 |
.head.text : { |
|||
9 |
_text = .; |
|||
10 |
HEAD_TEXT |
|||
11 |
} |
|||
12 |
…… |
|||
13 |
.text : { |
/* Real text segment*/ |
||
14 |
_stext = .; |
/* Text and read-only data*/ |
||
15 |
…… |
|||
16 |
} |
|||
17 |
} |
第1行,使用ENTRY命令将符号_text的值设置为Linux运行时入口地址。
_text在哪里定义?
看第9行,此处定义了_text符号。咬文嚼字一下哈,为啥叫这个名字呢?追溯到commit e2f81844efa2 ARM: vmlinux.lds: use _text and _stext the same way as x86。x86使用_text来标记.head.text段的起始地址,同时也是内核镜像的运行起始地址;用_stext来标记.text段的起始地址。_text之后第10行就是HEAD_TEXT。HEAD_TEXT定义在include/asm-generic/vmlinux.lds.h:
#define HEAD_TEXT KEEP(*(.head.text))
所以,只要在源码中定义了.head.text段,就会链接到_text。 这就必须说到定义在include/linux/init.h中的宏定义__HEAD。通过宏定义__HEAD就可以在源码中定义.head.text段。其中“ax”后缀的定义参考Section (Using as) (sourceware.org):a代表allocatable,x代表executable。
#define __HEAD .section ".head.text","ax" |
__HEAD宏在哪里使用?
在head.S(arch/arm64/kernel)的起始位置,使用了__HEAD宏。所以,Linux内核运行的入口位置就是head.S中的标号_head。
__HEAD _head: /* * DO NOT MODIFY. Image header expected by Linux boot-loaders. */ #ifdef CONFIG_EFI /* * This add instruction has no meaningful effect except that * its opcode forms the magic "MZ" signature required by UEFI. */ add x13, x18, #0x16 b stext #else b stext // branch to kernel start, magic .long 0 // reserved #endif |
回到vmlinux.lds.S文件,_text的值是多少?结合第6行和第9行,可知:
_text = KIMAGE_VADDR + TEXT_OFFSET。
根据生成的vmlinux.lds,可以直接看到宏定义自动展开的结果,算出来是0xFFFF 0000 0800 0000。
. = ((((((0xffffffffffffffff)) - (((1)) << (48)) + 1) + (0)) + (0x08000000))) + 0x00080000; |
也可以追一下宏定义的位置,手动展开并计算,具体过程如下:
KIMAGE_VADDR定义在arch/arm64/include/asm/memory.h,
#define VA_BITS (CONFIG_ARM64_VA_BITS) //配置为48 #define VA_START (UL(0xffffffffffffffff) - \ (UL(1) << VA_BITS) + 1) #define KIMAGE_VADDR (MODULES_END) #define MODULES_END (MODULES_VADDR + MODULES_VSIZE) #define MODULES_VADDR (VA_START + KASAN_SHADOW_SIZE) #define MODULES_VSIZE (SZ_128M) #define KASAN_SHADOW_SIZE (0) |
TEXT_OFFSET定义arch/arm64/Makefile:
TEXT_OFFSET := 0x00080000 |