第六章 xv6上下文切换
1. 栈 (Stack) 的工作方式
栈是一种后进先出 (LIFO - Last In, First Out) 的数据结构。你可以把它想象成一个叠盘子,最后放上去的盘子,最先被拿走。
在计算机中,栈主要用于:
- 函数调用: 当一个函数被调用时,需要保存当前程序的执行位置(返回地址),并将参数传递给新函数。
- 局部变量: 函数内部声明的局部变量通常也存储在栈上。
- 寄存器保存: 在函数切换(比如上下文切换)时,需要保存当前 CPU 寄存器的状态,栈是保存这些状态的一个好地方。
x86 上的栈:
- 栈指针 (
%esp): 在 x86 架构(32 位)中,%esp寄存器总是指向栈顶的下一个可用内存地址。 - 栈增长方向: 在 x86 上,栈通常是向下增长的,也就是说,当你向栈中压入(push)数据时,栈指针
%esp会减小。当你从栈中弹出(pop)数据时,栈指针%esp会增大。
重要指令:
pushl value(push long): 将value(一个 32 位值)压入栈。%esp减去 4 (32 位大小)。- 将
value存储到(%esp)指向的内存地址。
popl address(pop long): 从栈顶弹出一个 32 位值,并存入address。- 将
(%esp)指向的内存地址的值复制到address。 %esp增加 4。
- 将
栈帧 (Stack Frame):
当一个函数被调用时,它会创建一个“栈帧”。一个典型的栈帧通常包含:
- 参数 (Arguments): 调用者传递给函数的参数。
- 返回地址 (Return Address): 函数执行完毕后,程序应该继续执行的位置。
- 栈帧指针 (Base Pointer / Frame Pointer,
%ebp): 指向当前栈帧的底部(或某个固定位置),方便访问局部变量和参数。 - 局部变量 (Local Variables): 函数内部声明的变量。
函数调用和返回过程(简化版):
- 调用函数
foo(a, b):- 调用者将参数
b压栈。 - 调用者将参数
a压栈。 - 调用者执行
call foo指令。call指令做了两件事:- 将下一条指令的地址(
foo函数执行完后要返回的地址)压栈(这就是返回地址)。 - 跳转到
foo函数的代码。
- 将下一条指令的地址(
- 调用者将参数
- 在
foo函数内部:pushl %ebp: 将旧的栈帧指针压栈(保存起来)。movl %esp, %ebp: 将当前的栈指针%esp复制到%ebp,作为新的栈帧指针。- 现在,
%ebp指向foo的栈帧底部。你可以通过8(%ebp)访问参数a,通过12(%ebp)访问参数b。 - 为局部变量分配空间:
subl $size, %esp。
foo函数执行完毕:movl %ebp, %esp: 恢复栈指针到调用foo之前的位置(释放局部变量空间)。popl %ebp: 弹出之前保存的旧栈帧指针,恢复调用者的栈帧。ret: 从栈顶弹出返回地址,并跳转到该地址继续执行。
2. 汇编语言 (x86 32-bit)
汇编语言是机器码的符号表示。每条汇编指令都对应着 CPU 的一个具体操作。
常用寄存器 (32-bit x86):
- 通用寄存器:
%eax,%ebx,%ecx,%edx,%esi,%edi,%ebp,%esp%eax: 通常用于返回值。%ebx,%esi,%edi,%ebp: 经常被用作通用数据存储。%ecx,%edx: 常用于计数器或操作数。%esp: 栈指针。%ebp: 栈帧指针。
- 指令指针 (
%eip): 指向下一条要执行的指令。这个寄存器通常是隐藏的,不能直接操作,ret和call等指令会间接影响它。
常用指令 (在 swtch 中出现的):
movl src, dst: 将src的值复制到dst。movl value, %reg: 将一个常量值或内存值存入寄存器。movl %reg, value: 将寄存器值存入内存。movl %reg1, %reg2: 寄存器间复制。
popl address: 从栈顶弹出一个值并存入address。pushl value: 将value压入栈。ret: 从栈顶弹出返回地址,并跳转到该地址。call label: 将下一条指令的地址压栈,然后跳转到label。%esp: 栈指针。%ebp: 栈帧指针。
内存寻址 (Addressing):
(%reg): 表示%reg寄存器指向的内存地址。offset(%reg): 表示%reg寄存器指向的内存地址加上offset的地址。例如,4(%esp)就是%esp指向的地址往后 4 个字节处。
3. 操作系统的进程管理概念
- 进程 (Process): 是操作系统中一个正在运行的程序实例。每个进程都有自己独立的内存空间、代码、数据、堆栈等。
- 进程状态: 进程可以处于多种状态,如:运行 (Running)、就绪 (Ready)、等待 (Waiting)、终止 (Terminated)。
- CPU 分时 (Time-Sharing): 在单核 CPU 上,操作系统通过快速地在多个就绪进程之间切换,让每个进程都能在 CPU 上运行一小段时间,从而给用户一种“多任务并行”的体验。
- 上下文切换 (Context Switch): 这是实现 CPU 分时的核心机制。当操作系统决定将 CPU 从一个进程切换到另一个进程时,执行以下操作:
- 保存当前进程的状态: 将 CPU 的所有重要寄存器(包括
%eip,%esp,%ebp以及其他通用寄存器)的值保存到一个数据结构中,这个结构通常称为进程的上下文 (Process Context)。 - 加载新进程的状态: 从另一个进程的上下文数据结构中,将之前保存的寄存器值加载回 CPU。
- 恢复执行: 恢复执行新进程的代码。
- 保存当前进程的状态: 将 CPU 的所有重要寄存器(包括
swtch 函数
1 # void swtch(struct context **old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
- 参数:
old:struct context **(指向旧进程 context 的指针)。new:struct context *(指向新进程 context 的指针)。
- 目标: 保存当前 CPU 状态到
*old,然后从new加载 CPU 状态,并跳转到新进程。
保存旧进程的上下文 (*old):
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
%esp是当前栈指针。4(%esp): 在调用swtch时,栈上布局是:%esp指向返回地址。4(%esp)处是第一个参数old的地址。8(%esp)处是第二个参数new的地址。
movl 4(%esp), %eax: 将old的地址(struct context **old的值)复制到%eax。现在%eax指向old这个指针。
9 popl 0(%eax)
popl会弹出栈顶值。栈顶当前是swtch函数的返回地址。0(%eax):%eax指向old的地址,0(%eax)是old指向的struct context的起始地址。- 所以,这一行做了两件事:
- 弹出当前栈顶(
swtch的返回地址)。 - 将弹出的返回地址存入
*old(即old->context.eip)。
- 弹出当前栈顶(
- 这意味着,当旧进程被恢复时,它会从
swtch函数的ret返回(也就是图中的第 28 行),然后ret会弹出这个保存在old->eip的地址,并跳转执行。
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
- 这些指令将当前 CPU 的
%esp,%ebx,%ecx,%edx,%esi,%edi,%ebp寄存器的值,按顺序保存到old指向的context结构体的不同偏移量处。 4(%eax)对应%esp。8(%eax)对应%ebx。- ...
28(%eax)对应%ebp。
到这里,旧进程的所有重要 CPU 状态(寄存器值,包括返回地址)都已保存在 *old 中。
加载新进程的上下文 (new):
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
4(%esp)处现在存储的是第二个参数new的地址(因为old的地址在栈上,new的地址在old地址的上面)。movl 4(%esp), %eax: 将new的地址(struct context *new的值)复制到%eax。现在%eax指向new这个指针。
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
- 这些指令将
new指向的context结构体中的寄存器值,按相反的顺序(与保存时相反)加载回 CPU 的对应寄存器。 28(%eax)对应new->ebp。24(%eax)对应new->edi。- ...
8(%eax)对应new->ebx。
现在,除了栈指针 (%esp) 和指令指针 (%eip),其他大部分寄存器都已加载为新进程的状态。
26 movl 4(%eax), %esp # stack is switched here
4(%eax)处存储的是new->esp(新进程的栈指针)。movl 4(%eax), %esp: 将新进程的栈指针加载到%esp。- 这是最关键的一步,CPU 现在正式使用了新进程的栈!
27 pushl 0(%eax) # return addr put in place
0(%eax)处存储的是new->eip(新进程应该继续执行的指令地址)。pushl指令将new->eip压入新的栈(因为%esp已经指向了新进程的栈)。- 现在,栈顶就放着新进程恢复执行的地址。
28 ret # finally return into new ctxt
ret指令从栈顶弹出值(即new->eip),并将程序控制流跳转到这个地址。- 至此,CPU 已经成功地从旧进程切换到了新进程,并开始在新进程的上下文中执行。
总结 swtch 的流程:
- 参数传递:
swtch(struct context **old, struct context *new) - 保存旧进程:
- 将
swtch的返回地址(旧进程应该返回执行的地方)压入*old的eip字段。 - 将旧进程当前的
%esp,%ebx,%ecx,%edx,%esi,%edi,%ebp保存到*old的相应字段。
- 将
- 加载新进程:
- 将新进程的
%ebp,%edi,%esi,%edx,%ecx,%ebx从new加载到 CPU。 - 将新进程的栈指针
%esp从new加载到 CPU 的%esp。栈切换完成。 - 将新进程的指令指针
%eip从new压入新的栈。
- 将新进程的
- 返回(跳转)到新进程:
ret指令弹出栈顶(即new->eip),跳转到新进程的执行点。
理解这个过程,最重要的是要清楚:
- 栈是如何工作的(增长方向、
push/pop/call/ret的作用)。 - 寄存器是如何被保存和加载的,以及它们各自的作用。
- 上下文切换的目的是什么:保存当前状态,恢复目标状态,然后跳转。
swtch 函数是如此底层和关键,因为它直接操作了 CPU 状态和栈,是操作系统调度机制的基石。希望这个详细的解释能帮助你回忆起这些重要的概念!
浙公网安备 33010602011771号