内核入口 - start_kernel
内核初始化. Part 4.
Kernel entry point
还记得上一章的内容吗 - 跳转到内核入口之前的最后准备?你应该还记得我们已经完成一系列初始化操作,并停在了调用位于init/main.c
中的start_kernel
函数之前.start_kernel
函数是与体系架构无关的通用处理入口函数,尽管我们在此初始化过程中要无数次的返回arch/ 文件夹。如果你仔细看看start_kernel
函数的内容,你将发现此函数涉及内容非常广泛。在此过程中约包含了86个调用函数,是的,你发现它真的是非常庞大但是此部分并不是全部的初始化过程,在当前阶段我们只看这些就可以了。此章节以及后续所有在内核初始化过程章节的内容将涉及并详述它。
start_kernel
函数的主要目的是完成内核初始化并启动祖先进程(1号进程)。在祖先进程启动之前start_kernel
函数做了很多事情,如锁验证器,根据处理器标识ID初始化处理器,开启cgroups子系统,设置每CPU区域环境,初始化VFS Cache机制,初始化内存管理,rcu,vmalloc,scheduler(调度器),IRQs(中断向量表),ACPI(中断可编程控制器)以及其它很多子系统。只有经过这些步骤我们才看到本章最后一部分祖先进程启动的过程;同志们,如此复杂的内核子系统,有没有勾起你的学习欲望,有这么多的内核代码等着我们去征服,让我们开始吧。
注意:在此大章节的所有内容 Linux Kernel initialization process
,并不涉及内核调试相关,关于内核调试部分会有一个单独的章节来进行描述
关于 __attribute__
__attribute__
正如我上述所写,start_kernel
函数是定义在init/main.c.从已知代码中我们能看到此函数使用了__init
特性,你也许从其它地方了解过关于GCC __attribute__
相关的内容。在内核初始化阶段这个机制在所有的函数中都是有必要的。
在初始化过程完成后,内核将通过调用free_initmem
释放这些sections(段)。注意__init
属性是通过__cold
和notrace
两个属性来定义的。第一个属性cold
的目的是标记此函数很少使用所以编译器必须优化此函数的大小,第二个属性notrace
定义如下:
含有no_instrument_function
意思就是告诉编译器函数调用不产生环境变量(堆栈空间)。
在start_kernel
函数的定义中,你也可以看到__visible
属性的扩展:
含有externally_visible
意思就是告诉编译器有一些过程在使用该函数或者变量,为了放至标记这个函数/变量是unusable
。你可以在此include/linux/init.h处查到这些属性表达式的含义。
start_kernel 初始化
在start_kernel的初始之初你可以看到这两个变量:
第一个变量表示内核命令行的全局指针,第二个变量将包含parse_args
函数通过输入字符串中的参数'name=value',寻找特定的关键字和调用正确的处理程序。我们不想在这个时候参与这两个变量的相关细节,但是会在接下来的章节看到。我们接着往下走,下一步我们看到了此函数:
lockdep_init
初始化 lock validator. 其实现是相当简单的,它只是初始化了两个哈希表 list_head并设置lockdep_initialized
全局变量为1
。 关于自旋锁 spinlock以及互斥锁mutex 如何获取请参考链接.
下一个函数是set_task_stack_end_magic
,参数为init_task
和设置STACK_END_MAGIC
(0x57AC6E9D
)。init_task
代表初始化进程(任务)数据结构:
task_struct
存储了进程的所有相关信息。因为它很庞大,我在这本书并不会去介绍,详细信息你可以查看调度相关数据结构定义头文件 include/linux/sched.h。在此刻task_struct
包含了超过100
个字段!虽然你不会在这本书中看到关于task_struct
的解释,但是我们会经常使用它,因为它是介绍在Linux内核进程
的基本知识。我将描述这个结构中字段的一些含义,因为我们在后面的实践中见到它们。
你也可以查看init_task
的相关定义以及宏指令INIT_TASK
的初始化流程。这个宏指令来自于include/linux/init_task.h在此刻只是设置和初始化了第一个进程来(0号进程)的值。例如这么设置:
初始化进程状态为 zero 或者
runnable
. 一个可运行进程即为等待CPU去运行;初始化仅存的标志位 -
PF_KTHREAD
意思为 - 内核线程;一个可运行的任务列表;
进程地址空间;
初始化进程堆栈
&init_thread_info
-init_thread_union.thread_info
和initthread_union
使用共用体 -thread_union
包含了thread_info
进程信息以及进程栈:。
每个进程都有其自己的堆栈,x86_64
架构的CPU一般支持的页表是16KB or 4个页框大小。我们注意stack变量被定义为数据并且类型是unsigned long
。thread_union
结构的下一个字段为thread_union
定义如下:
此结构占用52个字节。thread_info
结构包含了特定体系架构相关的线程信息,我们都知道在X86_64
架构上内核栈是逆生成而thread_union.thread_info
结构则是正生长。所以进程进程栈是16KB并且thread_info
是在栈底。还需我们处理16 kilobytes - 62 bytes = 16332 bytes
.注意 thread_union
代表一个联合体union而不是结构体,用一张图来描述栈内存空间。 如下图所示:
http://www.quora.com/In-Linux-kernel-Why-thread_info-structure-and-the-kernel-stack-of-a-process-binds-in-union-construct
所以INIT_TASK
宏指令就是task_struct's
'结构。正如我上述所写,我并不会去描述这些字段的含义和值,在INIT_TASK
赋值处理的时候我们很快能看到这些。
现在让我们回到set_task_stack_end_magic
函数,这个函数被定义在kernel/fork.c功能为设置canary init
进程堆栈以检测堆栈溢出。
上述函数比较简单,set_task_stack_end_magic
函数的作用是先通过end_of_stack
函数获取堆栈并赋给 task_struct
。 关于检测配置需要打开内核配置宏CONFIG_STACK_GROWSUP
。因为我们学习的是x86架构的初始化,堆栈是逆生成,所以堆栈底部为:
task_thread_info
的定义如下,返回一个当前的堆栈;
进程的栈底,我们写STACK_END_MAGIC
这个值。如果设置canary
,我们可以像这样子去检测堆栈:
set_task_stack_end_magic
初始化完毕后的下一个函数是 smp_setup_processor_id
.此函数在x86_64
架构上是空函数:
在此架构上没有实现此函数,但在别的体系架构的实现可以参考s390 and arm64.
我们接着往下走,下一个函数是debug_objects_early_init
。此函数的执行几乎和lockdep_init
是一样的,但是填充的哈希对象是调试相关。上述我已经表明,关于内核调试部分会在后续专门有一个章节来完成。
debug_object_early_init
函数之后我们看到调用了boot_init_stack_canary
函数。task_struct->canary
的值利用了GCC特性,但是此特性需要先使能内核CONFIG_CC_STACKPROTECTOR
宏后才可以使用。 boot_init_stack_canary
什么也没有做, 否则基于随机数和随机池产生 TSC:
我们要获取随机数, 我们可以给stack_canary
字段 task_struct
赋值:
然后将此值写入IRQ堆栈的顶部:
关于IRQ的章节我们这里也不会详细剖析, 关于这部分介绍看这里IRQs.如果canary被设置, 关闭本地中断注册bootstrap CPU以及CPU maps. 我们关闭本地中断 (interrupts for current CPU) 使用 local_irq_disable
函数,展开后原型为 arch_local_irq_disable
函数include/linux/percpu-defs.h:
如果native_irq_enable
通过cli
指令判断架构,这里是X86_64
, Where native_irq_enable
is cli
instruction for x86_64
.中断的关闭(屏蔽)我们可以通过注册当前CPU ID到CPU bitmap来实现。
激活第一个CPU
当前已经走到start_kernel
函数中的boot_cpu_init
函数,此函数主要为了通过掩码初始化每一个CPU。首先我们需要获取当前处理器的ID通过下面函数:
现在是0. 如果CONFIG_DEBUG_PREEMPT
宏配置了那么 smp_processor_id
的值就来自于 raw_smp_processor_id
函数,原型如下:
this_cpu_read
函数与其它很多函数一样如(this_cpu_write
, this_cpu_add
等等...) 被定义在include/linux/percpu-defs.h 此部分函数主要为对 this_cpu
进行操作. 这些操作提供不同的对每cpuper-cpu 变量相关访问方式. 譬如让我们来看看这个函数 this_cpu_read
:
还记得上面我们所写,每cpu变量cpu_number
的值是this_cpu_read
通过raw_smp_processor_id
来得到,现在让我们看看 __pcpu_size_call_return
的执行:
是的,此函数虽然看起起奇怪但是它的实现是简单的,我们看到pscr_ret__
变量的定义是int
类型,为什么是int类型呢?好吧,变量
是common_cpu
它声明了每cpu(per-cpu)变量:
在下一个步骤中我们调用了__verify_pcpu_ptr
通过使用一个有效的每cpu变量指针来取地址得到cpu_number
。之后我们通过pscr_ret__
函数设置变量的大小,common_cpu
变量是int
,所以它的大小是4字节。意思就是我们通过this_cpu_read_4(common_cpu)
获取cpu变量其大小被pscr_ret__
决定。在__pcpu_size_call_return
的结束 我们调用了__pcpu_size_call_return:
需要调用percpu_from_op
并且通过mov
指令来传递每cpu变量,percpu_from_op
的内联扩展如下:
让我们尝试理解此函数是如果工作的,gs
段寄存器包含每个CPU区域的初始值,这里我们通过mov
指令copy common_cpu
到内存中去,此函数还有另外的形式:
等价于:
由于我们没有设置每个CPU的区域,我们只有一个 - 为当前CPU的值zero
通过此函数 smp_processor_id
返回.
返回的ID表示我们处于哪一个CPU上, boot_cpu_init
函数设置了CPU的在线, 激活, 当前的设置为:
上述我们所有使用的这些CPU的配置我们称之为- CPU掩码cpumask
. cpu_possible
则是设置支持CPU热插拔时候的CPU ID. cpu_present
表示当前热插拔的CPU. cpu_online
表示当前所有在线的CPU以及通过 cpu_present
来决定被调度出去的CPU. CPU热插拔的操作需要打开内核配置宏CONFIG_HOTPLUG_CPU
并且将 possible == present
以及active == online
选项禁用。这些功能都非常相似,每个函数都需要检查第二个参数,如果设置为true
,需要通过调用cpumask_set_cpu
or cpumask_clear_cpu
来改变状态。
譬如我们可以通过true或者第二个参数来这么调用:
让我们继续尝试理解to_cpumask
宏指令,此宏指令转化为一个位图通过struct cpumask *
,CPU掩码提供了位图集代表了当前系统中所有的CPU's,每CPU都占用1bit,CPU掩码相关定义通过cpu_mask
结构定义:
在来看下面一组函数定义了位图宏指令。
正如我们看到的定义一样, DECLARE_BITMAP
宏指令的原型是一个unsigned long
的数组,现在让我们查看如何执行to_cpumask
:
我不知道你是怎么想的, 但是我是这么想的,我看到此函数其实就是一个条件判断语句当条件为真的时候,但是为什么执行__check_is_bitmap
?让我们看看__check_is_bitmap
的定义:
原来此函数始终返回1,事实上我们需要这样的函数才达到我们的目的: 它在编译时给定一个bitmap
,换句话将就是检查bitmap
的类型是否是unsigned long *
,因此我们仅仅通过to_cpumask
宏指令将类型为unsigned long
的数组转化为struct cpumask *
。现在我们可以调用cpumask_set_cpu
函数,这个函数仅仅是一个 set_bit
给CPU掩码的功能函数。所有的这些set_cpu_*
函数的原理都是一样的。
如果你还不确定set_cpu_*
这些函数的操作并且不能理解 cpumask
的概念,不要担心。你可以通过读取这些章节cpumask or documentation.来继续了解和学习这些函数的原理。
现在我们已经激活第一个CPU,我们继续接着start_kernel函数往下走,下面的函数是page_address_init
,但是此函数不执行任何操作,因为只有当所有内存不能直接映射的时候才会执行。
Linux 内核的第一条打印信息
下面调用了pr_notice函数。
pr_notice其实是printk的扩展,这里我们使用它打印了Linux 的banner。
打印的是内核的版本号以及编译环境信息:
依赖于体系结构的初始化部分
下个步骤我们就要进入到指定的体系架构的初始函数,Linux 内核初始化体系架构相关调用setup_arch
函数,这又是一个类型于start_kernel
的庞大函数,这里我们仅仅简单描述,在下一个章节我们将继续深入。指定体系架构的内容,我们需要再一次阅读arch/
目录,setup_arch
函数定义在arch/x86/kernel/setup.c 文件中,此函数就一个参数-内核命令行。
此函数解析内核的段_text
和_data
来自于_text
符号和_bss_stop
(你应该还记得此文件arch/x86/kernel/head_64.S)。我们使用memblock
来解析内存块。
你可以阅读关于memblock
的相关内容在Linux kernel memory management Part 1.,你应该还记得memblock_reserve
函数的两个参数:
base physical address of a memory block;
size of a memory block.
我们可以通过__pa_symbol
宏指令来获取符号表_text
段中的物理地址
上述宏指令调用 __phys_reloc_hide
宏指令来填充参数,__phys_reloc_hide
宏指令在x86_64
上返回的参数是给定的。宏指令 __phys_addr_symbol
的执行是简单的,只是减去从_text
符号表中读到的内核的符号映射地址并且加上物理地址的基地址。
memblock_reserve
函数对内存页进行分配。
保留可用内存初始化initrd
之后我们保留替换内核的text和data段用来初始化initrd,我们暂时不去了解initrd的详细信息,你仅仅只需要知道根文件系统就是通过这种方式来进行初始化,这就是early_reserve_initrd
函数的工作,此函数获取RAM DISK的基地址、RAM DISK的大小以及RAM DISK的结束地址。
如果你阅读过这些章节Linux Kernel Booting Process,你就知道所有的这些参数都来自于boot_params
,时刻谨记boot_params
在boot期间已经被赋值,内核启动头包含了一下几个字段用来描述RAM DISK:
我们可以得到关于 boot_params
的一些信息. 具体查看get_ramdisk_image
:
关于32位的ramdisk的地址,我们可以阅读此部分内容来获取Documentation/x86/zero-page.txt:
32位变化后,我们获取64位的ramdisk原理一样,为此我们可以检查bootloader 提供的ramdisk信息:
并保留内存块将ramdisk传输到最终的内存地址,然后进行初始化:
结束语
以上就是第四部分关于内核初始化的部分内容,我们从start_kernel
函数开始一直到指定体系架构初始化setup_arch
的过程中停止,那么在下一个章节我们将继续研究体系架构相关的初始化内容。
如果你有任何的问题或者建议,你可以留言,也可以直接发消息给我twitter。
很抱歉,英语并不是我的母语,非常抱歉给您阅读带来不便,如果你发现文中描述有任何问题,请提交一个 PR 到 linux-insides-zh.
链接
最后更新于