本专题文章承接之前《kernel启动流程_head.S的执行》专题文章,我们知道在head.S执行过程中保存了bootloader传递的启动参数、启动模式以及FDT地址等,创建了内核空间的页表,最后为init进程初始化好了堆栈,并跳转到start_kernel执行。
本文重点介绍start_kernel的arch_call_rest_init的主要流程.
kernel版本:5.10
平台:arm64
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_active = RCU_SCHEDULER_INIT,标记调度器被激活。
从rcu_scheduler_active 变量的注释可以看到:
rcu_scheduler_active最初的值为RCU_SCHEDULER_INACTIVE,在第一个task创建后转换为RCU_SCHEDULER_INIT(本函数),此时RCU做一些硬件初始化(?), 当初始化完毕后会将状态转换为RCU_SCHEDULER_RUNNING
kernel_thread会调用do_fork来创建kernel_init进程也就是1号进程,此时kernel_init进程并未调度执行,直到schedule_preempt_disabled被执行。
设置此进程只允许运行在当前执行的cpu?
Reset policy of current process to default
创建kthreadd内核线程,它的任务就是管理和调度其他内核线程kernel_thread,进程号为2
表示kthreadd已经创建完毕,唤醒阻塞的kernel_init进程,但是要等到schedule()调用时才能执行
schedule_preempt_disabled
|--sched_preempt_enable_no_resched()
|--schedule()
\--preempt_disable()
调用schedule()函数切换当前进程,调用该函数1号进程(kernel_init)将被执行
调用cpu_idle(),当前cpu执行的0号进程进入idle函数的循环,在该循环中会周期性地检查
前面说过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
|--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()
等待2号进程kthreadd创建完毕
Now the scheduler is fully set up and can do blocking allocations
init can allocate pages on any node
init can run on any cpu
cpu唤醒其它cpu的准备工作,如设置其它cpu的启动地址
参考:https://www.cnblogs.com/linhaostudy/p/9371562.html
之前workqueue_init_early主要是遍历所有cpu的worker_pool,对其执行初始化,并链入unbound_pool_hash,创建一系列system workqueue,此处workqueue_init初始化了pool的相关node等, 同时也会遍历unbound_pool_hash哈希表,为每个pool创建worker
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.
遍历执行由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
|--watchdog_nmi_probe()
|--lockup_detector_setup()
watchdog死锁检测初始化
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)
参考: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_numa
|--sched_init_domains(cpu_active_mask)
|--sched_init_granularity()
|--init_sched_rt_class()
|--init_sched_dl_class()
|--sched_smp_initialized = true
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_works
Padata是一种机制,通过这种机制,内核可以将工作外包出去,以在在多个CPU上并行,同时可以选择保留它们的排序。它最初是为IPsec开发的,因为IPsec需要对大量的数据包进行加密和解密,而不需要对这些数据包重新排序。 这是目前padata的序列化作业支持的唯一用户。Padata还支持多线程作业,将作业平均拆分,同时对线程之间进行负载均衡和协调。
参考:Documentation/core-api/padata.rst
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解包》
通过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,就是所谓的:标准输入、标准输出、标准错误
等待所有的init函数调用完毕,为释放init段做准备
释放__init_begin~__init_end的区域
解析command line中sysctl参数,并对齐执行相应的回调process_sysctl_arg
启动init进程
rest_init最重要的就是创建kernel_init进程也就是1号进程 和 kthreadd进程也就是2号进程,并执行1号进程和2号进程的处理函数。
1号进程主要负责启动secondary core, 执行init段的初始化函数,这期间会将initrd释放到根文件系统中,并执行其中的init进程,1号进程的启动时机是2号进程创建完毕;
2号进程用于管理调度其它进程
0号进程在启动完1号和2号进程,加入到idle调度类,自身退化为idle进程