目录
- 1. 前言
- 2.arch_call_rest_init
- |- - rcu_scheduler_starting
- |- -kernel_thread(kernel_init, NULL, CLONE_FS)
- |- -tsk = find_task_by_pid_ns(pid, &init_pid_ns);
- |- -set_cpus_allowed_ptr(tsk, smp_processor_id())
- |- -numa_default_policy
- |- -kernel_thread(kthreadd, ...)
- |- -find_task_by_pid_ns(pid, &init_pid_ns)
- |- -complete(&kthreadd_done)
- |--schedule_preempt_disabled
- |- -cpu_startup_entry(CPUHP_ONLINE)
- 3. kernel_init
- |- -kernel_init_freeable
- |- - -wait_for_completion(&kthreadd_done)
- |- - -gfp_allowed_mask = __GFP_BITS_MASK
- |- - -set_mems_allowed(node_states[N_MEMORY])
- |- - -set_cpus_allowed_ptr(current, cpu_all_mask)
- |- - -smp_prepare_cpus(setup_max_cpus)
- |- - -workqueue_init
- |- - -init_mm_internals
- |- - -do_pre_smp_initcalls()
- |- - -lockup_detector_init
- |- - -smp_init
- |- - -sched_init_smp
- |- - -padata_init
- |- - -page_alloc_init_late
- |- - -page_ext_init()
- |- - -do_basic_setup()
- |- - -kunit_run_all_tests()
- |- - -console_on_rootfs()
- |- - -integrity_load_keys()
- |- -async_synchronize_full()
- |- -kprobe_free_init_mem()
- |- -ftrace_free_init_mem()
- |- -free_initmem()
- |- -mark_readonly()
- |- -pti_finalize()
- |- -system_state = SYSTEM_RUNNING
- |- -numa_default_policy()
- |- -rcu_end_inkernel_boot()
- |- -do_sysctl_args()
- |- -run_init_process(...)
- 4.总结
1. 前言
本专题文章承接之前《kernel启动流程_head.S的执行》专题文章,我们知道在head.S执行过程中保存了bootloader传递的启动参数、启动模式以及FDT地址等,创建了内核空间的页表,最后为init进程初始化好了堆栈,并跳转到start_kernel执行。
本文重点介绍start_kernel的arch_call_rest_init的主要流程.
kernel版本:5.10
平台:arm64
2.arch_call_rest_init
arch_call_rest_init->rest_init
|--rcu_scheduler_starting()
|--pid = kernel_thread(kernel_init, NULL, CLONE_FS)
|--tsk = find_task_by_pid_ns(pid, &init_pid_ns);
|--set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
|--numa_default_policy()
|--pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)
|--kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns)
|--complete(&kthreadd_done)
|--schedule_preempt_disabled();
|--cpu_startup_entry(CPUHP_ONLINE)
|- - rcu_scheduler_starting
主要设置rcu_scheduler_active = RCU_SCHEDULER_INIT,标记调度器被激活。
从rcu_scheduler_active 变量的注释可以看到:
rcu_scheduler_active最初的值为RCU_SCHEDULER_INACTIVE,在第一个task创建后转换为RCU_SCHEDULER_INIT(本函数),此时RCU做一些硬件初始化(?), 当初始化完毕后会将状态转换为RCU_SCHEDULER_RUNNING
|- -kernel_thread(kernel_init, NULL, CLONE_FS)
kernel_thread会调用do_fork来创建kernel_init进程也就是1号进程,此时kernel_init进程并未调度执行,直到schedule_preempt_disabled被执行。
|- -tsk = find_task_by_pid_ns(pid, &init_pid_ns);
|- -set_cpus_allowed_ptr(tsk, smp_processor_id())
设置此进程只允许运行在当前执行的cpu?
|- -numa_default_policy
Reset policy of current process to default
|- -kernel_thread(kthreadd, …)
创建kthreadd内核线程,它的任务就是管理和调度其他内核线程kernel_thread,进程号为2
|- -find_task_by_pid_ns(pid, &init_pid_ns)
|- -complete(&kthreadd_done)
表示kthreadd已经创建完毕,唤醒阻塞的kernel_init进程,但是要等到schedule()调用时才能执行
|–schedule_preempt_disabled
schedule_preempt_disabled
|--sched_preempt_enable_no_resched()
|--schedule()
\--preempt_disable()
调用schedule()函数切换当前进程,调用该函数1号进程(kernel_init)将被执行
|- -cpu_startup_entry(CPUHP_ONLINE)
调用cpu_idle(),当前cpu执行的0号进程进入idle函数的循环,在该循环中会周期性地检查
3. kernel_init
前面说过kernel_thread会调用do_fork来创建kernel_init进程也就是1号进程,此时kernel_init进程并未调度执行,直到schedule_preempt_disabled被执行。下面来看下kernel_init主要做了哪些工作?
kernel_init
|--kernel_init_freeable()
|--async_synchronize_full()
|--kprobe_free_init_mem()
|--ftrace_free_init_mem()
|--free_initmem()
|--mark_readonly()
|--pti_finalize()
|--system_state = SYSTEM_RUNNING
|--numa_default_policy()
|--rcu_end_inkernel_boot()
|--do_sysctl_args()
\--run_init_process(...)
|- -kernel_init_freeable
kernel_init_freeable
|--wait_for_completion(&kthreadd_done)
|--gfp_allowed_mask = __GFP_BITS_MASK
|--set_mems_allowed(node_states[N_MEMORY])
|--cad_pid = task_pid(current)
|--smp_prepare_cpus(setup_max_cpus)
|--workqueue_init()
|--init_mm_internals()
|--do_pre_smp_initcalls()
|--lockup_detector_init()
|--smp_init()
|--sched_init_smp()
|--padata_init()
|--page_alloc_init_late()
|--page_ext_init()
|--do_basic_setup()
|--kunit_run_all_tests()
|--console_on_rootfs()
\--integrity_load_keys()
|- - -wait_for_completion(&kthreadd_done)
等待2号进程kthreadd创建完毕
|- - -gfp_allowed_mask = __GFP_BITS_MASK
Now the scheduler is fully set up and can do blocking allocations
|- - -set_mems_allowed(node_states[N_MEMORY])
init can allocate pages on any node
|- - -set_cpus_allowed_ptr(current, cpu_all_mask)
init can run on any cpu
|- - -smp_prepare_cpus(setup_max_cpus)
cpu唤醒其它cpu的准备工作,如设置其它cpu的启动地址
参考:https://www.cnblogs.com/linhaostudy/p/9371562.html
|- - -workqueue_init
之前workqueue_init_early主要是遍历所有cpu的worker_pool,对其执行初始化,并链入unbound_pool_hash,创建一系列system workqueue,此处workqueue_init初始化了pool的相关node等, 同时也会遍历unbound_pool_hash哈希表,为每个pool创建worker
|- - -init_mm_internals
init_mm_internals
|--mm_percpu_wq = alloc_workqueue("mm_percpu_wq", WQ_MEM_RECLAIM, 0)
|--cpuhp_setup_state_nocalls(CPUHP_MM_VMSTAT_DEAD...)
|--cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN,...)
|--init_cpu_node_state()
|--start_shepherd_timer()
|--proc_create_seq("buddyinfo", 0444, NULL, &fragmentation_op)
|--proc_create_seq("pagetypeinfo", 0400, NULL, &pagetypeinfo_op)
|--proc_create_seq("vmstat", 0444, NULL, &vmstat_op)
|--proc_create_seq("zoneinfo", 0444, NULL, &zoneinfo_op)
start_shepherd_timer用于创建worker,关于worker的作用如下(没看懂):
vmstat workers are used for folding counter differentials into the zone, per node and global counters at certain time intervals.
|- - -do_pre_smp_initcalls()
遍历执行由early_initcall声明的函数。
从do_pre_smp_initcalls的代码可以看出,遍历执行位于__initcall_start和__initcall0_start之间的初始化函数。通过vmlinux.lds可以看到,这部分位于.initcallearly.init段:
__initcall_start = .; KEEP(*(.initcallearly.init)) __initcall0_start = .;
关于初始化函数包含如下的分类:
/*
* Early initcalls run before initializing SMP.
*
* Only for built-in code, not modules.
*/
#define early_initcall(fn) __define_initcall(fn, early)
/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
|- - -lockup_detector_init
lockup_detector_init
|--watchdog_nmi_probe()
|--lockup_detector_setup()
watchdog死锁检测初始化
- watchdog_nmi_probe:检测nmi是否可用
- lockup_detector_setup:Create the watchdog thread infrastructure and configure the detector(s)
|- - -smp_init
smp_init
|--idle_threads_init()
|--cpuhp_threads_init()
|--smpboot_register_percpu_thread(&cpuhp_threads))
|--kthread_unpark(this_cpu_read(cpuhp_state.thread))
|--bringup_nonboot_cpus(setup_max_cpus)
1.idle_threads_init
为每个非boot cpu都各fork一个idle task,将获得的task_struct记录到per_cpu变量idle_threads中
2.cpuhp_threads_init
为每个core都创建一个"cpuhp/%u"内核线程,结果记录在per_cpu变量cpuhp_state.thread中,然后启动当前cpu的"cpuhp/%u"线程:“cpuhp/0”
线程处理函数为smpboot_thread_fn - percpu hotplug thread loop function
3.bringup_nonboot_cpus
bringup未启动的cpu core
bringup_nonboot_cpus
|--for_each_present_cpu(cpu)
if (!cpu_online(cpu))
cpu_up(cpu, CPUHP_ONLINE)
|--_cpu_up(cpu, 0, target)
|--cpu_present(cpu)判断cpu是否present
|--if (st->state == CPUHP_OFFLINE)
| idle = idle_thread_get(cpu)
|--cpuhp_tasks_frozen = tasks_frozen
|--cpuhp_set_state(st, target)
|--if (st->state > CPUHP_BRINGUP_CPU)
| cpuhp_kick_ap_work(cpu)
|--cpuhp_up_callbacks(cpu, st, target)
关于cpuhp的状态机如下图:
参考:http://www.bubuko.com/infodetail-3303003.html
上图左边是cpu up时的状态切换:OFFLINE -> BRINGUP_CPU -> AP_OFFLINE -> AP_ONLINE -> AP_ACTIVE
cpu_up(cpu, CPUHP_ONLINE)第二个参数表示target status,表示需要将第一个参数表示的cpu,唤醒,并达到指定的状态,目前cpu0就是处于CPUHP_ONLINE,这是cpu正常工作时的状态,这个函数会继续调用_cpu_up(cpu, 0, target):
1、检查cpu_present(cpu),正常情况下,为1
2、如果待唤醒的cpu的cpuhp_state.state大于target表示的状态,那么直接返回。数值越大,表示cpu越接近CPUHP_ONLINE,既然是bring up,那么当然是将state变大为目标
3、如果待处理cpu的当前状态是CPUHP_OFFLINE,那么要先获取该cpu的idle task结构,由于该cpu目前还没有上电,自然属于这种
4、调用cpuhp_set_state(st, target),设置待唤醒cpu的目标状态为CPUHP_ONLINE,如果当前状态小于target,将bringup设置为1,表示要执行up操作,返回的是当前状态
5、如果待处理cpu的当前状态大于CPUHP_BRINGUP_CPU,那么会执行cpuhp_kick_ap_work(cpu)。
6、下面的唤醒步骤分两个阶段:
(1)首先从OFFLINE到CPUHP_BRINGUP_CPU, 会调用cpuhp_up_callbacks完成
(2)然后剩下从CPUHP_BRINGUP_CPU到CPUHP_ONLINE则通过AP hotplug thread进行,这个线程就是上面创建的"cpuhp/0", 它的task_struct存放在per_cpu变量cpuhp_state.thread中
4.smp_init小结
smp_init为每个非boot cpu都各fork一个idle task,为每个core创建了"cpuhp/%u"内核线程,同时bringup未启动的cpu core
|- - -sched_init_smp
sched_init_smp
|--sched_init_numa
|--sched_init_domains(cpu_active_mask)
|--sched_init_granularity()
|--init_sched_rt_class()
|--init_sched_dl_class()
|--sched_smp_initialized = true
- sched_init_domains
from:https://blog.csdn.net/u013836909/article/details/94206035
遍历cpu_active_mask中的所有CPU,对每个CPU遍历所有的SDTL,相当于每个CPU都有自己的一套SDTL对应的调度域,为每个CPU都初始化一整套SDTL对应的调度域和调度组。遍历cpu_active_mask中的所有CPU的调度域,创建并初始化调度组。
注:内核有一个数据结构struct sched_domain_topology_level来描述CPU的层次关系,简称SDTL,请见[/include/linux/sched/topology.h:186]
|- - -padata_init
创建padata_works
Padata是一种机制,通过这种机制,内核可以将工作外包出去,以在在多个CPU上并行,同时可以选择保留它们的排序。它最初是为IPsec开发的,因为IPsec需要对大量的数据包进行加密和解密,而不需要对这些数据包重新排序。 这是目前padata的序列化作业支持的唯一用户。Padata还支持多线程作业,将作业平均拆分,同时对线程之间进行负载均衡和协调。
参考:Documentation/core-api/padata.rst
|- - -page_alloc_init_late
|- - -page_ext_init()
|- - -do_basic_setup()
do_basic_setup
|--cpuset_init_smp();
|--driver_init();
|--init_irq_proc();
|--do_ctors();
|--usermodehelper_enable();
|--do_initcalls();
|....
|--rootfs_initcall(populate_rootfs)
|....
do_basic_setup最主要的是会执行init段的函数,其中会调用到rootfs_initcall,它会将initrd释放到rootfs的/目录,这里可参考《start_kernel之rootfs挂载及cpio initrd解包》
|- - -kunit_run_all_tests()
|- - -console_on_rootfs()
通过filp_open("/dev/console", O_RDWR, 0)得到 struct file *file,然后又使用init_dup(file)执行了3次,一共得到了3个文件描述符。
init_dup(file)实际执行了如下操作:
设置current->files->fdt->fd[fd]为file,这里fd分别为0,1,2,就是所谓的:标准输入、标准输出、标准错误
|- - -integrity_load_keys()
|- -async_synchronize_full()
等待所有的init函数调用完毕,为释放init段做准备
|- -kprobe_free_init_mem()
|- -ftrace_free_init_mem()
|- -free_initmem()
释放__init_begin~__init_end的区域
|- -mark_readonly()
|- -pti_finalize()
|- -system_state = SYSTEM_RUNNING
|- -numa_default_policy()
|- -rcu_end_inkernel_boot()
|- -do_sysctl_args()
解析command line中sysctl参数,并对齐执行相应的回调process_sysctl_arg
|- -run_init_process(…)
启动init进程
4.总结
rest_init最重要的就是创建kernel_init进程也就是1号进程 和 kthreadd进程也就是2号进程,并执行1号进程和2号进程的处理函数。
1号进程主要负责启动secondary core, 执行init段的初始化函数,这期间会将initrd释放到根文件系统中,并执行其中的init进程,1号进程的启动时机是2号进程创建完毕;
2号进程用于管理调度其它进程
0号进程在启动完1号和2号进程,加入到idle调度类,自身退化为idle进程