0%

FreeBSD源码笔记03-上下文硬件切换

本文解析进程上下文切换的核心代码;区别于linux的使用TR寄存器的硬切换,也区别于linux的软切换;FreeBSD的切换仍然继承于unix-v6风格下的函数栈的切换(直接简单可理解),所以理解函数栈帧的切换是其中的本质;进程切换要保存进程的现场环境以便于恢复现场,这里就需要重新去温习一下x86_64位的硬件体系结构编程,最后对汇编语言的理解和编写能力也有一定的要求,这里的汇编不是nasm而是gas语法.

1. x86_64平台寄存器使用约定

相关的内容可以点击这里了解,函数参数使用的寄存器参照下图:

2. 上下文硬件切换代码位置

1
2
3
4
5
// freebsd/sys/kern/sched_ule.c(line:2145)
td->td_oncpu = NOCPU;
//根据上图td => %rdi,newtd => %rsi,mtx => %rdx
cpu_switch(td, newtd, mtx);
cpuid = td->td_oncpu = PCPU_GET(cpuid);

3. x86_64位的硬件的寄存器

4. cpu_switch注释

这个需要另外开辟一个笔记来记录栈帧原理,虽然x86和x86_64的栈帧回溯方式发生了改变,但是在FreeBSD中,使用的还是x86的栈帧方式;具体文章可以先参考这里;
cpu_switch的切换进程的基本原理是先将旧的进程保存到旧进程的 PCB结构中(内核页),然后将新进程的PCB中的寄存器信息恢复到寄存器中,其中rbp和rsp的恢复比较重要,至此在ret的时候将跳转到rsp所指的地址去执行(也就是新进程去执行),整个切换过程都是在内核模式下进行的.以下cpu_switch的参数意思参考第一节的内容,pcb的保存所使用的寄存器信息参见第三节的图片.

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
//freebsd/sys/amd64/amd64/cpu_switch.S(line:75)
/*
* cpu_switch(old, new, mtx)
*
* Save the current thread state, then select the next thread to run
* and load its state.
* %rdi = oldtd
* %rsi = newtd
* %rdx = mtx
*/
ENTRY(cpu_switch)
/* Switch to new thread. First, save context. */
/*将旧进程的pcb地址放入%r8中*/
leaq TD_MD_PCB(%rdi),%r8

/* Hardware registers */
// 保存当前硬件状态的信息放入到pcb结构体中.
// 其中以%rbp,%rsp,%eip最为重要
// (%rsp)就是当前进程的返回地址
movq (%rsp),%rax
movq %r15,PCB_R15(%r8)
movq %r14,PCB_R14(%r8)
movq %r13,PCB_R13(%r8)
movq %r12,PCB_R12(%r8)
movq %rbp,PCB_RBP(%r8)
movq %rsp,PCB_RSP(%r8)
movq %rbx,PCB_RBX(%r8)
movq %rax,PCB_RIP(%r8)

/*中间省略代码,保存fs,gs等等*/
ctx_switch_fpusave_done:
/* Save is done. Now fire up new thread. Leave old vmspace. */
/* 将%rsi(新进程地址)放入到%12中*/
movq %rsi,%r12
movq %rdi,%r13
movq %rdx,%r15
movq %rsi,%rdi
callq pmap_activate_sw
movq %r15,TD_LOCK(%r13) /* Release the old thread */
sw1:
// 将新进程的pcb地址放入到%r8
leaq TD_MD_PCB(%r12),%r8
#if defined(SCHED_ULE) && defined(SMP)
movq $blocked_lock, %rdx
movq TD_LOCK(%r12),%rcx
cmpq %rcx, %rdx
je sw1wait

/*中间省略代码,恢复fs,gs,tss等等操作*/
done_load_dr:

/* Restore context. */
// 将新进程的硬件环境寄存器信息还原到寄存器中
// 特别是%rbp,%rsp,以及%eip
movq PCB_R15(%r8),%r15
movq PCB_R14(%r8),%r14
movq PCB_R13(%r8),%r13
movq PCB_R12(%r8),%r12
movq PCB_RBP(%r8),%rbp
movq PCB_RSP(%r8),%rsp
movq PCB_RBX(%r8),%rbx
movq PCB_RIP(%r8),%rax
// 这一句就是将原来的返回地址放入到%rsp所指的地方
movq %rax,(%rsp)
movq PCPU(CURTHREAD),%rdi
call fpu_activate_sw
cmpb $0,cpu_flush_rsb_ctxsw(%rip)
jne rsb_flush
// 将%rsp的地址弹出到%eip中执行!切换到新进程执行
ret

/*省略一节以后在增加注释*/
END(cpu_switch)

后续: 上下文切换的硬件依赖性部分就是这个cpu_switch函数,因为注重效率所以使用gas汇编编写,其中未有详尽的地方也可以看完以后思考一下,这个切换是否可以打断?如果能够打断会发生什么?不同CPU上运行的代码是否是共用一套寄存器,还是每个CPU单独使用寄存器,上述代码对于页面的保存恢复没有注释,以及如果一个进程被迁出内存了,放在虚拟内存的时候,是否可以直接切换到这个进程执行,还是需要等待所有内容迁入到内存中在执行?跟平台无关的进程上下文切换函数是mi_switch,他最终调用不同平台的cpu_switch实现,上述代码是x86_64平台下的实现,后续将读源码的进度继续解读进程切换的细节