Linux 内核的启动流程要比 uboot 复杂的多,涉及到的内容也更多,因此我们大致的了解一下Linux 内核的启动流程即可。
Linux启动流程
启动过程可以分为以下几个主要步骤:
1.引导加载程序(Bootloader)阶段
Linux 内核的启动流程首先由 引导加载程序(Bootloader)控制。常见的引导加载程序有 GRUB(用于x86架构)、U-Boot(用于嵌入式系统)等。这个阶段的主要任务是加载并启动内核镜像。
硬件初始化:引导加载程序进行基本的硬件初始化,如设置CPU、内存、I/O等设备的基本状态。
内核镜像加载:引导加载程序从存储介质(如硬盘、闪存、网络等)加载内核镜像到内存。内核镜像通常是一个压缩的文件(如 vmlinuz),有时是 zImage 或 uImage 格式。
加载设备树:对于非x86架构,U-Boot会加载设备树文件,它描述了硬件架构的信息。
传递启动参数:引导加载程序会将启动参数传递给内核,这些参数可能包括内核命令行参数、设备树地址、RAM的大小等信息。
上一章Uboot已经详细讲解了这一部分,有需要请阅读往期文章
2.内核初始化
首先分析 Linux 内核的连接脚本文件 arch/arm/kernel/vmlinux.lds,通过链接脚本可以找到 Linux 内核的第一行程序是从哪里执行的。
1 OUTPUT_ARCH(arm)
2 ENTRY(stext)
3 jiffies = jiffies_64;
4 SECTIONS
5 {
6 /DISCARD/ : {
7 *(.ARM.exidx.exit.text) *(.ARM.extab.exit.text) *(.ARM.exidx.text.exit) *(.ARM.extab.text.exit)
*(.exitcall.exit) *(.discard) *(.discard.*)
8 }
……
ENTRY 指明了了 Linux 内 核 入 口 , 入 口 为 stext , stext 定 义 在 文 arch/arm/kernel/head.S 中
Linux 内核将每种处理器都抽象为一个 proc_info_list 结构体,每种处理器都对应一个 procinfo。因此可以通过处理器 ID 来找到对应的 procinfo 结构
该函数首先调用函数__create_page_tables 创建页表。然后调用 __enable_mmu 函数使能MMU,
最终调用 start_kernel 来启动 Linux 内核,start_kernel 函数定义在文件 init/main.c 中。
start_kernel 通过调用众多的子函数来完成 Linux 启动之前的一些初始化工作,由于 start_kernel 函数里面调用的子函数太多,而这些子函数又很复杂,因此我们简单的来看一下一些重要的子函数。
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
set_task_stack_end_magic(&init_task);/* 设置任务栈结束魔术数,用于栈溢出检测 */
smp_setup_processor_id(); /* 跟 SMP 有关(多核处理器),设置处理器 ID */
init_timers(); /* 初始化定时器 */
kmem_cache_init_late(); /* slab 初始化,slab 是 Linux 内存分配器 */
kmemleak_init(); /* kmemleak 初始化,kmemleak 用于检查内存泄漏 */
vfs_caches_init(totalram_pages); /* 为 VFS 创建缓存 */
debug_objects_early_init(); /* 做一些和 debug 有关的初始化 */
.............
if (efi_enabled(EFI_RUNTIME_SERVICES)) {
efi_free_boot_services();
}
rest_init(); /* rest_init 函数 */
}
3.启动内核线程
start_kernel 函数最后调用了 rest_init,接下来简单看一下 rest_init 函数。rest_init 函数定义在文件 init/main.c 中,函数内容如下:
406 noinline void __ref rest_init(void)
407 {
408 struct task_struct *tsk;
409 int pid;
410
411 rcu_scheduler_starting();
417 pid = kernel_thread(kernel_init, NULL, CLONE_FS);
423 rcu_read_lock();
424 tsk = find_task_by_pid_ns(pid, &init_pid_ns);
425 set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
426 rcu_read_unlock();
427
428 numa_default_policy();
429 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
430 rcu_read_lock();
431 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
432 rcu_read_unlock();
433
441 system_state = SYSTEM_SCHEDULING;
442
443 complete(&kthreadd_done);
444
449 schedule_preempt_disabled();
451 cpu_startup_entry(CPUHP_ONLINE);
452 }
第 411 行,调用函数 rcu_scheduler_starting,启动 RCU 锁调度器
第417行,调用函数kernel_thread创建kernel_init线程,也就是大名鼎鼎的init内核进程。 init 进程负责所有用户进程的启动和管理。
- 在最初,init 进程是在内核空间中创建的,但它一旦被创建并执行,就会通过 execve 或类似的系统调用进入用户空间。这时,它会从内核态切换到用户态,开始执行用户空间的任务。
- init 进程的职责包括启动用户空间的其他进程,设置系统的运行级别,启动各种服务和守护进程等。
- init 进程的PID为1:在Linux中,init 进程的PID是1。因为它是所有其他用户进程的祖先进程,它的父进程是内核。作为PID为1的进程,init 进程对系统的运行至关重要。如果 init 进程终止,整个系统也将崩溃。
第 429 行,调用函数 kernel_thread 创建 kthreadd 内核进程,此内核进程的 PID 为 2。 kthreadd 进程负责所有内核进程的启动和管理。
第 451 行,最后调用函数 cpu_startup_entry 来进入 idle 进程,idle 进程的 PID 为 0,idle 进程叫做空闲进程,当 CPU 没有事情做的时候就在 idle 空闲进程
当 rest_init 完成它的初始化任务后,它会启动一些主要的内核线程,如 init 进程。这时,调度器开始正常运行,调度各种进程和线程。
4. 启动用户空间
- 内核的初始化完成后,它将启动第一个用户空间进程 init,并且完成从内核空间到用户空间的切换。
- init 会调用一系列初始化脚本,这些脚本会设置用户空间环境(例如网络配置、挂载文件统、启动守护进程等)。
- 系统启动过程中,init 会启动其他必要的用户空间服务,如网络服务、日志服务、图形界面等。
5. 系统进入正常运行状态
一旦 init 进程完成了系统的初始化,系统进入了正常的运行状态。此时,所有必要的服务和进程都已经启动,用户可以开始登录并使用系统。