kernel启动流程-start_kernel的执行_1.概述

1.前言

本专题文章承接之前《kernel启动流程_head.S的执行》专题文章,我们知道在head.S执行过程中保存了bootloader传递的启动参数(保存在boot_args数组)、启动模式(保存在__boot_cpu_mode)以及FDT地址(__fdt_pointer)等,创建了内核空间的页表,最后为init进程初始化好了堆栈,并跳转到start_kernel执行。
本专题文章就是简要介绍start_kernel的启动流程,并对进程、内存、IO等的初始化部分做重点分析,本文是start_kernel的概述部分,总体介绍start_kernel包含的主要流程。

kernel版本:5.10
平台:arm64

2. start_kernel总体流程

set_task_stack_end_magic(&init_task);

init_task定义在init/init_task.c,此处在init_task栈底存入魔数,将来用于栈溢出检测,通过gdb可以查看到:
在这里插入图片描述
如上0xffff800011730000处就是init_task.stack的地址,存放的魔数为0x57ac6e9d
#define STACK_END_MAGIC 0x57AC6E9D

smp_setup_processor_id();

将当前CPU的ID保存到__cpu_logical_map[0] ,它是从MPIDR_EL1寄存器中读出的,它通过AF3~AF0组成了多级ID,通过AF3.AF2.AF1.AF0可以找到具体的某个core,且每个CORE的id在运行期间是不会改变的,如下为kernel的第一条打印:

Booting Linux on physical CPU 0x0000000000 [0x411fd070]

其中0x411fd070为MIDR_EL1的值,它保存了CPU ID信息

cgroup_init_early();

对Control Groups进行早期的初始化,主要是对init进程的cgroups及subsystem进行初始化(TODO)

关中断执行必要的设置

local_irq_disable();
early_boot_irqs_disabled = true;

在关闭中断的前提下,执行一些必要的设置

boot_cpu_init();

首先通过smp_processor_id获取当前运行的cpu,它主要保存在per_cpu变量cpu_number中,激活第一个cpu,这里第一个cpu为0号CPU,主要是通过设置cpu_mask结构体对应bit

from include/linux/cpumask.h:
cpu_possible_mask- has bit ‘cpu’ set iff cpu is populatable
cpu_present_mask - has bit ‘cpu’ set iff cpu is populated
cpu_online_mask - has bit ‘cpu’ set iff cpu available to scheduler
cpu_active_mask - has bit ‘cpu’ set iff cpu available to migration

page_address_init();

初始化高端内存的,arm64没有用到

early_security_init();

linux security module的早期初始化

setup_arch(&command_line);

setup_arch主要是处理器架构相关的处理,主要工作包含如下:

  • 获取并初始化全局处理器信息;
  • 获取fdt相关的信息,添加可用内存信息到memblock.memory,获取command_line;
  • 初始化init进程的内存结构体init_mm;
  • 由于内存子系统未初始化,创建固定映射区,用于早期IO等的页表映射;
  • 添加预留区域到memblock.reserved区域并创建CMA/DMA
  • 为memblock.memory区域创建struct page
setup_boot_config(command_line);

通过setup_arch(&command_line);->setup_machine_fdt函数中会解析chosen中的bootargs命令行存放到boot_command_line,也会返回给command_line参数,此处setup_boot_config主要用于解析command_line中的bootconfig选项

setup_command_line(command_line);

保存command_line(也就是boot_command_line)分别保存在saved_command_line和static_command_line,如果存在extra_command_line,则先保存extra_command_line再保存boot_command_line

setup_nr_cpu_ids();

设置cpu的数目

setup_per_cpu_areas();
setup_per_cpu_areas
    |--pcpu_embed_first_chunk(PERCPU_MODULE_RESERVE,...)
    |--for_each_possible_cpu(cpu) 
           __per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu];

在SMP系统中,setup_per_cpu_areas初始化使用per_cpu宏定义的静态per-cpu变量,这种变量对系统中的每个cpu都有一个独立的副本。此类变量保存在内核二进制映像的一个独立的段中。setup_per_cpu_areas的目的就是为系统的各个CPU分别创建一份这些数据的副本。

from:https://blog.csdn.net/yin262/article/details/46787879
每CPU变量主要是数据结构的数组,系统的每个CPU对应数组的一个元素。一个CPU不应该访问与其他CPU对应的数组元素,另外,它可以随意读或修改它自己的元素而不用担心出现竞争条件,因为它是唯一有资格这么做的CPU。但是,这也意味着每CPU变量基本上只能在特殊情况下使用,也就是当它确定在系统的CPU上的数据在逻辑上是独立的时候。每CPU的数组元素在主存中被排列以使每个数据结构存放在硬件高速缓存的不同行,因此,对每CPU数组的并发访问不会导致高速缓存行的窃用和失效(这种操作会带来昂贵的系统开销)。虽然每CPU变量为来自不同CPU的并发访问提供保护,但对来自异步函数(中断处理程序和可延迟函数)的访问不提供保护,在这种情况下需要另外的同步技术。此外,在单处理器和多处理器系统中,内核抢占都可能使每CPU变量产生竞争条件。总的原则是内核控制路径应该在禁用抢占的情况下访问每CPU变量。因为当一个内核控制路径获得了它的每CPU变量本地副本的地址,然后它因被抢占而转移到另外一个CPU上,但仍然引用原来CPU元素的地址,这是非常危险的。
每CPU变量的声明和普通变量的声明一样,主要的区别是使用了attribute((section(PER_CPU_BASE_SECTION sec)))来指定该变量被放置的段中,普通变量默认会被放置data段或者bss段中。
看到这里有一个问题:我们只是声明了一个变量,那么如果有多个副本的呢?奥妙在于内核加载的过程。
一般情况下,ELF文件中的每一个段在内存中只会有一个副本,而.data…percpu段再加载后,又被复制了NR_CPUS次,一个每CPU变量的多个副本在内存中是不会相邻。
分配内存以及复制.data.percup内容的工作由pcpu_embed_first_chunk来完成。__per_cpu_offset数组中记录了每个CPU的percpu区域的开始地址。我们访问每CPU变量就要依靠__per_cpu_offset中的地址

smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */

todo

boot_cpu_hotplug_init();

todo

build_all_zonelists(NULL);

构建zonelist,伙伴分配器从zonelist分配,从MAX_NR_ZONES开始设置到zonelist->zoneref[0]中

page_alloc_init();

内存页初始化,主要是在cpu发生热插拔时将CPU管理的页面移动到空闲列表

/* parameters may set static keys */
jump_label_init();
parse_early_param();

解析需要’早期’处理的启动参数用
early_param会根据参数定义结构体,包含了回调函数,并将其放置在.init.setup段,parse_early_param将根据boot_command_line进行解析,解析xx=yy的格式,如果xx与.init.setup中结构体匹配则执行它的回调函数;需要注意的是__setup与early_param类似,都会将所定义的参数放入.init.setup段,只不过early_param定义的参数将通过parse_early_param在启动阶段解析

after_dashes = parse_args("Booting kernel",
                  static_command_line, __start___param,
                  __stop___param - __start___param,
                  -1, -1, NULL, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
           NULL, set_init_arg);
if (extra_init_args)
parse_args("Setting extra init args", extra_init_args,
           NULL, 0, -1, -1, NULL, set_init_arg);
[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
[    0.000000] Linux version 5.10.0-dirty (ubuntu@VM-0-9-ubuntu) (aarch64-linux-gnu-gcc (Linaro GCC 5.4-2017.01) 5.4.1 20161213, GNU ld (Linaro_Binutils-2017.01) 2.25.2 Linaro 2016_02) #4 SMP PREEMPT Tue Dec 15 14:10:44 CST 2020
setup_log_buf(0);
vfs_caches_init_early();
sort_main_extable();
trap_init();
mm_init();
ftrace_init();

如上将使用较大的memory,要求必须在kmem_cache_init之前?

  • setup_log_buf:使用memblock_alloc分配一个启动时log缓冲区
  • vfs_caches_init_early:初始化dentry和inode的hashtable
  • sort_main_extable:对内核异常向量表进行排序
  • trap_init:对内核陷阱异常进行初始化
  • mm_init: 主要功能就是将memblock管理的空闲内存释放到伙伴系统
  • ftrace_init:ftrace子系统初始化
/* trace_printk can be enabled here */
early_trace_init(); 

此处trace_printk被使能

sched_init();

初始化进程调度器,此调度器可用,但是完整的调度器配置在smp_init()
sched_init主要完成了如下的工作:

  1. 初始化了root task group,包含每个cpu core的调度实体,调度队列,以及配额时间和调度周期;
  2. 初始化root调度域,调度域是负载均衡的基本单位;
  3. 初始化每个cpu的运行队列rq
  4. 初始化init进程的负载权值,它与运行时间相关;
  5. 初始化idle进程
preempt_disable();

此时调度器还很脆弱,在cpu_idle()之前必须被禁用?

radix_tree_init();

初始化内核基数树

housekeeping_init(); 
workqueue_init_early();

workqueue早期初始化,初始化完毕后,就可以允许创建workqueue,并允许work入队/出队, 但是work的执行要等到线程可以创建时才行.
workqueue_init_early主要是遍历所有cpu的worker_pool,对其执行初始化,将所有的worker_pool加入到unbound_pool_hash哈希表,并创建一系列system workqueue, 包括:system_wq, system_highpri_wq,system_long_wq,system_unbound_wq,system_freezable_wq,system_power_efficient_wq,system_freezable_power_efficient_wq

rcu_init();
trace_init(); 
initcall_debug_enable();

from: https://blog.csdn.net/zangdongming/article/details/37769265
initcall_debug是一个内核参数,可以跟踪initcall,用来定位内核初始化的问题。在cmdline中增加initcall_debug后,内核启动过程中会增加如下形式的日志,在调用每一个init函数前有一句打印,结束后再有一句打印并且输出了该Init函数运行的时间,通过这个信息可以用来定位启动过程中哪个init函数运行失败以及哪些init函数运行时间较长。
calling init_workqueues+0x0/0x414 @ 1
initcall init_workqueues+0x0/0x414 returned 0 after 0 usecs
除了在启动过程中会增加日志外,在系统休眠唤醒过程中也会增加如下形式的日志,可以用来定位休眠唤醒失败及休眠唤醒时间太长的问题。
calling xxxxxx.dma+ @ 6, parent: xxx.0
call xxxxxx.dma+ returned 0 after 2 usecs

context_tracking_init();
/* init some links before init_ISA_irqs() */   
early_irq_init(); 

初始化中断数目nr_irqs,并通过for循环为每个中断分配中断描述符irq_desc,置位allocated_irqs表示该中断已经分配中断描述符,irq_insert_desc将分配的中断描述符插入到irq_desc_tree基数树

init_IRQ();

init_IRQ:初始化中断,包括中断栈的初始化,中断控制器的初始化

tick_init(); 

初始化时钟滴答控制器,主要包含tick broadcart和tick nohz初始化

关于内核时间子系统对底层硬件的抽象:

参考:https://blog.csdn.net/flaoter/article/details/77413163
对于ARM64底层硬件而言,有一个全局的global counter,同时每个cpu core有一个本地的local timer,linux据此抽象出了clock event和clock source。
(1) clock source实现计时功能,linux内核有各种time line, 包括real time clock, monotonic clock, monotonic raw clock等。clocksource提供了一个单调增加的计时器产生tick,为timeline提供时钟源。timekeeper是内核提供时间服务的基础模块,负责选择并维护最优的clocksource;
(2) clock event实现定时功能。clock event管理可产生event或是触发中断的定时器, 一般而言,每个CPU形成自己的一个小系统,也就要管理自己的clock event。tick device是基于clock event设备进行工作的,cpu管理自己的调度、进程统计等是基于tick device的。低精度timer和高精度timer都是基于tick device生成的定时器设备

内核如何选取tick device用于产生进程调度的tick中断?

from: http://www.wowotech.net/timer_subsystem/periodic-tick.html
在multi core的环境下,每一个CPU core都自己的tick device(可以称之local tick device),这些tick device中有一个被选择做global tick device,负责维护整个系统的jiffies。如果该tick device的是第一次设定,并且目前系统中没有global tick设备,那么可以考虑选择该tick设备作为global设备,进行系统时间和jiffies的更新。更细节的内容请参考timekeeping文档

关于tick broadcast:

当某个cpu core进入idle状态时,有可能出于power saving的考虑,会停掉当前cpu core的local timer,此时如何维护该cpu core的timer事件,就需要一个全局的HW timer(在ARM64中为global counter?)或某一个本地的tick devcie来负责唤醒idle状态的cpu core,这个全局的HW timer就是tick broadcast device
参考:
http://www.wowotech.net/timer_subsystem/tick-broadcast-framework.html
https://lwn.net/Articles/574962/

关于nohz

from: https://blog.csdn.net/flaoter/article/details/77413163
周期性的时钟中断,设计起来简单,但是它本身有个严重的缺点:这个时钟中断必须定时的周期产生,不管处理器当前是忙,还是空闲。如果处理器处于空闲状态,那也必须每1ms(或10ms…)被唤醒一次,做一些简单的统计工作,然后进入空闲状态。相当于做了无用功,并且增加了系统能耗。Kernel引入nohz模式后,当CPU处于空闲状态时,系统直接关掉这个周期性的时钟中断。例如,如果2s之后才有一个定时器到期,那么CPU会一直空闲等待2s,直到定时器时钟中断将他唤醒。但是,禁止周期性的时钟中断,并不意味着禁止了其他的中断。系统的其他设备中断依然可以得到CPU的响应,系统调用(syscall)也依旧工作

rcu_init_nohz(); 
init_timers();

init_timers:初始化各个cpu core的timer, 注册timer软中断

hrtimers_init(); 

hrtimers_init:初始各个cpu core的hr timer,注册hr timer软中断

softirq_init(); 

初始化各个cpu core的tasklet和tasklet_hi链表,注册tasklet和tasklet_hi软中断

timekeeping_init();

初始化时钟源和common timekeeping values

from:http://www.wowotech.net/timer_subsystem/timekeeping.html
timekeeping模块是一个提供时间服务的基础模块。Linux内核提供各种time line,real time clock,monotonic clock、monotonic raw clock等,timekeeping模块就是负责跟踪、维护这些timeline的,并且向其他模块(timer相关模块、用户空间的时间服务等)提供服务,而timekeeping模块维护timeline的基础是基于clocksource模块和tick模块。通过tick模块的tick事件,可以周期性的更新time line,通过clocksource模块、可以获取tick之间更精准的时间信息。

rand_initialize(); 
add_latent_entropy();  
add_device_randomness(command_line, strlen(command_line));
boot_init_stack_canary();  

随机化相关

time_init();

time初始化。
通过of_clk_init扫描并初始化DT中的clock;
通过timer_probe遍历通过TIMER_OF_DECLARE声明的timer,执行其声明的回调函数对timer进行初始化

perf_event_init();
profile_init();
call_function_init();
early_boot_irqs_disabled = false;
local_irq_enable(); 

重开中断

kmem_cache_init_late();

初始化slab分配器的缓存机制

console_init();

初始化控制台,主要是执行由console_initcall宏声明的函数回调

lockdep_init();

打印锁的依赖信息

mem_encrypt_init(); 
 setup_per_cpu_pageset();    

setup_per_cpu_pageset为zone中的pageset数组的第一个元素分配内存,也就是为第一个cpu分配,系统的所有的内存域都会考虑进来。该函数还负责设置冷热分配器的限制,在SMP系统上对应于其它CPU的pageset数组成员,将会在相应的CPU激活时初始化。

from: https://blog.csdn.net/u013592097/article/details/51698468
所谓的pcp是每cpu页框高速缓冲,由数据结构struct per_cpu_pageset描述,包含在内存域struct zone中。在内核中,系统会经常请求和释放单个页框,如果每个cpu高速缓存包含一些预先分配的页框,被用于满足本地cpu发出的单一内存请求,就能提升系统的性能。 setup_zone_pageset完成对每个cpu每个zone的pageset遍历,利用pageset_init()初始化pcp链表,和利用pageset_set_high_and_batch为每个pageset计算每次在高速缓存中将要添加或被删去的页框个数

numa_policy_init();     

初始化NUMA的内存访问策略,将所有numa节点的内存分配策略设置为MPOL_PREFERRED,即有限从指定的numa节点上分配内存

参考:Documentation/admin-guide/mm/numa_memory_policy.rst

acpi_early_init();    
 late_time_init();   
sched_clock_init();    

初始化调度器时钟

calibrate_delay();      

延时校准

pid_idr_init();   

初始化进程pid位图

anon_vma_init();         

创建anon_vma的slab缓存

thread_stack_cache_init();       

创建进程thread_info的slab高速缓存

cred_init();        

创建任务信用系统的slab高速缓存

fork_init();  

初始化进程创建机制.
为task_struct创建slab高速缓存,根据系统资源计算出最大的线程数量,存放到全局max_threads

关于最大线程数可以参考:
https://www.169it.com/tech-qa-linux/article-7178382663516895179.html

proc_caches_init(); 

创建进程所需的各结构体slab高速缓存

uts_ns_init();      
buffer_init();           

为buffer_head结构体创建slab高速缓存

key_init();      

初始化内核密钥管理系统

security_init();       

初始化内核安全框架

dbg_late_init();       

初始化内核调试模块kdb

vfs_caches_init();       

初始化虚拟文件系统。
创建denty,inode,filp的slab高速缓存;设定支持的最大文件数量;通过mnt_init来mount rootfs(真正的根文件系统,内核挂载的第一个文件系统);创建bdev_cache的slab高速缓存并注册并挂载bdev文件系统;创建cdev_map用于管理字符设备号与字符设备的映射关系

pagecache_init();       
  1. 初始化等待队列哈希表,大小为PAGE_WAIT_TABLE_SIZE,在页面不可用时,用于维护等待页面的waiters;哈希表的每个bucket代表一个等待队列,通过page地址计算哈希值,来选取waiter放入哪个等待队列。
  2. 通过page_writeback_init来调整page writeback dirty limits
signals_init();      

创建信号队列slab高速缓存

seq_file_init();        

为seq file创建slab高速缓存

proc_root_init();    

proc文件系统初始化

nsfs_init();     

名称空间文件系统初始化

cpuset_init();   

初始化cpuset

cgroup_init();    

control group正式初始化

taskstats_init_early();     

任务状态早期初始化函数,创建高速缓存并初始化互斥机制

delayacct_init();      

Initialization code related to collection of per-task “delay” statistics which measure how long it had to wait for cpu, sync block io, swapping etc

poking_init();         
check_bugs();  
acpi_subsystem_init(); 
arch_post_acpi_subsys_init();    
sfi_init_late();      
kcsan_init();        
arch_call_rest_init();

调用rest_init, rest_init最重要的就是创建kernel_init进程(也就是1号进程) 和 kthreadd进程(也就是2号进程),并执行(1号进程和2号进程)对应的处理函数。

kernel_init进程主要负责启动secondary core, 执行init段的初始化函数,这期间会将initrd释放到根文件系统中,并执行其中的init进程,1号进程的启动时机是2号进程创建完毕;

kthreadd进程用于管理调度其它进程

0号进程在启动完kernel_init和kthreadd进程,加入到idle调度类,自身退化为idle进程

prevent_tail_call_optimization();

参考文档

  1. 早期内存分配器:memblock
  2. https://blog.csdn.net/zangdongming/article/details/37769265
  3. http://www.wowotech.net/timer_subsystem/periodic-tick.html
  4. https://blog.csdn.net/flaoter/article/details/77413163
  5. http://www.wowotech.net/timer_subsystem/timekeeping.html
  6. https://blog.csdn.net/u013592097/article/details/51698468
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值