不读洛阳纸贵之书,不赴争相参观之地,不信喧嚣一时之论。以是,大器初成。
上下文切换可能由多种情况引起,例如:
- 发生同步异常
- 发生异步异常
- 高优先级进程被创建,发生抢占
- 获取锁失败,重新调度一个新的进程
但是总的来说发生来源只有两类:
- 异常
- 调度
但是这么划分其实不够准确,因为可能由于发生了某个异常,在处理异常的时候,进而发生了调度。因此,进一步从特权级角度出发的分类是:
- 特权级不同情况下的上下文切换:主要指由于同步/异步异常的发生,用户进程陷入内核,保存上下文到内核栈顶。内核处理完后(我们不关心内核处理过程中发生的事情),将栈顶放回各个寄存器,然后回到原特权级。
- 特权级相同情况下的上下文切换: 主要指在内核中,或者说内核线程的切换。发生的原因可能是某个内核线程获取锁失败,保存内核线程的上下文,直接切换到另一个内核线程执行。
最近实现的这个os是共享栈模型,这就导致用户进程下文不能直接保存在内核栈中,需要独立保存。更坑的是内核线程上下文的保存,处理寄存器外还需要保存整个内核栈的footprint。
1. 异常引起上下文切换
在AArch64中,发生异常的控制流如下
- 处理器执行异常向量表中对应项内的指令:传入中断的类型(kind:同步,irq,fiq,SError)和发生源(source:current EL with SP0/SPx,lower EL using AArch64/32)
context save
- 执行异常处理器(
exception handler
),会根据上述参数以及ESL_ELx的信息multiplex分发执行对应处理例程 context restore
在执行异常处理器相关代码的过程中会使用到各种通用寄存器,由于通用寄存器是各个异常级别共享的,异常处理器的执行就会影响之前代码执行过程中的处理器状态。所以执行异常处理器之前,需要保存上下文。之后从异常处理器返回时,需要恢复上下文。这个过程即上下文切换。
实际上,上下文切换前后的上下文均会有所不同:
- 实现进程切换:需要swap out整个进程的上下文。
- 实现系统调用:修改寄存器的值以提供系统调用返回值。
- 实现breakpoint异常:修改ELR寄存器,以从下一条指令开始执行,而非当前指令。
下面主要说一下context save
和context restore
的具体内容:
2. Trap Frame
执行异常处理器前,保存上下文的结构被称为trap frame。其实现通常都是将各种状态push进栈中。在push完成以后,栈指针就成为了trap frame的指针。
完整的Cortex-A53执行状态包括了:
- x0…x30 - all 64-bits of all 31 general purpose registers
- q0…q31 - all 128-bits of all SIMD/FP registers
- TPIDR - the 64-bit “thread ID” register
- 存储在
TPIDR_ELs
- 存储在
- sp - the stack pointer
- 存储在
SP_ELs
- 存储在
- PSTATE - the program state
- 存储在
SPSR_ELx
,也包含了返回的异常级别
- 存储在
- pc - the program counter
ELR_ELx
存储prefered link address,通常ELR_ELx
为异常发生时的PC的值,或PC + 4
如下图,此外,x31(xzr)
主要是满足ARM对栈指针16字节对齐的要求:
注意:在栈中保存的顺序不一定按该顺序,但是数据均要保存。
3. ELx: Preferred Exception Return Address
当异常被带入ELx
,CPU将preferred link address存储在ELR_ELx
中。该值被定义为:
- 对于异步异常,第一条没有执行的指令或由于该中断的发生导致没有执行完的指令
- 对于非系统调用的同步异常,生成该异常的指令的地址
- 对于异常生成指令(HVC,SVC等),异常生成指令下一条指令的地址
例如,brk
指令属于第二类。因此,我们需要在异常处理器中手动将该值设置为ELR_ELx + 4
。
4. 示例:异常上下文切换的实现
注意:每次都保存和恢复SIMD/FP寄存器开销非常昂贵。一种优化是仅在它们被source exception level或异常处理器使用时才进行相关操作。
1 |
|
4.1. trap frame结构
1 |
|
4.2. 能处理brk
异常的异常处理器
1 |
|
5. 进程上下文切换
5.1. 一般情况
进程间上下文切换经历以下步骤:
- 异常发生,异常处理器调用进程调度器进行调度
context_save
保存当前进程的状态到栈上,然后把trap frame指针(也即栈顶指针)存入进程结构体中,修改进程状态,将结构体放入队列- 从队列取出下一个进程的结构体,修改状态(修改为运行),执行
context_restore
,通过结构体中trap frame指针恢复之前执行状态,eret
返回后继续执行
上述步骤可以看出,对于进程间上下文切换而言,context_save
和context_restore
由两个不同进程执行。这点和例如系统调用之类的异常处理不同。
5.2. 特例:bootstrap第一个进程
系统启动后,不存在进程的概念,也没有对应的结构体。因而需要手动创建一个进程结构体(会分配新的内存空间),然后切换到其上。首先考虑上面的步骤:
- 第二步,会把trap frame的内容保存到手动创的线程结构体中
- 然后第三步,又取出同一个结构体,执行
context_restore
,将第二步的trap frame恢复
然而保存和恢复的trap frame和希望创建的第一个进程没有关系。进程的新的内核栈空间并没有系统boot后执行产生的数据(显然boot后执行函数会在栈中留下各个调用的活动记录)。因而实际上破坏了第一个进程。
因而考虑下面的实现方式,初始化第一个进程结构体后,在它的栈上手动创建一个trap frame。然后直接调用上述第三步,执行context_restore
根据结构体内设置的内容返回。
TODO: 为什么需要额外的trap frame结构体。。。。
什么是上下文?
- 当前进程:上下文就是当前各个状态不用多说
- 并非当前正在运行,需要恢复的进程:上次切换前trap frame构造完成时的状态
- 独享内核栈,则该栈就是
- 共享内核栈,则需要
- 1)将trap frame放入当前进程的某块独享空间
- 2)从下一个进程的独享空间的trap frame取出并覆盖当前trap frame
1 |
|
由于第一个进程的状态trap frame是我们手动构造的,
6. 进程间上下文切换
上述实现其实缺乏一般性