第六章 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 位值)压入栈。
    1. %esp 减去 4 (32 位大小)。
    2. value 存储到 (%esp) 指向的内存地址。
  • popl address (pop long): 从栈顶弹出一个 32 位值,并存入 address
    1. (%esp) 指向的内存地址的值复制到 address
    2. %esp 增加 4。

栈帧 (Stack Frame):

当一个函数被调用时,它会创建一个“栈帧”。一个典型的栈帧通常包含:

  • 参数 (Arguments): 调用者传递给函数的参数。
  • 返回地址 (Return Address): 函数执行完毕后,程序应该继续执行的位置。
  • 栈帧指针 (Base Pointer / Frame Pointer, %ebp): 指向当前栈帧的底部(或某个固定位置),方便访问局部变量和参数。
  • 局部变量 (Local Variables): 函数内部声明的变量。

函数调用和返回过程(简化版):

  1. 调用函数 foo(a, b):
    • 调用者将参数 b 压栈。
    • 调用者将参数 a 压栈。
    • 调用者执行 call foo 指令。call 指令做了两件事:
      • 将下一条指令的地址(foo 函数执行完后要返回的地址)压栈(这就是返回地址)。
      • 跳转到 foo 函数的代码。
  2. foo 函数内部:
    • pushl %ebp: 将旧的栈帧指针压栈(保存起来)。
    • movl %esp, %ebp: 将当前的栈指针 %esp 复制到 %ebp,作为新的栈帧指针。
    • 现在,%ebp 指向 foo 的栈帧底部。你可以通过 8(%ebp) 访问参数 a,通过 12(%ebp) 访问参数 b
    • 为局部变量分配空间:subl $size, %esp
  3. 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): 指向下一条要执行的指令。这个寄存器通常是隐藏的,不能直接操作,retcall 等指令会间接影响它。

常用指令 (在 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 从一个进程切换到另一个进程时,执行以下操作:
    1. 保存当前进程的状态: 将 CPU 的所有重要寄存器(包括 %eip, %esp, %ebp 以及其他通用寄存器)的值保存到一个数据结构中,这个结构通常称为进程的上下文 (Process Context)
    2. 加载新进程的状态: 从另一个进程的上下文数据结构中,将之前保存的寄存器值加载回 CPU。
    3. 恢复执行: 恢复执行新进程的代码。

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 的起始地址。
  • 所以,这一行做了两件事:
    1. 弹出当前栈顶(swtch 的返回地址)。
    2. 将弹出的返回地址存入 *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 的流程:

  1. 参数传递: swtch(struct context **old, struct context *new)
  2. 保存旧进程:
    • swtch 的返回地址(旧进程应该返回执行的地方)压入 *oldeip 字段。
    • 将旧进程当前的 %esp, %ebx, %ecx, %edx, %esi, %edi, %ebp 保存到 *old 的相应字段。
  3. 加载新进程:
    • 将新进程的 %ebp, %edi, %esi, %edx, %ecx, %ebxnew 加载到 CPU。
    • 将新进程的栈指针 %espnew 加载到 CPU 的 %esp栈切换完成。
    • 将新进程的指令指针 %eipnew 压入新的栈
  4. 返回(跳转)到新进程:
    • ret 指令弹出栈顶(即 new->eip),跳转到新进程的执行点。

理解这个过程,最重要的是要清楚:

  • 栈是如何工作的(增长方向、push/pop/call/ret 的作用)。
  • 寄存器是如何被保存和加载的,以及它们各自的作用。
  • 上下文切换的目的是什么:保存当前状态,恢复目标状态,然后跳转。

swtch 函数是如此底层和关键,因为它直接操作了 CPU 状态和栈,是操作系统调度机制的基石。希望这个详细的解释能帮助你回忆起这些重要的概念!

posted on 2025-11-13 11:27  Leo_Yide  阅读(5)  评论(0)    收藏  举报