ARM架构(四)上下文切换

不读洛阳纸贵之书,不赴争相参观之地,不信喧嚣一时之论。以是,大器初成。

上下文切换可能由多种情况引起,例如:

  • 发生同步异常
  • 发生异步异常
  • 高优先级进程被创建,发生抢占
  • 获取锁失败,重新调度一个新的进程

但是总的来说发生来源只有两类:

  • 异常
  • 调度

但是这么划分其实不够准确,因为可能由于发生了某个异常,在处理异常的时候,进而发生了调度。因此,进一步从特权级角度出发的分类是:

  • 特权级不同情况下的上下文切换:主要指由于同步/异步异常的发生,用户进程陷入内核,保存上下文到内核栈顶。内核处理完后(我们不关心内核处理过程中发生的事情),将栈顶放回各个寄存器,然后回到原特权级。
  • 特权级相同情况下的上下文切换: 主要指在内核中,或者说内核线程的切换。发生的原因可能是某个内核线程获取锁失败,保存内核线程的上下文,直接切换到另一个内核线程执行。

最近实现的这个os是共享栈模型,这就导致用户进程下文不能直接保存在内核栈中,需要独立保存。更坑的是内核线程上下文的保存,处理寄存器外还需要保存整个内核栈的footprint。

1. 异常引起上下文切换

在AArch64中,发生异常的控制流如下

  1. 处理器执行异常向量表中对应项内的指令:传入中断的类型(kind:同步,irq,fiq,SError)和发生源(source:current EL with SP0/SPx,lower EL using AArch64/32)
  2. context save
  3. 执行异常处理器(exception handler),会根据上述参数以及ESL_ELx的信息multiplex分发执行对应处理例程
  4. context restore

在执行异常处理器相关代码的过程中会使用到各种通用寄存器,由于通用寄存器是各个异常级别共享的,异常处理器的执行就会影响之前代码执行过程中的处理器状态。所以执行异常处理器之前,需要保存上下文。之后从异常处理器返回时,需要恢复上下文。这个过程即上下文切换。

实际上,上下文切换前后的上下文均会有所不同:

  • 实现进程切换:需要swap out整个进程的上下文。
  • 实现系统调用:修改寄存器的值以提供系统调用返回值。
  • 实现breakpoint异常:修改ELR寄存器,以从下一条指令开始执行,而非当前指令。

下面主要说一下context savecontext 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字节对齐的要求:

arm-context-switch.png

注意:在栈中保存的顺序不一定按该顺序,但是数据均要保存。

3. ELx: Preferred Exception Return Address

当异常被带入ELx,CPU将preferred link address存储在ELR_ELx中。该值被定义为:

  1. 对于异步异常,第一条没有执行的指令或由于该中断的发生导致没有执行完的指令
  2. 对于非系统调用的同步异常,生成该异常的指令的地址
  3. 对于异常生成指令(HVC,SVC等),异常生成指令下一条指令的地址

例如,brk指令属于第二类。因此,我们需要在异常处理器中手动将该值设置为ELR_ELx + 4

4. 示例:异常上下文切换的实现

注意:每次都保存和恢复SIMD/FP寄存器开销非常昂贵。一种优化是仅在它们被source exception level或异常处理器使用时才进行相关操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
.global context_save
context_save:
    // FIXME: Save the remaining context to the stack.

    // Save registers begin from x27
    // since we have pushed xzr, lr, x29, x28

    stp x26, x27, [SP, #-16]!
    stp x24, x25, [SP, #-16]!
    stp x22, x23, [SP, #-16]!
    stp x20, x21, [SP, #-16]!
    stp x18, x19, [SP, #-16]!
    stp x16, x17, [SP, #-16]!
    stp x14, x15, [SP, #-16]!
    stp x12, x13, [SP, #-16]!
    stp x10, x11, [SP, #-16]!
    stp x8, x9, [SP, #-16]!
    stp x6, x7, [SP, #-16]!
    stp x4, x5, [SP, #-16]!
    stp x2, x3, [SP, #-16]!
    stp x0, x1, [SP, #-16]!

    stp q30, q31, [SP, #-32]!
    stp q28, q29, [SP, #-32]!
    stp q26, q27, [SP, #-32]!
    stp q24, q25, [SP, #-32]!
    stp q22, q23, [SP, #-32]!
    stp q20, q21, [SP, #-32]!
    stp q18, q19, [SP, #-32]!
    stp q16, q17, [SP, #-32]!
    stp q14, q15, [SP, #-32]!
    stp q12, q13, [SP, #-32]!
    stp q10, q11, [SP, #-32]!
    stp q8, q9, [SP, #-32]!
    stp q6, q7, [SP, #-32]!
    stp q4, q5, [SP, #-32]!
    stp q2, q3, [SP, #-32]!
    stp q0, q1, [SP, #-32]!

    // now free to use general regs to get special regs
    // and push into stack

    mrs x0, SP_EL0
    mrs x1, TPIDR_EL0
    stp x0, x1, [SP, #-16]!
    mrs x0, ELR_EL1
    mrs x1, SPSR_EL1
    stp x0, x1, [SP, #-16]!

    // move current stack pointer to x2 as the third argument
    mov x2, SP

    // save caller regs: lr will be used after function call
    stp lr, xzr, [SP, #-16]!

    // info: x0 <- x29
    mov x0, x29

    // syndrome reg: x1 <- ESR_ELx
    mrs x1, ESR_EL1

    bl handle_exception

    ldp lr, xzr, [SP], #16
    ret

.global context_restore
context_restore:
    // FIXME: Restore the context from the stack.

    // restore by backward order

    ldp x0, x1, [SP], #16
    msr ELR_EL1, x0
    msr SPSR_EL1, x1
    ldp x0, x1, [SP], #16
    msr SP_EL0, x0
    msr TPIDR_EL0, x1

    ldp q0, q1, [SP], #32
    ldp q2, q3, [SP], #32
    ldp q4, q5, [SP], #32
    ldp q6, q7, [SP], #32
    ldp q8, q9, [SP], #32
    ldp q10, q11, [SP], #32
    ldp q12, q13, [SP], #32
    ldp q14, q15, [SP], #32
    ldp q16, q17, [SP], #32
    ldp q18, q19, [SP], #32
    ldp q20, q21, [SP], #32
    ldp q22, q23, [SP], #32
    ldp q24, q25, [SP], #32
    ldp q26, q27, [SP], #32
    ldp q28, q29, [SP], #32
    ldp q30, q31, [SP], #32

    ldp x0, x1, [SP], #16
    ldp x2, x3, [SP], #16
    ldp x4, x5, [SP], #16
    ldp x6, x7, [SP], #16
    ldp x8, x9, [SP], #16
    ldp x10, x11, [SP], #16
    ldp x12, x13, [SP], #16
    ldp x14, x15, [SP], #16
    ldp x16, x17, [SP], #16
    ldp x18, x19, [SP], #16
    ldp x20, x21, [SP], #16
    ldp x22, x23, [SP], #16
    ldp x24, x25, [SP], #16
    ldp x26, x27, [SP], #16

    // x28, x29, x30(lr) and xzr will be handled later
    ret

.macro HANDLER source, kind
    .align 7
    stp     lr, xzr, [SP, #-16]!
    stp     x28, x29, [SP, #-16]!
    
    mov     x29, \source
    movk    x29, \kind, LSL #16
    bl      context_save
    
    bl      context_restore
    ldp     x28, x29, [SP], #16
    ldp     lr, xzr, [SP], #16
    eret
.endm
    
.align 11
.global vectors
vectors:
    // FIXME: Setup the 16 exception vectors.
    HANDLER 0 0
    HANDLER 0 1
    HANDLER 0 2
    HANDLER 0 3
    HANDLER 1 0
    HANDLER 1 1
    HANDLER 1 2
    HANDLER 1 3
    HANDLER 2 0
    HANDLER 2 1
    HANDLER 2 2
    HANDLER 2 3
    HANDLER 3 0
    HANDLER 3 1
    HANDLER 3 2
    HANDLER 3 3

4.1. trap frame结构

1
2
3
4
5
6
7
8
9
10
11
12
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct TrapFrame {
    // FIXME: Fill me in.
    pub elr_elx: u64,
    spsr_elx: u64,
    sp_els: u64,
    tpidr_els: u64,
    q: [u128; 32], // q0...q31
    x: [u64; 31], // x0...x30(lr)
    xzr: u64, // for 16byte alignment purpose
}

4.2. 能处理brk异常的异常处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[no_mangle]
pub extern "C" fn handle_exception(info: Info, esr: u32, tf: &mut TrapFrame) {
    kprintln!("exception happened: {:#?}", info);

    match info.kind {
        Kind::Synchronous => {
            use Syndrome::*;
            
            match Syndrome::from(esr) {
                Brk(k) => {
                    kprintln!("brk exception: {:#?}", k);
                    shell::shell("debug > ");
                    // 这里手动加4,以eret时返回到下一条指令
                    tf.elr_elx += 4;
                },
                other => {
                    kprintln!("sync exception captured: {:#?}", other);
                }
            }
        },
        Kind::Irq => {},
        Kind::Fiq => {},
        Kind::SError => {},
    }
}

5. 进程上下文切换

5.1. 一般情况

进程间上下文切换经历以下步骤:

  1. 异常发生,异常处理器调用进程调度器进行调度
  2. context_save保存当前进程的状态到栈上,然后把trap frame指针(也即栈顶指针)存入进程结构体中,修改进程状态,将结构体放入队列
  3. 从队列取出下一个进程的结构体,修改状态(修改为运行),执行context_restore,通过结构体中trap frame指针恢复之前执行状态eret返回后继续执行

上述步骤可以看出,对于进程间上下文切换而言,context_savecontext_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
2
3
4
5
6
7
tf1 in stack
--context save------
              |     \    tf1' in stack  - schedule_out(): 保存tf1'
 进程内切换    |  进程间切换 ----
              |     /    tf2' in stack  - switch_to(): 恢复tf2'
--context restore-----
tf1/2 in stack

由于第一个进程的状态trap frame是我们手动构造的,

6. 进程间上下文切换

上述实现其实缺乏一般性

7. 参考