3.2.5 宙之CPU的时分复用

 点击查看系列文章 =》 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博客

原创不易,需要大家多多鼓励!您的关注、点赞、收藏就是我的创作动力!

  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值