点击查看系列文章 =》 Interrupt Pipeline系列文章大纲-CSDN博客
3.2.5.1 __primary_switched开始构建0号进程
宙者,古往今来,时间为宙。盘古为了开天辟地,必须分开空间和时间。在时间维度,要对CPU的运行时间进行切分,即基于进程调度的时分复用。
内核要支持多任务运行,本质上就是多个任务分享CPU的时间,轮流上阵占用CPU。接下来内核从__primary_switched开始构建0号进程。为了抓住核心,对其进行精简:
__primary_switched:
adrp x4, init_thread_union
add sp, x4, #THREAD_SIZE
adr_l x5, init_task
msr sp_el0, x5 // Save thread_info
adr_l x8, vectors // load VBAR_EL0 with virtual
msr vbar_el1, x8 // vector table address
isb
stp xzr, x30, [sp, #-16]!
mov x29, sp
add sp, sp, #16
mov x29, #0
mov x30, #0
b start_kernel
ENDPROC(__primary_switched)
3.2.5.2 0号进程内核栈的构建
第2~3行,是内核从头开始后,第一次进行栈的初始化,让SP_EL1指向内核栈的栈底(内核栈高地址)。0号进程是内核进程,没有用户态,它只有内核栈。它的内核栈是静态定义的变量init_thread_union,类型是union thread_union。
include/linux/sched/task.h:
extern union thread_union init_thread_union;
include/linux/sched.h:
union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK // ia64架构,可忽略
struct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASK //默认配置为y
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
arch/arm64/include/asm/memory.h:
#define THREAD_SIZE (UL(1) << THREAD_SHIFT)
CONFIG_ARCH_TASK_STRUCT_ON_STACK仅针对ia64架构,可以忽略。
CONFIG_THREAD_INFO_IN_TASK在ARM64默认配置为yes,所以thread_info会放置在task_struct结构中。
最终,union thread_union里面最需要关注的是unsigned long stack[]数组,这个数组的大小为THREAD_SIZE字节,通常为4KB或16KB字节,取决于THREAD_SHIFT。
总之第2~3行执行完毕,内核栈如下图:
第11行,进行压栈操作,在栈中FP的位置压入0,在栈中LR中的位置压入X30。第12行,X29(FP寄存器)指向SP_EL1。在第12~14行之间精简了代码,实际上可能会调用kasan_early_init和kaslr_early_init函数。
第14~16行,让SP_EL1重新指向栈底(高地址)。如下就是跳转到start_kernel前内核栈的样子:
3.2.5.3 0号进程的task_struct结构
第4~5行代码,让SP_EL0指向0号进程的task_struct结构体变量init_task。它定义在init/init_task.c。为了方便分析,只留下感兴趣的初始化部分:
struct task_struct init_task
= {
#ifdef CONFIG_THREAD_INFO_IN_TASK
.thread_info = INIT_THREAD_INFO(init_task),
.stack_refcount = ATOMIC_INIT(1),
#endif
……
.stack = init_stack,
……
.mm = NULL,
.active_mm = &init_mm,
……
.comm = INIT_TASK_COMM,
……
};
EXPORT_SYMBOL(init_task);
首先看.comm字段,它代表0号进程的名字,叫做“swapper”。”comm”应该是command name的缩写。
include/linux/init_task.h: #define INIT_TASK_COMM "swapper"
其次看.stack字段,它代表了进程的栈。按照理解,它应该指向上一章节分析的init_thread_union,为什么这里是指向init_stack?最终在include/asm-generic/vmlinux.lds.h找到玄机:init_thread_union的链接地址之后,紧接着是符号init_stack,所以init_stack的值刚好是init_thread_union的高地址即0号进程内核栈的栈底。
节选自include/asm-generic/vmlinux.lds.h:
3.2.5.4 0号进程的内存空间
struct task_struct init_task中的成员.mm和.active_mm怎么理解? 正解在Documentation/vm/active_mm.rst中,好处见memory management - current->mm gives NULL in linux kernel - Stack Overflow。
在这里,只要知道如下结论即可:内核进程的mm成员总是为NULL,它运行的时候总是使用active_mm。0号进程的active_mm指向init_mm,定义在mm/init-mm.c。这里特别关注成员.pgd,它指向就是内核镜像页表swapper_pg_dir!
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.arg_lock = __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
.user_ns = &init_user_ns,
.cpu_bitmap = { [BITS_TO_LONGS(NR_CPUS)] = 0},
INIT_MM_CONTEXT(init_mm)
};
3.2.5.5 进程调度的起搏器异常向量表
中断截止到目前都是关闭的,这里只是加载异常向量表。关于异常的具体分析,详见第二章。
adr_l x8, vectors // load VBAR_EL1 with virtual
msr vbar_el1, x8 // vector table address
为什么说异常是进程调度的起搏器?
- 只有异常打开的情况下,时钟中断才能开始运行,时间才开始流动,才能对进程的运行时间进行计量,对进程是否需要切换做出判断并标记。
- Linux在中断和系统调用返回时,才会真正执行进程切换。
中断真正开启的时机,是在接下来的start_kernel中。详见下文分解。
点击查看系列文章 =》 Interrupt Pipeline系列文章大纲-CSDN博客
原创不易,需要大家多多鼓励!您的关注、点赞、收藏就是我的创作动力!