一、start_kernel函数整体调用
首先看一下 start_kernel 大致做了哪些内容
/linux-6.6.10/init/main.c
asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
void start_kernel(void)
{
char *command_line; // 内核命令行参数指针
char *after_dashes; // 用于解析命令行中'--'后的参数
/* 初始化init_task的栈结束魔法数(用于检测栈溢出) */
set_task_stack_end_magic(&init_task);
/* 设置当前CPU ID,处理和SMP(多核处理器)有关的事务 */
smp_setup_processor_id();
/* 早期调试对象初始化 */
debug_objects_early_init();
/* 初始化内核构建ID */
init_vmlinux_build_id();
/* 早期cgroup子系统初始化,它以一组进程为目标进行系统资源分配和控制 */
cgroup_init_early();
/* 禁用中断(此时中断系统尚未完全初始化) */
local_irq_disable();
early_boot_irqs_disabled = true; // 标记早期中断禁用状态
/*
* 此时中断仍被禁用。先完成必要设置,再启用中断。
*/
boot_cpu_init(); // 启动CPU初始化
page_address_init(); // 页地址系统初始化
pr_notice("%s", linux_banner); // 打印Linux版本信息
early_security_init(); // 早期安全子系统初始化
/*
* 系统架构相关的初始化,该函数会解析传递进来的ATAGS或者设备树DTS文件,
* 根据设备树里的model和compatible两个属性来查找Linux是否支持这个单板
*/
setup_arch(&command_line); // 架构相关设置(会解析命令行参数)
setup_boot_config(); // 处理启动配置
setup_command_line(command_line); // 保存命令行参数
setup_nr_cpu_ids(); // 设置CPU数量
setup_per_cpu_areas(); // 初始化每CPU数据区
smp_prepare_boot_cpu(); // 架构相关的启动CPU准备
boot_cpu_hotplug_init(); // CPU热插拔初始化
/* 打印完整的命令行参数 */
pr_notice("Kernel command line: %s\n", saved_command_line);
/* 参数可能设置静态键 */
jump_label_init();
/* 解析早期参数 */
parse_early_param();
...
/* 在内存分配器初始化前的随机数系统早期初始化 */
random_init_early(command_line);
...
/*
* 以下操作使用较大的bootmem分配,必须在页分配器初始化前完成
*/
setup_log_buf(0); // 初始化日志缓冲区
vfs_caches_init_early(); // 早期VFS缓存初始化
sort_main_extable(); // 对异常表进行排序
trap_init(); // 陷阱/异常处理初始化
mm_core_init(); // 内存管理核心初始化
poking_init(); // 动态代码补丁初始化
ftrace_init(); // 函数跟踪系统初始化
/* 此时可以启用trace_printk */
early_trace_init();
/*
* 在启动任何中断(如定时器中断)前初始化调度器。
* 完整的拓扑结构将在smp_init()时设置。
*/
sched_init();
...
/*
* 允许早期创建工作队列和工作项排队/取消。
* 工作项执行依赖kthreads,将在workqueue_init()后开始。
*/
workqueue_init_early();
rcu_init(); // RCU机制初始化
...
/* 此后跟踪事件可用 */
trace_init();
...
early_irq_init(); // 早期中断控制器初始化
init_IRQ(); // 中断子系统初始化
tick_init(); // 定时器滴答初始化
rcu_init_nohz(); // NO_HZ模式RCU初始化
init_timers(); // 定时器初始化
srcu_init(); // SRCU初始化
hrtimers_init(); // 高精度定时器初始化
softirq_init(); // 软中断初始化
timekeeping_init(); // 时间维护初始化
time_init(); // 体系结构相关时间初始化
...
/* 标记早期启动阶段结束,启用中断 */
early_boot_irqs_disabled = false;
local_irq_enable();
kmem_cache_init_late(); // 延迟的slab缓存初始化
/*
* 注意:此处是刻意提前初始化控制台(在PCI等设置完成前),
* 以便早期输出调试信息。console_init()需要处理这种情况。
*/
console_init();
...
setup_per_cpu_pageset(); // 每CPU页集初始化
numa_policy_init(); // NUMA策略初始化
acpi_early_init(); // ACPI早期初始化
if (late_time_init) // 延迟的时间初始化(可选)
late_time_init();
sched_clock_init(); // 调度时钟初始化
calibrate_delay(); // 校准延迟循环
arch_cpu_finalize_init(); // 架构相关的CPU最终初始化
pid_idr_init(); // PID分配器初始化
anon_vma_init(); // 匿名VMA初始化
...
thread_stack_cache_init(); // 线程栈缓存初始化
cred_init(); // 凭证系统初始化
fork_init(); // fork相关初始化
proc_caches_init(); // proc文件系统缓存初始化
uts_ns_init(); // UTS命名空间初始化
key_init(); // 密钥子系统初始化
security_init(); // 安全子系统初始化
dbg_late_init(); // 延迟调试初始化
net_ns_init(); // 网络命名空间初始化
vfs_caches_init(); // VFS缓存初始化
pagecache_init(); // 页缓存初始化
signals_init(); // 信号系统初始化
seq_file_init(); // 序列文件初始化
proc_root_init(); // proc根文件系统初始化
nsfs_init(); // 命名空间文件系统初始化
cpuset_init(); // cpuset初始化
cgroup_init(); // cgroup初始化
taskstats_init_early(); // 任务统计早期初始化
delayacct_init(); // 延迟统计初始化
acpi_subsystem_init(); // ACPI子系统初始化
arch_post_acpi_subsys_init(); // ACPI后的架构相关初始化
kcsan_init(); // 内核并发检测工具初始化
/* 调用reset_init函数,创建init、kthread、idle线程 */
arch_call_rest_init();
...
}
start_kernel 里面调用了大量的函数,每一个函数都涉及一个内核模块的体系结构,内容非常庞大无法一一展开。这里简单总结一下 start_kernel 函数里所做的重要内容:
- 内核架构 、通用配置相关初始化
- 中断向量表相关初始化
- 内存管理相关初始化
- 进程管理相关初始化
- 进程调度相关初始化
- 网络子系统管理初始化
- 虚拟文件系统初始化
- 文件系统初始化 等等
函数是内核从"机器代码"到"完整系统"的桥梁,最终通过 arch_call_rest_init()--> rest_init() 交出控制权给用户空间,完成启动流程
二、rest_init 从内核初始化过渡到用户空间
以下是添加了详细中文注释的代码:
/linux-6.6.10/init/main.c
/*
* rest_init - 内核启动的最后阶段初始化,创建第一个用户进程并进入空闲循环
* 注意:该函数不会返回(noreturn),将永久运行在空闲循环中
*/
noinline void __ref __noreturn rest_init(void)
{
struct task_struct *tsk; // 任务结构体指针
int pid; // 进程ID
/* 启动RCU调度器,为多核环境做准备 */
rcu_scheduler_starting();
/*
* 首先创建init进程(PID 1),但需要注意:
* 1. init进程会尝试创建内核线程
* 2. 如果在内核线程守护进程(kthreadd)创建前调度init进程,会导致OOPS
* 使用CLONE_FS标志共享文件系统信息
*/
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
/*
* 将init进程固定在启动CPU上:
* 1. 在sched_init_smp()运行前,任务迁移功能还不完善
* 2. 之后sched_init_smp()会设置init进程允许在非隔离CPU上运行
*/
rcu_read_lock(); // 开始RCU读临界区
tsk = find_task_by_pid_ns(pid, &init_pid_ns); // 通过PID查找任务
tsk->flags |= PF_NO_SETAFFINITY; // 禁止修改CPU亲和性
set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id())); // 绑定到当前CPU
rcu_read_unlock(); // 结束RCU读临界区
/* 设置NUMA默认内存策略 */
numa_default_policy();
/* 创建内核线程守护进程kthreadd(PID 2),负责管理所有内核线程 */
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); // 记录kthreadd任务指针
rcu_read_unlock();
/*
* 启用might_sleep()和smp_processor_id()检查:
* 1. 不能更早启用,因为CONFIG_PREEMPTION=y时kernel_thread()会触发might_sleep()警告
* 2. CONFIG_PREEMPT_VOLUNTARY=y时init任务可能已经调度,但被kthreadd_done阻塞
*/
system_state = SYSTEM_SCHEDULING; // 更新系统状态为"调度中"
/* 通知kthreadd已完成初始化,解除可能存在的阻塞 */
complete(&kthreadd_done);
/*
* 引导空闲线程必须至少执行一次schedule()来启动调度:
* 1. 禁用抢占的情况下执行调度
* 2. 确保调度器正确初始化
*/
schedule_preempt_disabled();
/*
* 进入CPU空闲循环,永不返回:
* 1. 禁用抢占状态下调用
* 2. CPUHP_ONLINE表示CPU热插拔状态为在线
*/
cpu_startup_entry(CPUHP_ONLINE);
}
主要做了下面几件事:
(1)创建1号 init 进程(用户态进程祖先)
user_mode_thread 创建 kernel_init 1号进程:此时kernel_init函数运行在内核态,后续通过execve系统调用实现态切换,从原内核线程转变为用户空间init进程,是用户态所有进程的祖先。
(2)创建2号 kthreadd 进程(内核态进程祖先)
kernel_thread 创建 kthreadd 2号内核进程:守护进程(PID 2),确保内核线程管理就绪。负责所有内核进程的调度和管理,是内核所有进程运行的祖先。
(3)修改状态机,开启内核调度
system_state = SYSTEM_SCHEDULING:标记系统进入调度段。schedule_preempt_disabled:调用schedule函数开启内核的调度系统,从此linux系统开始转起来了。
(4)设置自身为空闲进程(0号idle进程)
cpu_startup_entry() :循环调用do_idle() ,是 Linux 内核空闲任务(idle task)的核心实现,是 CPU 空闲时执行的低功耗循环,主要职责:在无任务可调度时进入低功耗状态;处理 CPU 热插拔离线事件;响应调度请求和中断事件
因此,整体过程就是:
先创建一个1号进程和2号进程,用作用户态进程的祖先和内核态进程祖先,以及管理用户态和内核态进程,同时开启调度系统并创建一个空闲进程。调度系统会观察系统中所有的进程,这些进程里面只有有哪个需要被运行,调度系统就会终止 do_idle()(空闲进程)转而去执行有意义的干活的进程,这样操作系统就转起来了。
这种设计完美实现了"有事做事,无事节能"的操作系统核心哲学,也是Linux能效比如此出色的关键所在。