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);
}
}
到此,我们完成了 LK
对 linux 内核的引导
过程。我们再补充 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 线程上下文切换
的细节分析。