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;
}
            

线程的具体创建过程

  1. 某一个线程A调用kthread_create函数来创建新线程,调用后阻塞;kthread_create会将任务封装后添加到kthreadd监控的工作队列中;
  2. kthreadd进程检测到工作队列中有任务,则结束休眠状态,通过调用create_kthread函数创建线程,最后调用"kernel_thread --> do_fork"来创建线程,且新线程执行体为kthead
  3. 新线程创建成功后,执行kthead,kthreadd线程则继续睡眠等待创建新进程;
  4. 线程A调用kthread_create返回后,在合适的时候通过wake_up_process(pid)来唤醒新创建的线程

4.总结

linux启动的第一个进程是0号进程
0号进程会创建两个进程,分别是1号进程和2和进程。
1号进程最终会去调用可init可执行文件,init进程最终会去创建所有的应用进程。
2号进程会在内核中负责创建所有的内核线程
所以说0号进程是1号和2号进程的父进程;1号进程是所有用户态进程的父进程;2号进程是所有内核线程的父进程。

posted @ 2024-11-05 17:35  Charles_hui  阅读(48)  评论(0编辑  收藏  举报