Files
linux-insides-zh/Initialization/linux-initialization-4.md
2024-07-02 11:24:09 +08:00

451 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
内核初始化. Part 4.
================================================================================
Kernel entry point
================================================================================
还记得上一章的内容吗 - [跳转到内核入口之前的最后准备](/Initialization/linux-initialization-3.md)?你应该还记得我们已经完成一系列初始化操作,并停在了调用位于`init/main.c`中的`start_kernel`函数之前.`start_kernel`函数是与体系架构无关的通用处理入口函数尽管我们在此初始化过程中要无数次的返回arch/ 文件夹。如果你仔细看看`start_kernel`函数的内容你将发现此函数涉及内容非常广泛。在此过程中约包含了86个调用函数是的你发现它真的是非常庞大但是此部分并不是全部的初始化过程在当前阶段我们只看这些就可以了。此章节以及后续所有在[内核初始化过程](/Initialization/)章节的内容将涉及并详述它。
`start_kernel`函数的主要目的是完成内核初始化并启动祖先进程(1号进程)。在祖先进程启动之前`start_kernel`函数做了很多事情,如[锁验证器](https://www.kernel.org/doc/Documentation/locking/lockdep-design.txt),根据处理器标识ID初始化处理器开启cgroups子系统设置每CPU区域环境初始化[VFS](http://en.wikipedia.org/wiki/Virtual_file_system) Cache机制初始化内存管理rcu,vmalloc,scheduler(调度器),IRQs(中断向量表),ACPI(中断可编程控制器)以及其它很多子系统。只有经过这些步骤我们才看到本章最后一部分祖先进程启动的过程;同志们,如此复杂的内核子系统,有没有勾起你的学习欲望,有这么多的内核代码等着我们去征服,让我们开始吧。
**注意:在此大章节的所有内容 `Linux Kernel initialization process`,并不涉及内核调试相关,关于内核调试部分会有一个单独的章节来进行描述**
关于 `__attribute__`
---------------------------------------------------------------------------------
正如我上述所写,`start_kernel`函数是定义在[init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c).从已知代码中我们能看到此函数使用了`__init`特性你也许从其它地方了解过关于GCC `__attribute__`相关的内容。在内核初始化阶段这个机制在所有的函数中都是有必要的。
```C
#define __init __section(.init.text) __cold notrace
```
在初始化过程完成后,内核将通过调用`free_initmem`释放这些sections(段)。注意`__init`属性是通过`__cold``notrace`两个属性来定义的。第一个属性`cold`的目的是标记此函数很少使用所以编译器必须优化此函数的大小,第二个属性`notrace`定义如下:
```C
#define notrace __attribute__((no_instrument_function))
```
含有`no_instrument_function`意思就是告诉编译器函数调用不产生环境变量(堆栈空间)。
`start_kernel`函数的定义中,你也可以看到`__visible` 属性的扩展:
```
#define __visible __attribute__((externally_visible))
```
含有`externally_visible`意思就是告诉编译器有一些过程在使用该函数或者变量,为了放至标记这个函数/变量是`unusable`。你可以在此[include/linux/init.h](https://github.com/torvalds/linux/blob/master/include/linux/init.h)处查到这些属性表达式的含义。
start_kernel 初始化
--------------------------------------------------------------------------------
在start_kernel的初始之初你可以看到这两个变量
```C
char *command_line;
char *after_dashes;
```
第一个变量表示内核命令行的全局指针,第二个变量将包含`parse_args`函数通过输入字符串中的参数'name=value',寻找特定的关键字和调用正确的处理程序。我们不想在这个时候参与这两个变量的相关细节,但是会在接下来的章节看到。我们接着往下走,下一步我们看到了此函数:
```C
lockdep_init();
```
`lockdep_init` 初始化 [lock validator](https://www.kernel.org/doc/Documentation/locking/lockdep-design.txt). 其实现是相当简单的,它只是初始化了两个哈希表 [list_head](https://github.com/hust-open-atom-club/linux-insides-zh/blob/master/DataStructures/linux-datastructures-1.md)并设置`lockdep_initialized` 全局变量为`1`
关于自旋锁 [spinlock](http://en.wikipedia.org/wiki/Spinlock)以及互斥锁[mutex](http://en.wikipedia.org/wiki/Mutual_exclusion) 如何获取请参考链接.
下一个函数是`set_task_stack_end_magic`,参数为`init_task`和设置`STACK_END_MAGIC` (`0x57AC6E9D`)。`init_task`代表初始化进程(任务)数据结构:
```C
struct task_struct init_task = INIT_TASK(init_task);
```
`task_struct` 存储了进程的所有相关信息。因为它很庞大,我在这本书并不会去介绍,详细信息你可以查看调度相关数据结构定义头文件 [include/linux/sched.h](https://github.com/torvalds/linux/blob/master/include/linux/sched.h#L1278)。在此刻`task_struct`包含了超过`100`个字段!虽然你不会在这本书中看到关于`task_struct`的解释但是我们会经常使用它因为它是介绍在Linux内核`进程`的基本知识。我将描述这个结构中字段的一些含义,因为我们在后面的实践中见到它们。
你也可以查看`init_task`的相关定义以及宏指令`INIT_TASK`的初始化流程。这个宏指令来自于[include/linux/init_task.h](https://github.com/torvalds/linux/blob/master/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`进程信息以及进程栈:。
```C
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
```
每个进程都有其自己的堆栈,`x86_64`架构的CPU一般支持的页表是16KB or 4个页框大小。我们注意stack变量被定义为数据并且类型是`unsigned long``thread_union`结构的下一个字段为`thread_union` 定义如下:
```C
struct thread_info {
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int saved_preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1;
};
```
此结构占用52个字节。`thread_info`结构包含了特定体系架构相关的线程信息,我们都知道在`X86_64`架构上内核栈是逆生成而`thread_union.thread_info`结构则是正生长。所以进程进程栈是16KB并且`thread_info`是在栈底。还需我们处理`16 kilobytes - 62 bytes = 16332 bytes`.注意 `thread_union`代表一个联合体[union](http://en.wikipedia.org/wiki/Union_type)而不是结构体,用一张图来描述栈内存空间。
如下图所示:
```C
+-----------------------+
| |
| |
| stack |
| |
|_______________________|
| | |
| | |
| | |
|______________________| +--------------------+
| | | |
| thread_info |<----------->| task_struct |
| | | |
+-----------------------+ +--------------------+
```
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](https://github.com/torvalds/linux/blob/master/kernel/fork.c#L297)功能为设置[canary](http://en.wikipedia.org/wiki/Stack_buffer_overflow) `init` 进程堆栈以检测堆栈溢出。
```C
void set_task_stack_end_magic(struct task_struct *tsk)
{
unsigned long *stackend;
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC; /* for overflow detection */
}
```
上述函数比较简单,`set_task_stack_end_magic`函数的作用是先通过`end_of_stack`函数获取堆栈并赋给 `task_struct`
关于检测配置需要打开内核配置宏`CONFIG_STACK_GROWSUP`。因为我们学习的是x86架构的初始化堆栈是逆生成所以堆栈底部为
```C
(unsigned long *)(task_thread_info(p) + 1);
```
`task_thread_info`的定义如下,返回一个当前的堆栈;
```C
#define task_thread_info(task) ((struct thread_info *)(task)->stack)
```
进程的栈底,我们写`STACK_END_MAGIC`这个值。如果设置`canary`,我们可以像这样子去检测堆栈:
```C
if (*end_of_stack(task) != STACK_END_MAGIC) {
//
// handle stack overflow here
//
}
```
`set_task_stack_end_magic` 初始化完毕后的下一个函数是 `smp_setup_processor_id`.此函数在`x86_64`架构上是空函数:
```C
void __init __weak smp_setup_processor_id(void)
{
}
```
在此架构上没有实现此函数,但在别的体系架构的实现可以参考[s390](http://en.wikipedia.org/wiki/IBM_ESA/390) and [arm64](http://en.wikipedia.org/wiki/ARM_architecture#64.2F32-bit_architecture).
我们接着往下走,下一个函数是`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](http://en.wikipedia.org/wiki/Time_Stamp_Counter):
```C
get_random_bytes(&canary, sizeof(canary));
tsc = __native_read_tsc();
canary += tsc + (tsc << 32UL);
```
我们要获取随机数, 我们可以给`stack_canary` 字段 `task_struct`赋值:
```C
current->stack_canary = canary;
```
然后将此值写入IRQ堆栈的顶部:
```C
this_cpu_write(irq_stack_union.stack_canary, canary); // read below about this_cpu_write
```
关于IRQ的章节我们这里也不会详细剖析, 关于这部分介绍看这里[IRQs](http://en.wikipedia.org/wiki/Interrupt_request_%28PC_architecture%29).如果canary被设置, 关闭本地中断注册bootstrap CPU以及CPU maps. 我们关闭本地中断 (interrupts for current CPU) 使用 `local_irq_disable` 函数,展开后原型为 `arch_local_irq_disable` 函数[include/linux/percpu-defs.h](https://github.com/torvalds/linux/blob/master/include/linux/percpu-defs.h):
```C
static inline notrace void arch_local_irq_enable(void)
{
native_irq_enable();
}
```
如果`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通过下面函数
```C
int cpu = smp_processor_id();
```
现在是0. 如果`CONFIG_DEBUG_PREEMPT` 宏配置了那么 `smp_processor_id` 的值就来自于 `raw_smp_processor_id` 函数,原型如下:
```C
#define raw_smp_processor_id() (this_cpu_read(cpu_number))
```
`this_cpu_read` 函数与其它很多函数一样如(`this_cpu_write`, `this_cpu_add` 等等...) 被定义在[include/linux/percpu-defs.h](https://github.com/torvalds/linux/blob/master/include/linux/percpu-defs.h) 此部分函数主要为对 `this_cpu` 进行操作. 这些操作提供不同的对每cpu[per-cpu](/Concepts/linux-cpu-1.md) 变量相关访问方式. 譬如让我们来看看这个函数 `this_cpu_read`:
```
__pcpu_size_call_return(this_cpu_read_, pcp)
```
还记得上面我们所写每cpu变量`cpu_number` 的值是`this_cpu_read`通过`raw_smp_processor_id`来得到,现在让我们看看 `__pcpu_size_call_return`的执行:
```C
#define __pcpu_size_call_return(stem, variable) \
({ \
typeof(variable) pscr_ret__; \
__verify_pcpu_ptr(&(variable)); \
switch(sizeof(variable)) { \
case 1: pscr_ret__ = stem##1(variable); break; \
case 2: pscr_ret__ = stem##2(variable); break; \
case 4: pscr_ret__ = stem##4(variable); break; \
case 8: pscr_ret__ = stem##8(variable); break; \
default: \
__bad_size_call_parameter(); break; \
} \
pscr_ret__; \
})
```
是的,此函数虽然看起起奇怪但是它的实现是简单的,我们看到`pscr_ret__` 变量的定义是`int`类型为什么是int类型呢好吧`变量``common_cpu` 它声明了每cpu(per-cpu)变量:
```C
DECLARE_PER_CPU_READ_MOSTLY(int, cpu_number);
```
在下一个步骤中我们调用了`__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
```C
#define this_cpu_read_4(pcp) percpu_from_op("mov", pcp)
```
需要调用`percpu_from_op` 并且通过`mov`指令来传递每cpu变量`percpu_from_op`的内联扩展如下:
```C
asm("movl %%gs:%1,%0" : "=r" (pfo_ret__) : "m" (common_cpu))
```
让我们尝试理解此函数是如果工作的,`gs`段寄存器包含每个CPU区域的初始值这里我们通过`mov`指令copy `common_cpu`到内存中去,此函数还有另外的形式:
```C
this_cpu_read(common_cpu)
```
等价于:
```C
movl %gs:$common_cpu, $pfo_ret__
```
由于我们没有设置每个CPU的区域,我们只有一个 - 为当前CPU的值`zero` 通过此函数 `smp_processor_id`返回.
返回的ID表示我们处于哪一个CPU上, `boot_cpu_init` 函数设置了CPU的在线, 激活, 当前的设置为:
```C
set_cpu_online(cpu, true);
set_cpu_active(cpu, true);
set_cpu_present(cpu, true);
set_cpu_possible(cpu, true);
```
上述我们所有使用的这些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或者第二个参数来这么调用
```C
cpumask_set_cpu(cpu, to_cpumask(cpu_possible_bits));
```
让我们继续尝试理解`to_cpumask`宏指令,此宏指令转化为一个位图通过`struct cpumask *`CPU掩码提供了位图集代表了当前系统中所有的CPU's每CPU都占用1bitCPU掩码相关定义通过`cpu_mask`结构定义:
```C
typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;
```
在来看下面一组函数定义了位图宏指令。
```C
#define DECLARE_BITMAP(name, bits) unsigned long name[BITS_TO_LONGS(bits)]
```
正如我们看到的定义一样, `DECLARE_BITMAP`宏指令的原型是一个`unsigned long`的数组,现在让我们查看如何执行`to_cpumask`:
```C
#define to_cpumask(bitmap) \
((struct cpumask *)(1 ? (bitmap) \
: (void *)sizeof(__check_is_bitmap(bitmap))))
```
我不知道你是怎么想的, 但是我是这么想的,我看到此函数其实就是一个条件判断语句当条件为真的时候,但是为什么执行`__check_is_bitmap`?让我们看看`__check_is_bitmap`的定义:
```C
static inline int __check_is_bitmap(const unsigned long *bitmap)
{
return 1;
}
```
原来此函数始终返回1事实上我们需要这样的函数才达到我们的目的 它在编译时给定一个`bitmap`,换句话将就是检查`bitmap`的类型是否是`unsigned long *`,因此我们仅仅通过`to_cpumask`宏指令将类型为`unsigned long`的数组转化为`struct cpumask *`。现在我们可以调用`cpumask_set_cpu` 函数,这个函数仅仅是一个 `set_bit`给CPU掩码的功能函数。所有的这些`set_cpu_*`函数的原理都是一样的。
如果你还不确定`set_cpu_*`这些函数的操作并且不能理解 `cpumask`的概念,不要担心。你可以通过读取这些章节[cpumask](/Concepts/linux-cpu-2.md) or [documentation](https://www.kernel.org/doc/Documentation/cpu-hotplug.txt).来继续了解和学习这些函数的原理。
现在我们已经激活第一个CPU我们继续接着start_kernel函数往下走下面的函数是`page_address_init`,但是此函数不执行任何操作,因为只有当所有内存不能直接映射的时候才会执行。
Linux 内核的第一条打印信息
---------------------------------------------------------------------------------
下面调用了pr_notice函数。
```C
#define pr_notice(fmt, ...) \
printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
```
pr_notice其实是printk的扩展这里我们使用它打印了Linux 的banner。
```C
pr_notice("%s", linux_banner);
```
打印的是内核的版本号以及编译环境信息:
```
Linux version 4.0.0-rc6+ (alex@localhost) (gcc version 4.9.1 (Ubuntu 4.9.1-16ubuntu6) ) #319 SMP
```
依赖于体系结构的初始化部分
---------------------------------------------------------------------------------
下个步骤我们就要进入到指定的体系架构的初始函数Linux 内核初始化体系架构相关调用`setup_arch`函数,这又是一个类型于`start_kernel`的庞大函数,这里我们仅仅简单描述,在下一个章节我们将继续深入。指定体系架构的内容,我们需要再一次阅读`arch/`目录,`setup_arch`函数定义在[arch/x86/kernel/setup.c](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/setup.c) 文件中,此函数就一个参数-内核命令行。
此函数解析内核的段`_text``_data`来自于`_text`符号和`_bss_stop`(你应该还记得此文件[arch/x86/kernel/head_64.S](https://github.com/torvalds/linux/blob/master/arch/x86/kernel/head_64.S#L46))。我们使用`memblock`来解析内存块。
```C
memblock_reserve(__pa_symbol(_text), (unsigned long)__bss_stop - (unsigned long)_text);
```
你可以阅读关于`memblock`的相关内容在[Linux kernel memory management Part 1.](/MM/linux-mm-1.md),你应该还记得`memblock_reserve`函数的两个参数:
* base physical address of a memory block;
* size of a memory block.
我们可以通过`__pa_symbol`宏指令来获取符号表`_text`段中的物理地址
```C
#define __pa_symbol(x) \
__phys_addr_symbol(__phys_reloc_hide((unsigned long)(x)))
```
上述宏指令调用 `__phys_reloc_hide` 宏指令来填充参数,`__phys_reloc_hide`宏指令在`x86_64`上返回的参数是给定的。宏指令 `__phys_addr_symbol`的执行是简单的,只是减去从`_text`符号表中读到的内核的符号映射地址并且加上物理地址的基地址。
```C
#define __phys_addr_symbol(x) \
((unsigned long)(x) - __START_KERNEL_map + phys_base)
```
`memblock_reserve`函数对内存页进行分配。
保留可用内存初始化initrd
---------------------------------------------------------------------------------
之后我们保留替换内核的text和data段用来初始化[initrd](http://en.wikipedia.org/wiki/Initrd),我们暂时不去了解initrd的详细信息你仅仅只需要知道根文件系统就是通过这种方式来进行初始化这就是`early_reserve_initrd` 函数的工作此函数获取RAM DISK的基地址、RAM DISK的大小以及RAM DISK的结束地址。
```C
u64 ramdisk_image = get_ramdisk_image();
u64 ramdisk_size = get_ramdisk_size();
u64 ramdisk_end = PAGE_ALIGN(ramdisk_image + ramdisk_size);
```
如果你阅读过这些章节[Linux Kernel Booting Process](/Booting/),你就知道所有的这些参数都来自于`boot_params`,时刻谨记`boot_params`在boot期间已经被赋值内核启动头包含了一下几个字段用来描述RAM DISK
```
Field name: ramdisk_image
Type: write (obligatory)
Offset/size: 0x218/4
Protocol: 2.00+
The 32-bit linear address of the initial ramdisk or ramfs. Leave at
zero if there is no initial ramdisk/ramfs.
```
我们可以得到关于 `boot_params`的一些信息. 具体查看`get_ramdisk_image`:
```C
static u64 __init get_ramdisk_image(void)
{
u64 ramdisk_image = boot_params.hdr.ramdisk_image;
ramdisk_image |= (u64)boot_params.ext_ramdisk_image << 32;
return ramdisk_image;
}
```
关于32位的ramdisk的地址我们可以阅读此部分内容来获取[Documentation/x86/zero-page.txt](https://github.com/0xAX/linux/blob/master/Documentation/x86/zero-page.txt):
```
0C0/004 ALL ext_ramdisk_image ramdisk_image high 32bits
```
32位变化后我们获取64位的ramdisk原理一样为此我们可以检查bootloader 提供的ramdisk信息
```C
if (!boot_params.hdr.type_of_loader ||
!ramdisk_image || !ramdisk_size)
return;
```
并保留内存块将ramdisk传输到最终的内存地址然后进行初始化
```C
memblock_reserve(ramdisk_image, ramdisk_end - ramdisk_image);
```
结束语
---------------------------------------------------------------------------------
以上就是第四部分关于内核初始化的部分内容,我们从`start_kernel`函数开始一直到指定体系架构初始化`setup_arch`的过程中停止,那么在下一个章节我们将继续研究体系架构相关的初始化内容。
如果你有任何的问题或者建议,你可以留言,也可以直接发消息给我[twitter](https://twitter.com/0xAX)。
**很抱歉,英语并不是我的母语,非常抱歉给您阅读带来不便,如果你发现文中描述有任何问题,请提交一个 PR 到 [linux-insides-zh](https://github.com/hust-open-atom-club/linux-insides-zh).**
链接
--------------------------------------------------------------------------------
* [GCC function attributes](https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html)
* [this_cpu operations](https://www.kernel.org/doc/Documentation/this_cpu_ops.txt)
* [cpumask](http://www.crashcourse.ca/wiki/index.php/Cpumask)
* [lock validator](https://www.kernel.org/doc/Documentation/locking/lockdep-design.txt)
* [cgroups](http://en.wikipedia.org/wiki/Cgroups)
* [stack buffer overflow](http://en.wikipedia.org/wiki/Stack_buffer_overflow)
* [IRQs](http://en.wikipedia.org/wiki/Interrupt_request_%28PC_architecture%29)
* [initrd](http://en.wikipedia.org/wiki/Initrd)
* [Previous part](/Initialization/linux-initialization-3.md)