内核入口
Table of Contents
start_kernel函数是与体系架构无关的通用处理入口函数,尽管在此初始化过程中要无数次的返回arch 文件夹 如果仔细看看start_kernel函数的内容,将发现此函数涉及内容非常广泛 在此过程中约包含了86个调用函数,是的,它真的是非常庞大 但是此部分并不是全部的初始化过程,在当前阶段只看这些就可以了 此章节以及后续所有在内核初始化过程章节的内容将涉及并详述它
start_kernel 函数的主要目的是完成内核初始化并启动 始祖进程 1号进程
在始祖进程启动之前start_kernel函数做了很多事情: 如锁验证器,根据处理器标识ID初始化处理器,开启cgroups子系统,设置每CPU区域环境 初始化VFS Cache机制,初始化内存管理,rcu,vmalloc,scheduler(调度器),IRQs(中断向量表),ACPI(中断可编程控制器)以及其它很多子系统 只有经过这些步骤才看到本章最后一部分祖先进程启动的过程
GCC attribute
start_kernel函数是定义在 init/main.c 从已知代码中能看到此函数使用了 __init特性 。在内核初始化阶段这个机制在所有的函数中都是有必要的:
#define __init __section(.init.text) __cold notrace
在初始化过程完成后,内核将通过调用 free_initmem 释放这些 sections 段 。__init属性是通过 __cold 和 notrace 两个属性来定义的:
- cold的目的是标记此函数很少使用,所以编译器必须优化此函数的大小
notrace定义如下:
#define notrace __attribute__((no_instrument_function))
含有no_instrument_function:告诉编译器函数调用不产生环境变量(堆栈空间)
在start_kernel函数的定义中,也可以看到 __visible 属性的扩展:
#define __visible __attribute__((externally_visible))
含有 externally_visible 意思就是告诉编译器有一些过程在使用该函数或者变量。可以在此 include/linux/init.h 处查到这些属性表达式的含义
start_kernel 初始化
在 start_kernel的初始之初可以看到这两个变量:
char *command_line; // 内核命令行的全局指针 char *after_dashes; // 包含parse_args函数通过输入字符串中的参数'name=value',寻找特定的关键字和调用正确的处理程序
这个时候不参与这两个变量的相关细节,但是会在接下来的章节看到
lockdep_init
接着往下走,下一步看到了此函数:
lockdep_init();
lockdep_init 初始化 lock validator . 其实现是相当简单的,它只是初始化了两个哈希表 list_head 并设置 lockdep_initialized 全局变量为 1
关于自旋锁 spinlock 以及 互斥锁mutex 如何获取 请参考链接 https://zh.wikipedia.org/wiki/%E8%87%AA%E6%97%8B%E9%94%81 https://zh.wikipedia.org/wiki/%E4%BA%92%E6%96%A5%E9%94%81
set_task_stack_end_magic
下一个函数是 set_task_stack_end_magic ,参数为 init_task 初始化进程(任务) 数据结构。这个函数会为栈底设置一个魔术数字 STACK_END_MAGIC : 0x57AC6E9D 来防止栈溢出攻击
struct task_struct init_task = INIT_TASK(init_task);
task_struct 存储了进程的所有相关信息,可以查看调度相关数据结构定义头文件 include/linux/sched.h
因为它很庞大,这本书并不会去介绍 现在 task_struct已经包含了超过100个字段! 虽然不会在这本书中看到关于task_struct的解释,但是会经常使用它 因为它是介绍在Linux内核进程的基本知识,接下来将描述这个结构中字段的一些含义
可以查看 宏INIT_TASK 的初始化流程。这个宏指令来自于 include/linux/init_task.h 这里只是设置和初始化了第一个进程来(0号进程)的值。例如:
- 初始化进程状态为 zero 或者 runnable. 一个可运行进程即为等待CPU去运行
- 初始化仅存的标志位 PF_KTHREAD: 意思为 内核线程
- 一个可运行的任务列表
- 进程地址空间
- 初始化进程堆栈 &init_thread_info:
init_thread_union.thread_info 和 init_thread_union 使用union thread_union
- thread_union 包含了 thread_info进程信息以及进程栈:
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代表一个联合体union而不是结构体
thread_info 定义如下:
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; };
thread_info结构包含了特定体系架构相关的线程信息,此结构占用52个字节 在X86_64架构上内核栈是逆生成而thread_union.thread_info结构则是正生长 进程进程栈是16KB并且thread_info是在栈底,因此可以使用的是:16KB - 52 bytes = 16332 bytes
用一张图来描述栈内存空间。 如下图所示:
e+-----------------------+ | | | | | stack | | | |_______________________| | | | | | | | | | |__________↓____________| +--------------------+ | | | | | thread_info |<----------->| task_struct | | | | | +-----------------------+ +--------------------+
现在回到set_task_stack_end_magic函数,这个函数被定义在 kernel/fork.c 功能为设置 init 进程堆栈以检测堆栈溢出 :
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 */ }
先通过 end_of_stack 函数获取堆栈并赋给 task_struct 。因为学习的是x86架构的初始化,堆栈是逆生成,所以堆栈底部为:
(unsigned long *)(task_thread_info(p) + 1);
在进程的栈底,写入STACK_END_MAGIC这个值
smp_setup_processor_id
下一个函数是 smp_setup_processor_id .此函数在x86_64架构上是空函数:
void __init __weak smp_setup_processor_id(void) { }
在此架构上没有实现此函数,但在别的体系架构的实现可以参考 arm64
debug_object_early_init
debug_object_early_init 函数的执行几乎和lockdep_init是一样的,但是填充的哈希对象是调试相关
boot_init_stack_canary
task_struct->canary 的值利用了GCC特性 但是此特性需要先使能内核CONFIG_CC_STACKPROTECTOR宏后才可以使用,否则什么也不做
boot_init_stack_canary 基于随机数和随机池产生 TSC :
get_random_bytes(&canary, sizeof(canary));
tsc = __native_read_tsc();
canary += tsc + (tsc << 32UL);
给当前字段的 stack_canary 字段赋值:
current->stack_canary = canary;
然后将此值写入IRQ 堆栈的顶部:
this_cpu_write(irq_stack_union.stack_canary, canary);
关于IRQ的章节这里也不会详细剖析
local_irq_disable
canary被设置后, 关闭本地中断 interrupts for current CPU 使用 local_irq_disable 函数,展开后原型为 arch_local_irq_disable 函数 include/linux/percpu-defs.h:
static inline notrace void arch_local_irq_disable(void) { native_irq_disable(); }
激活第一个CPU
当前已经走到start_kernel函数中的 boot_cpu_init 函数,此函数主要为了通过 掩码 初始化 每一个 CPU 。首先需要获取当前处理器的ID通过下面函数:
int cpu = smp_processor_id();
smp_processor_id 的值就来自于 raw_smp_processor_id 函数,原型如下:
#define raw_smp_processor_id() (this_cpu_read(cpu_number))
this_cpu_read 函数与其它很多函数一样如(this_cpu_write, this_cpu_add 等等…) 被定义在 include/linux/percpu-defs.h 这里函数主要为对 this_cpu 进行操作. 这些操作提供不同的对每cpuper-cpu 变量相关访问方式. 譬如来看看这个函数 this_cpu_read:
__pcpu_size_call_return(this_cpu_read_, pcp)
现在看看 __pcpu_size_call_return 的执行:
#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类型 , variable参数 是common_cpu 它声明了每cpu(per-cpu)变量:
DECLARE_PER_CPU_READ_MOSTLY(int, cpu_number);
- 调用了 __verify_pcpu_ptr 通过使用一个有效的 per-cpu变量指针 来取地址得到 cpu_number
通过pscr_ret__ 函数设置变量的大小,common_cpu变量是int,所以它的大小是4字节
意思就是通过this_cpu_read4(common_cpu)获取cpu变量,其大小被pscr_ret__决定
在__pcpu_size_call_return的结束 调用了 __pcpu_size_call_return :
#define this_cpu_read_4(pcp) percpu_from_op("mov", pcp)
需要调用 percpu_from_op 并且通过 mov 指令来传递每cpu变量,percpu_from_op的内联扩展如下:
asm("movl %%gs:%1,%0" : "=r" (pfo_ret__) : "m" (common_cpu))
gs段寄存器包含每个CPU区域的初始值,这里通过mov指令copy common_cpu到内存中去
此函数还有另外的形式:
this_cpu_read(common_cpu)
等价于:
movl %gs:$common_cpu, $pfo_ret__
由于没有设置每个CPU的区域, 并且只有一个 所以当前CPU的值zero 通过此函数 smp_processor_id返回
boot_cpu_init 函数设置了CPU的在线, 激活:
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 或 cpumask_clear_cpu 来改变状态。譬如可以通过第二个参数 true 来这么调用:
cpumask_set_cpu(cpu, to_cpumask(cpu_possible_bits));
继续尝试理解 to_cpumask宏 指令,此宏指令转化为一个位图:通过 struct cpumask 指针,CPU掩码提供了位图集代表了当前系统中所有的CPU's,每CPU都占用1bit,CPU掩码相关定义通过cpu_mask结构定义:
typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;
下面一组函数定义了位图宏指令:
#define DECLARE_BITMAP(name, bits) unsigned long name[BITS_TO_LONGS(bits)] // static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;
DECLARE_BITMAP宏指令的原型是一个unsigned long的数组
再来查看如何执行to_cpumask:
#define to_cpumask(bitmap) \ ((struct cpumask *)(1 ? (bitmap) \ : (void *)sizeof(__check_is_bitmap(bitmap))))
其实就是一个条件判断语句当条件为真的时候,但是为什么执行__check_is_bitmap?
看看 __check_is_bitmap 的定义:
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 或者 cpu-hotplug.来继续了解和学习这些函数的原理
现在已经激活第一个CPU,继续接着start_kernel函数往下走 下面的函数是page_address_init, 但是此函数不执行任何操作,因为只有当所有内存不能直接映射的时候才会执行
内核的第一条打印信息
下面调用了 pr_notice 函数:
#define pr_notice(fmt, ...) \ printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
pr_notice其实是 printk 的扩展,这里使用它打印了Linux 的banner:
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的庞大函数,这里仅仅简单描述 在下一节将继续深入,指定体系架构的内容
setup_arch函数定义在 arch/x86/kernel/setup.c 文件中,此函数就一个参数:内核命令行
这里使用 memblock 来解析内存块:
memblock_reserve(__pa_symbol(_text), (unsigned long)__bss_stop - (unsigned long)_text);
。此函数解析 内核段 _text 和 _data 来自于 _text符号和 _bss_stop
_text 和 _bss_stop 符号来自于文件arch/x86/kernel/head_64.S
memblock_reserve函数的两个参数:
- base physical address of a memory block
- size of a memory block
关于memblock的相关内容在 Linux kernel memory management Part 1
通过 __pa_symbol宏 指令来获取符号表 _text段 中的 物理地址 :
#define __pa_symbol(x) \ __phys_addr_symbol(__phys_reloc_hide((unsigned long)(x)))
- 调用 __phys_reloc_hide 宏 指令来填充参数,这个宏指令在x86_64上返回的参数是给定的
- 宏指令 __phys_addr_symbol 的执行是简单的,只是减去从 “_text符号表中读到的内核的符号映射地址” 并且加上 “物理地址的基地址”
#define __phys_addr_symbol(x) \ ((unsigned long)(x) - __START_KERNEL_map + phys_base)
memblock_reserve函数对内存页进行分配
保留可用内存初始化initrd
在内核text和data段中保留内存用来初始化initrd
暂时不去了解initrd的详细信息,仅仅只需要知道根文件系统就是通过这种方式来进行初始化
这就是 early_reserve_initrd 函数的工作,此函数获取 RAM DISK的基地址 、 RAM DISK的大小 以及 RAM DISK的结束地址 :
u64 ramdisk_image = get_ramdisk_image(); u64 ramdisk_size = get_ramdisk_size(); u64 ramdisk_end = PAGE_ALIGN(ramdisk_image + ramdisk_size);
如果阅读过前面Linux启动过程 ,就知道所有的这些参数都来自于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.
具体查看 get_ramdisk_image :
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的地址,可以阅读此部分内容来获取zero-page.txt:
0C0/004 ALL ext_ramdisk_image ramdisk_image high 32bits
获取64位的ramdisk原理一样,为此可以检查bootloader 提供的ramdisk信息:
if (!boot_params.hdr.type_of_loader || !ramdisk_image || !ramdisk_size) return;
校验成功后保留内存块,并将ramdisk传输到最终的内存地址,然后进行初始化:
memblock_reserve(ramdisk_image, ramdisk_end - ramdisk_image);
Next: 与系统架构有关的初始化 | Previous: 进入内核入口点之前最后的准备工作 | Home:内核初始化 |