Little Kernel 启动流程简析

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文对基于 ARM32 平台,对 BootLoader Little Kernel 的启动流程做了简单分析。

3. Little Kernel启动流程简析

Little Kernel,是一种 BootLoader,类似于 u-boot。它和 u-boot 最大的差别在于,Little Kernel 使用了多线程,而 u-boot 是一个单线程的流程。在后续描述中,我们都将 Little Kernel 简写为 LK

3.1 LK 单线程时代

/*
 * 早期初始化,是为LK建立多线程运行的环境,所进行的必要初始化。如: 
 * . 多线程运行环境建立
 * . 必要的硬件初始化: MMU, Cache, Clock, 中断等
 */
bl		kmain
	kmain()
		/* 建立 LK 第1个线程。 */
		thread_init_early()
			/* 创建 bootstrap_thread, "bootstrap" */
			init_thread_struct()
			/*
			 * 设置当前线程,也是 LK 的第一个线程: 
			 * "bootstrap" 线程的状态为THREAD_RUNNING, 
			 * 所以,kmain() 就是我们第一个线程 "bootstrap" 的入口
			 */
			t->priority = HIGHEST_PRIORITY;
			t->state = THREAD_RUNNING;
			ist_add_head(&thread_list, &t->thread_list_node);
			current_thread = &bootstrap_thread;
		
		/* 架构相关早期初始化:MMU,Cache,中断向量, ... */
		arch_early_init() 
		/* 平台相关早期初始化:版型、平台时钟,GIC,timer等初始化 */
		platform_early_init()
			board_init()
			platform_clock_init()
			qgic_init()
			qtimer_init()
		target_early_init()

		bs_set_timestamp()
		call_constructors()
		heap_init()
		thread_init()
		
		/* 
		 * 建立LK的第2个线程, 用于DPC(Delayed Procedure Call)处理。
		 * 注意,这里只是创建了 "dpc" 线程,但是它还不会运行。直到后面的 exit_critical_section() 启用中断后,"dpc" 才会 timer 中断发生时, 在中断处理接口 timer_tick() 中得到处理可能的调度。 */
		dpc_init() 
			...
			thread_resume(thread_create("dpc", &dpc_thread_routine, NULL, DPC_PRIORITY, DEFAULT_STACK_SIZE))
		
		/*
		 * 定时器初始化。
			 * timer_init() 和 中断处理 一起, 构建 LK 的 多线程的核心:
			 * 当注册的 时钟中断处理函数 timer_tick(), 在中断中被调用时,如果返回 INT_RESCHEDULE 则引起线程调度。
			 * 
			 * 中断处理的流程参考 “3.2. LK 的中断处理流程”。
			 */
		timer_init()
			...
			platform_set_periodic_timer(timer_tick, NULL, 10);

		/* 
		 * LK首线程 "bootstrap" 完成早期一些必要的初始化后,LK多
		 * 线程运行环境已经准备好了,它的历史使命也就结束了。我
		 * 们即将进入 "bootstrap2" 时代,使用多线程的方式,加速
		 * 完成剩下的初始化工作。 
		 * 但是,直到后面的 exit_critical_section() 开启中断前,
		 * 我们仍然只有首线程 "bootstrap" 在运行,"dpc" 和 "bootstrap2" 线程都不会运行。
		 */
		thread_resume(thread_create("bootstrap2", &bootstrap2, NULL, DEFAULT_PRIORITY, DEFAULT_STACK_SIZE))
		
		/* 
		 * 使能中断。 
		 * 这条简单的语句,实际上是开启了 LK 多线程时代:
		 * timer 中断发生时,中断处理接口 timer_tick() 进行了多线程的调度。
		 * 在此之前,创建的 "dpc", "bootstrap2" 都不会得到运行。
		 */
		exit_critical_section();
		
		/* 好吧,我们的 "bootstrap" 线程(以kmain为入口的第1个LK线程)
		    * 完成前期的初始化工作后,历史使命已经完成,退化为idle线程:
		    * 降低自己的优先级,退位让贤给LK的其他线程 */
		thread_become_idle()
			thread_set_name("idle")
			thread_set_priority(IDLE_PRIORITY) /* 既然没啥要是做了,降低自己的优先级,将给多的CPU资源让给LK的其他线程 */
			idle_thread_routine() /* 做些无关紧要的事 */

3.2 LK多线程时代

随着上面的 exit_critical_section() 调用开启中断后,我们进入了 LK多线程时代

3.2.1 线程的调度

线程的调度,是随着 timer 中断的到来发生的。按上面 platform_set_periodic_timer() 设置的,以 10 ms 为周期,产生 timer 中断

/* 
 * 调用注册的各类中断处理接口。 
 * 这里只分析 timer 中断的处理接口,因为它引起进程的调度。
 */
bl  platform_irq
	gic_platform_irq()
		/* ARM 平台的返回值,记录在寄存器 r0 */
		ret = handler[num].func(handler[num].arg)
			...
			timer_tick()
				/* 返回 INT_RESCHEDULE 指示产生线程调度. 否则不产生调度 */
				return INT_RESCHEDULE; /* 需产生调度的情形 */
				...
...
/* 测试 platform_irq() 的返回值,如果为非 0 值,则产生调度,即调用 thread_preempt() */
cmp     r0, #0
blne    thread_preempt /* 进程线程调度, 如切换到 "dpc", "bootstrap2" 等线程 */
	...
	arch_context_switch()
		arm_context_switch()
	
	exit_critical_section();

3.2.2 用多线程完成初始化和系统引导

bootstrap2 线程将负责完成 剩下的初始化linux内核的引导LK 将初始化的单元组织成app的形式。下面看 bootstrap2 如何拉起这些 app 完成剩下的初始化。

/* bootstrap2 线程在 timer 中断中被拉起,其入口函数为 bootstrap2()。 */
bootstrap2()
	arch_init()
#if WITH_LIB_BIO
	bio_init()
#endif
#if WITH_LIB_FS
	fs_init()
#endif
	platform_init()
	target_init()

	/*
	 * 前面都是准备工作,这里就进入正题了。
	 * 主要涉及的源码目录 app.
	 */
	apps_init()
		...
		/*
		 * 初始化所有 LK 中所有的 app. 
		 * 由宏对 APP_START(), APP_END 定义,类似于 u-boot 的 U_BOOT_CMD()。
		 * linux 内核 是其中一个 app.
		 */
		for (app = &__apps_start; app != &__apps_end; app++) {
			if (app->init)
				app->init(app) = aboot_init() / ...
					...
					/* 启动 linux 内核 app */
					boot_linux_from_mmc() /* 从 eMMC 介质启动 linux */
						...
						boot_linux()
		}
		
		/* 启动所有 LK 中除 linux 内核 外的 其它 app. */
		for (app = &__apps_start; app != &__apps_end; app++) {
			if (app->entry && (app->flags & APP_FLAG_DONT_START_ON_BOOT) == 0) {
				start_app(app);
						}
		}

到此,我们完成了 LKlinux 内核的引导过程。我们再补充 LK 线程调度和切换代码的分析,具体是 thread_preempt()thread_resume() 这两个函数。thread_create() 相对比较简单,这里就不做分析。

3.2.3 线程切换代码分析

3.2.3.1 线程的 resume 分析
/* 线程的resume由接口 thread_resume() 发起,是当前线程主动放弃
 * CPU,主动发起的调度的行为 */
thread_resume()
	/* 将想要运行的目标线程,插入运行队列头部 */
	insert_in_run_queue_head()
		list_add_head(&run_queue[t->priority], &t->queue_node);
		run_queue_bitmap |= (1<<t->priority);
	/* 当前线程主动放弃CPU */	
	thread_yield()
		current_thread->state = THREAD_READY;
		current_thread->remaining_quantum = 0;
		insert_in_run_queue_tail(current_thread); /* 当前线程进入运行队列尾部 */
		thread_resched()
			/* 接下来的具体流程参看 3.2.3.3 中的流程分析 */
3.2.3.2 线程的抢占分析
/*
 * 在 timer 中断中,周期性发起的抢占行为。 
 *
 * 先来看一下 timer 中断过程中,都发生了什么。
 * ARM平台下,架构为我们做了以下事情:
 * . LR/R14 = 中断返回地址,即被中断线程 oldthread 的下一条指令。
 *   后面分析会提到 oldthread 是什么。
 * . SPSR = CPSR
 * . CPSR[4:0] = [EXCEPTION MODE NUMBER]
 * . CPSR[5] = 0
 * . if (中断是 RESET or FIQ ) then
 *		CPSR[6] = 1 // 禁止FIQ中断
 *	else
 *		CPSR[7] = 1 // 禁止IRQ中断
 *	. PC = [EXCEPTION VETOR ADDRESS], 即中断向量地址
 * 
 *  我们从 arm 中断处理接口开始分析,上下文切换是架构密切相关的。
 */
arm_irq
	/* 当前被中断线程 oldthread 的部分上下文(寄存器),被保存到r13指向的ram空间 */
	stmia	r13, { r4-r6 }
	mov		r4, r13
	sub		r5, lr, #4
	mrs		r6, spsr

	...
	
	/* 保存当前线程 oldthread 的上下文到其自身的堆栈 */
	stmfd	sp!, { r5 }

	/* save C trashed regs, supervisor lr */
	stmfd	sp!, { r0-r3, r12, lr }

	/* call into higher level code */
	mov	r0, sp /* iframe */
	bl	platform_irq
		...
		timer_tick()
			return INT_RESCHEDULE; /* 需产生调度的情形 */

	/* 
	 * 如果是 timer 中断的 handler 返回 INT_RESCHEDULE ,意味调度时机到来。那
	 * 这样的情形到底是怎么发生的呢?继续往下看。
	 */
	cmp     r0, #0
	blne    thread_preempt
		current_thread->state = THREAD_READY;
		if (current_thread->remaining_quantum > 0)
			insert_in_run_queue_head(current_thread); /* 时间配额还没有用完,继续放入运行队列头部 */
		else
			insert_in_run_queue_tail(current_thread); /* 时间配额消耗完了,放入运行队列队尾 */
		thread_resched()
			/* 接下来的具体流程参看 3.2.3.3 中的流程分析 */
3.2.3.3 线程上下文切换的细节
/* 不管是 thread_resume() 还是 thread_preempt(),最终都要处理上下文切换细节,也即都调用 thread_resched() */
thread_resume() / thread_preempt()
	...
	thread_resched()
		oldthread = current_thread;
			/* 寻找一个新的可运行线程队列:共32个优先级,同一个优先级有多个线程 */
			int next_queue = HIGHEST_PRIORITY - __builtin_clz(run_queue_bitmap) - (32 - NUM_PRIORITIES);
			newthread = list_remove_head_type(&run_queue[next_queue], thread_t, queue_node);
			
			if (list_is_empty(&run_queue[next_queue]))
				run_queue_bitmap &= ~(1<<next_queue);
			
			/* 新线程置为可运行状态 */
			newthread->state = THREAD_RUNNING;
					
			/* 如果新线程和当前线程相同,接下来的上下文切换动作就没必要了 */
			if (newthread == oldthread)
				return;
			
			/* 新线程的时间片重置 */
			if (newthread->remaining_quantum <= 0)
				newthread->remaining_quantum = 5;
			
			/* 新旧线程上下文切换 */
			oldthread->saved_critical_section_count = critical_section_count;
			current_thread = newthread;
			critical_section_count = newthread->saved_critical_section_count;
			
			/* 到此,线程切换arch无关部分已经完成,剩下的arch相关细节,交给arch代码处理,这也是分层设计的思想。 */
			arch_context_switch(oldthread, newthread)
				arm_context_switch(&oldthread->arch.sp, newthread->arch.sp)
					/* 把当前线程(oldthread,即将被切换)的寄存器,以及lr都压到当前线程的堆栈上 */
					sub		r3, sp, #(11*4)		/* can't use sp in user mode stm */
					mov		r12, lr
					stmia	r3, { r4-r11, r12, r13, r14 }^
					
					/* 更新当前线程(oldthread,即将被切换)sp -> oldthread->arch.sp */
					str		r3, [r0]

					/* load new regs */
					ldmia	r1, { r4-r11, r12, r13, r14 }^
					mov		lr, r12				/* restore lr */
					/* 
					 * 在 timer 中断中发生调度的情形, 我们切换到
					 * newthread 的堆栈, 等下在中断 arm_irq 函数
					 * 中使用的 newthread 的新堆栈!!! 
					 */
					add		sp, r1, #(11*4)     /* restore sp */
					
					/* 切换到新的线程 newthread: 
					 * newthread 被切出去时,lr 记录的返回地址是
					 * 之前线程被切换出去时紧挨
					 * call arm_context_switch 指令的下一条汇编
					 * 指令的地址 
					 */
					bx		lr

于此,我们也完成了 LK 线程上下文切换 的细节分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值