Linux启动流程-kernel
一.Preface
内核的启动过程涉及到许多内容,且包含许多细节。如果熟悉kernel的启动过程会不仅能加深对Linux系统的理解,而且对于定位kernel启动崩溃的问题、ARM架构的理解也有帮助。这里以imx6ull为例,做一个初步的总结。
二.启动kernel的入口。
1."vmlinux.lds"链接脚本文件
内核的链接脚本文件"arch/arm/kernel/vmlinux.lds",通过链接脚本可以找到内核的第一行程序是从哪里执行的。vmlinux.lds 中有如下代码。
点击查看代码
#if (defined(CONFIG_SMP_ON_UP) && !defined(CONFIG_DEBUG_SPINLOCK)) || \
defined(CONFIG_GENERIC_BUG)
#define ARM_EXIT_KEEP(x) x
#define ARM_EXIT_DISCARD(x)
#else
#define ARM_EXIT_KEEP(x)
#define ARM_EXIT_DISCARD(x) x
#endif
OUTPUT_ARCH(arm)
ENTRY(stext) @ 内核的第一行程序入口
2.内核入口
1.ENTRY(stext)在"arch/arm/kernel/head.S"文件中被定义
点击查看代码
.arm
__HEAD
ENTRY(stext) @ 入口
ARM_BE8(setend be ) @ ensure we are in BE8 mode
THUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM.
THUMB( bx r9 ) @ If this is a Thumb-2 kernel,
THUMB( .thumb ) @ switch to Thumb now.
THUMB(1: )
@ ensure svc mode and all interrupts masked
safe_svcmode_maskall r9 @确保 CPU 处于 SVC 模式,并且关闭了所有的中断。
mrc p15, 0, r9, c0, c0 @ 从协处理器CP15 c0寄存器中获取cpu_id保存到cpu_r9寄存器中
bl __lookup_processor_type @ r5=procinfo r9=cpuid 检查当前系统是否支持此 CPU,如果支持就获取 procinfo 信 息。
movs r10, r5 @ invalid processor (r5=0)?
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p @ yes, error 'p'
/*
* r1 = machine no, r2 = atags or dtb,
* r8 = phys_offset, r9 = cpuid, r10 = procinfo
*/
bl __vet_atags @验证设备树的合法性
bl __create_page_tables @创建页表
/*
* The following calls CPU specific code in a position independent
* manner. See arch/arm/mm/proc-*.S for details. r10 = base of
* xxx_proc_info structure selected by __lookup_processor_type
* above. On return, the CPU will be ready for the MMU to be
* turned on, and r0 will hold the CPU control register value.
*/
@将函数__mmap_switched 的地址保存到 r13 寄存器中,__mmap_switched 最终会调用 start_kernel 函数
ldr r13, =__mmap_switched @ address to jump to after mmu has been enabled
adr lr, BSYM(1f) @ return (PIC) address
mov r8, r4 @ set TTBR1 to swapper_pg_dir
ldr r12, [r10, #PROCINFO_INITFUNC]
add r12, r12, r10
ret r12
1: b __enable_mmu @使 能 MMU;它通过调用__turn_mmu_on 打开 MMU,__turn_mmu_on 执行 r13 里面保存的__mmap_switched @函数,最终调用start_karnel
ENDPROC(stext)
.ltorg
2.__mmap_switched函数被定义在在"arch/arm/kernel/head-common.S"文件中
点击查看代码
__mmap_switched:
adr r3, __mmap_switched_data @将标签(__mmap_switched_data)的地址加载到寄存器r3中。这个地址包含了一些重要的数据,比如内存映射的参数 @或者配置信息。
@确保了在内核启动时,.data段(数据段)被正确地从 ROM 拷贝到 RAM 中
ldmia r3!, {r4, r5, r6, r7} @从 __mmap_switched_data 开始的内存地址加载四个值到 r4、r5、r6、r7。
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, [r3, #16] )
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
cmp r7, #0
strne r0, [r7] @ Save control register values
b start_kernel @跳入内核,控制流将从汇编语言切换到 C 语言,不再返回。
ENDPROC(__mmap_switched)
三.start_kernel
start_kernel函数里许多子函数完成系统初始化的工作,它被定义在"init/main.c"文件中。这里只挑出几个重点的初始化函数。
1.boot_cpu_init()
这个函数用于初始化启动 CPU
点击查看代码
/*__init 属性表示这个函数只在初始化时使用,之后可以被优化掉。*/
static void __init boot_cpu_init(void)
{
int cpu = smp_processor_id();//获取当前 CPU 的 ID
/* Mark the boot cpu "present", "online" etc for SMP and UP case */
set_cpu_online(cpu, true);//将启动 CPU 的状态设置为在线(online),表示CPU可以执行任务。
set_cpu_active(cpu, true);//将启动 CPU 的状态设置为活跃(active),表示CPU正在运行内核代码。
set_cpu_present(cpu, true);//将启动 CPU 的状态设置为存在(present),表示CPU在系统中物理存在。
set_cpu_possible(cpu, true);//将启动 CPU 的状态设置为可能(possible),表示CPU可以被调度器考虑用于任务调度。
}
2.parse_args()
解析由Uboot传进来的参数(bootargs),并最后保存在一个argv_init 数组中,以便内核可以在后续的初始化过程中使用这些参数。这些参数在初始化控制台(console_init)中被打印出来。
点击查看代码
asmlinkage __visible void __init start_kernel(void)
{
...
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas();
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
build_all_zonelists(NULL, NULL);
page_alloc_init();
pr_notice("Kernel command line: %s\n", boot_command_line);
parse_early_param();
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
set_init_arg);
...
}
//argv_init数组的信息会在kernel_init(守护进程)中由run_init_process函数调用
//如果参数错误就会启动报错:pr_err("Failed to execute %s (error %d)\n",
static int __init set_init_arg(char *param, char *val, const char *unused)
{
unsigned int i;
if (panic_later)
return 0;
repair_env_string(param, val, unused);
for (i = 0; argv_init[i]; i++) {
if (i == MAX_INIT_ARGS) {
panic_later = "init";
panic_param = param;
return 0;
}
}
argv_init[i] = param;
return 0;
}
3.mm_init()
点击查看代码
static void __init mm_init(void)
{
page_ext_init_flatmem(); // 初始化页面扩展机制,适用于连续页面的情况
mem_init(); // 初始化内存管理相关的数据结构
kmem_cache_init(); // 初始化内核对象缓存,内核中用于管理各种内核对象(如进程描述符、文件对象等)的内存分配器。
percpu_init_late(); // 晚期每个CPU的数据初始化
pgtable_init(); // 初始化页表,这是内存管理单元(MMU)用于将虚拟地址映射到物理地址的数据结构。
vmalloc_init(); // 初始化虚拟内存分配器,允许内核分配可以从任何物理内存位置映射的内存区域。
ioremap_huge_init(); // 初始化用于大物理地址映射的机制,通常用于直接映射大的物理内存区域到内核的虚拟地址空间。
}
4.sched_init()
调度器的初始化,调度器用于管理进程的执行和切换。
5.init_IRQ()
这个函数确保了在内核启动过程中,中断系统被正确配置和初始化,以便能够正确处理来自硬件的中断请求。
点击查看代码
void __init init_IRQ(void)
{
int ret;
/*
* 如果内核配置了设备树支持(CONFIG_OF)且机器描述中没有提供特定的
* 初始化中断函数,则调用通用的中断芯片初始化函数。
*/
if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
irqchip_init(); // 使用通用的中断芯片初始化函数
else
machine_desc->init_irq(); // 使用机器特定的中断初始化函数
/*
* 如果内核配置了设备树支持和二级缓存(L2X0)支持,并且机器描述中
* 提供了二级缓存的辅助掩码或值,则进行二级缓存的初始化。
*/
if (IS_ENABLED(CONFIG_OF) && IS_ENABLED(CONFIG_CACHE_L2X0) &&
(machine_desc->l2c_aux_mask || machine_desc->l2c_aux_val)) {
/*
* 如果外部缓存的写安全函数未设置,则从机器描述中获取并设置。
*/
if (!outer_cache.write_sec)
outer_cache.write_sec = machine_desc->l2c_write_sec;
/*
* 使用机器描述中提供的辅助值和掩码初始化二级缓存。
* 如果初始化失败,打印错误信息。
*/
ret = l2x0_of_init(machine_desc->l2c_aux_val,
machine_desc->l2c_aux_mask);
if (ret)
pr_err("L2C: failed to init: %d\n", ret); // 打印二级缓存初始化失败的错误信息
}
}
6.rest_init()
rest_init主要是创建守护进程 kernel_init,创建内核线程管理进程kthreadd,启动空闲线程。
最后,cpu_startup_entry 函数的调用标志着 CPU 正式启动,开始执行调度的任务。
点击查看代码
static noinline void __init_refok rest_init(void)
{
int pid;
/*
* 启动 RCU(Read-Copy-Update)调度器,这是 Linux 内核中用于
* 处理读-写锁争用的机制。
*/
rcu_scheduler_starting();
/*
* 初始化 SMP(对称多处理)启动线程,这是内核在多核系统上
* 启动其他 CPU 时需要的。
*/
smpboot_thread_init();
/*
* 我们需要首先启动 init 进程,以确保它获得 PID 1。然而,init 任务
* 最终会想要创建 kthreads(内核线程),如果我们在创建 kthreadd
* 之前调度它,将会导致 OOPS(内核崩溃)。
*/
kernel_thread(kernel_init, NULL, CLONE_FS);
/*
* 设置 NUMA(非均匀内存访问)的默认策略。
*/
numa_default_policy();
/*
* 创建 kthreadd 线程,这是内核线程的管理者。它将负责创建和
* 管理其他内核线程。
*/
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
/*
* 上下文:读取 RCU 锁,以安全地访问 kthreadd_task 变量。
*/
rcu_read_lock();
/*
* 查找并获取 kthreadd 任务的指针。
*/
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
/*
* 上下文:释放 RCU 读锁。
*/
rcu_read_unlock();
/*
* 完成 kthreadd 完成信号,表示 kthreadd 已经启动。
*/
complete(&kthreadd_done);
/*
* 启动空闲线程,这是每个 CPU 上的默认线程,当没有其他任务
* 运行时执行。
*/
init_idle_bootup_task(current);
/*
* 调度器调用,禁止抢占,确保初始化代码的执行不会被打断。
*/
schedule_preempt_disabled();
/*
* 调用 CPU 启动入口点,标记 CPU 为在线状态,并开始执行。
* 这个函数不会返回,因为 CPU 将开始执行调度的任务。
*/
cpu_startup_entry(CPUHP_ONLINE);
}
四.三个特殊进程
1.idle进程
idle进程的PID为0,它是系统的第一个进程,是init进程和kthreadd进程的父进程。
idle进程在系统启动完成后,最后会进入cpu_idle_loop()的循环,CPU在没有任务执行时会进入这个循环。
点击查看代码
static void cpu_idle_loop(void)
{
while (1) {
/*
* 如果架构有轮询位(polling bit),我们保持一个不变性:
*
* 如果我们没有被调度(即 rq->curr != rq->idle),我们的轮询位是清除的。
* 这意味着,如果 rq->idle 有设置轮询位,那么设置 need_resched 将保证 CPU 重新调度。
*/
__current_set_polling(); // 设置当前任务的轮询位
tick_nohz_idle_enter(); // 进入无时钟(NO_HZ)空闲状态
while (!need_resched()) { // 如果不需要重新调度
check_pgt_cache(); // 检查页面全局目录缓存
rmb(); // 读取内存屏障,确保顺序
if (cpu_is_offline(smp_processor_id())) { // 如果当前 CPU 离线
rcu_cpu_notify(NULL, CPU_DYING_IDLE, (void *)(long)smp_processor_id()); // 通知 RCU 当前 CPU 即将死亡
smp_mb(); // 确保所有活动在 CPU 死亡前完成
this_cpu_write(cpu_dead_idle, true); // 标记当前 CPU 的空闲任务为死亡
arch_cpu_idle_dead(); // 架构特定的 CPU 空闲死亡处理
}
local_irq_disable(); // 禁用本地中断
arch_cpu_idle_enter(); // 进入架构特定的 CPU 空闲状态
/*
* 在轮询模式下,我们重新启用中断并自旋。
*
* 如果我们在从空闲状态唤醒时检测到时钟广播设备已经过期,
* 我们不想进入深度空闲,因为我们知道 IPI 即将到来。
*/
if (cpu_idle_force_poll || tick_check_broadcast_expired()) // 如果强制轮询或时钟广播过期
cpu_idle_poll(); // 执行 CPU 轮询
else
cpuidle_idle_call(); // 调用 CPU 空闲策略
arch_cpu_idle_exit(); // 退出架构特定的 CPU 空闲状态
}
/*
* 由于我们从上面的循环中退出,我们知道 TIF_NEED_RESCHED 必须被设置,
* 将其传播到 PREEMPT_NEED_RESCHED。
*
* 这对于轮询空闲循环是必需的,因为我们不会有 IPI 来帮助我们折叠状态。
*/
preempt_set_need_resched(); // 设置需要重新调度
tick_nohz_idle_exit(); // 退出无时钟(NO_HZ)空闲状态
__current_clr_polling(); // 清除当前任务的轮询位
/*
* 我们承诺在设置 need_resched 时调用 sched_ttwu_pending 和重新调度,
* 当轮询位被设置时。这意味着清除轮询位需要在执行这些操作之前可见。
*/
smp_mb__after_atomic(); // 内存屏障,确保顺序
sched_ttwu_pending(); // 调度任务唤醒
schedule_preempt_disabled(); // 禁用抢占式调度
}
}
2.init进程
init守护进程,用户进程的父进程。核心功能是驱动初始化、在FileSystem下找到init程序运行。
init进程在内核空间完成初始化后, 会在filesytem下加载init程序, 最后运行在用户态,变为守护进程监视系统其他进程。
3.kthreadd进程
kthreadd进程是其它内核线程的父进程,负责创建其它内核线程,一直运行在内核态。
点击查看代码
/*
kthreadd进程的任务就是不断轮询,如果任务队列为空,则线程主动让出cpu(调用schedule后会让出cpu,本线程会睡眠):如果不为空,则依次从任务队列中取出任务,然后创建相应的线程。
*/
int kthreadd(void *unused)
{
struct task_struct *tsk = current;
/* Setup a clean context for our children to inherit. */
set_task_comm(tsk, "kthreadd");
ignore_signals(tsk);
set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_FLAG_KTHREAD)); /*允许kthreadd在任意cpu上执行*/
set_mems_allowed(node_states[N_MEMORY]);
current->flags |= PF_NOFREEZE;
cgroup_init_kthreadd();
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list)) /* 判断内核线程链表是否为空 */
schedule(); /* 若没有需要创建的内核线程,进行一次调度,让出cpu */
__set_current_state(TASK_RUNNING);
spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) { /*依次取出任务*/
struct kthread_create_info *create;
create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list); /*从任务列表中摘除*/
spin_unlock(&kthread_create_lock);
/* 只要kthread_create_list不为空,就根据表中元素创建内核线程 */
create_kthread(create); //创建子进程
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
}
return 0;
}
线程的具体创建过程
- 某一个线程A调用kthread_create函数来创建新线程,调用后阻塞;kthread_create会将任务封装后添加到kthreadd监控的工作队列中;
- kthreadd进程检测到工作队列中有任务,则结束休眠状态,通过调用create_kthread函数创建线程,最后调用"kernel_thread --> do_fork"来创建线程,且新线程执行体为kthead
- 新线程创建成功后,执行kthead,kthreadd线程则继续睡眠等待创建新进程;
- 线程A调用kthread_create返回后,在合适的时候通过wake_up_process(pid)来唤醒新创建的线程
4.总结
linux启动的第一个进程是0号进程
0号进程会创建两个进程,分别是1号进程和2和进程。
1号进程最终会去调用可init可执行文件,init进程最终会去创建所有的应用进程。
2号进程会在内核中负责创建所有的内核线程
所以说0号进程是1号和2号进程的父进程;1号进程是所有用户态进程的父进程;2号进程是所有内核线程的父进程。