Linux学习笔记(二):Linux启动第二阶段---从start_kernel到rest_init创建1号2号进程概述

一、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能效比如此出色的关键所在。

     

     

     

     

     

     

     

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值