Context Switching

来自osdev
跳到导航 跳到搜索

在普遍受内存保护的环境中,“上下文(context)” 是虚拟地址空间,其中包含的可执行文件,其数据等。

发生 “上下文切换” 的原因多种多样-因为调用了内核函数,应用程序已被抢占,或者因为它产生了时间片。

上下文切换涉及存储旧状态和检索新状态。 存储和检索的实际信息可以包括EIP、一般寄存器、段寄存器、CR3 (和分页结构) 、FPU/MMX寄存器、SSE寄存器和其他东西。 因为上下文切换可能涉及更改大量数据,所以它可能是操作系统中最昂贵的操作。

执行上下文切换的方法有很多。 x86 CPU提供了一种完全在硬件上进行操作的方法,但是出于性能和可移植性的原因,大多数现代操作系统都在软件中进行上下文切换。

软件上下文切换

软件上下文切换可以在所有CPU上使用,并且可以用于仅保存和重新加载需要更改的状态。 基本思想是提供一个保存当前 堆栈指针 (ESP) 并重新加载新的堆栈指针 (SS:ESP) 的功能。 当函数被调用时,EIP将存储在旧栈上,当函数返回时,新的EIP将从新栈中弹出。 当然,操作系统通常需要改变的不仅仅是栈和EIP。

注意在中断处理程序中如何发生抢占。 如果你的操作系统为中断处理程序保存了寄存器状态,那么如果你在调度程序中使用setjmp/longjmp,则可以跳转到IRQ的中断处理程序,该中断处理程序抢占了要切换到的进程。 然后只要返回即可。

Eflags,一般寄存器和任何数据段寄存器也应该被推到旧栈上,以及从新栈中弹出。 如果需要更改分页结构,则还需要重新加载CR3。

可以保存并重新加载FPU/MMX和SSE状态,但是通过复制硬件上下文切换机制 (在CR0中设置TS标志),也可以欺骗CPU在第一次使用FPU/MMX或SSE指令时生成异常。

细节

当CPU改变到更高的特权级别 (CPL 0是最高的) 时,它将从 Task State Segment (TSS) 加载SS和ESP的新值。 如果操作系统使用多个权限级别,则必须创建并加载TSS。 当处理器处于Ring3中时生成的中断将把堆栈切换到TSS中产生的许可级别栈条目。 在软件上下文切换期间,需要在TSS中设置SS0:ESP0 (可能还有SS1:ESP1或SS2:ESP2) 的值。 如果处理器在 Long Mode 下运行,则栈选择器不再存在,并且RSP0-2字段用于提供目标栈地址。

如果上下文切换还需要更改IO端口权限,则可以为每个 进程 加载不同的TSS。 运行虚拟8086任务时,不会检查TSS中的IO权限映射以提供I/O端口保护。 IO保护可以通过将IO权限级别设置为0来实现。 当Ring3中的进程试图写入或从IO端口读取时,这将生成 常规保护故障。 然后,GP故障处理程序可以检查权限并代表用户模式代码执行端口IO。

其他可能性

在上下文切换期间,操作系统可以执行严格上来说不属于上下文切换的其他工作。 一件常见的工作是计算最后一个线程/任务/进程使用的时间量,以便软件 (和最终用户) 可以确定所有CPU时间的去向。 另一种可能的工作是动态更改线程/任务/流程优先级。

性能考虑

将虚拟地址转换为物理地址是昂贵的。 处理器必须访问通常具有3-4个级别的pages表结构。 因此,单个内存访问实际上需要4-5次内存访问。

为了缓解这个问题,大多数现代处理器在转换后备缓冲区 (TLB) 中缓存虚拟到物理的翻译。 TLB是MMU的一部分,并对系统开发人员和用户 (大部分) 透明。

当虚拟内存被更新时 -- 例如,当一个进程的地址空间在软件上下文切换期间被另一个进程的地址空间替换时 -- TLB会突然包含不再有效的 “陈旧” 转换。 为了达到正确的行为,必须刷新这些转换。 写入CR3将刷新TLB。 但是,通过写入CR3,除了最后一个用户进程之外,还消除了内核的所有转换过程。 这不太理想,因为接下来的几个操作必须等待缓慢的虚拟到物理转换。

最近的英特尔和AMD处理器带有标记的TLB,它允许你使用特定的地址空间配置标记给定的转换。 在此方案中,TLB条目永远不会出现 “陈旧”,因此无需刷新TLB。

硬件上下文切换

某些CPU具有特殊的机制来在硬件中执行上下文切换。 以下信息仅提供80x86 CPU的详细信息。

硬件上下文切换机制 (在CPU手册中称为硬件任务切换 Hardware Task Switching) 可用于更改除FPU/MMX和SSE状态之外的所有CPU状态。 要使用硬件机制,你需要告诉CPU在哪里保存现有的CPU状态,以及在哪里加载新的CPU状态。 CPU状态始终存储在称为TSS (任务状态段 Task State Segment) 的特殊数据结构中。

要触发上下文切换,并告诉CPU在哪里加载它的新状态,从远距版本的CALL和JMP指令被使用。 给出的偏移量将被忽略,并且该段用于引用GDT中的 “TSS描述符”。 TSS描述符用于指定要用于从中加载新CPU状态的TSS的基址和限制。

CPU有一个称为 “TR” (或任务寄存器) 的寄存器,该寄存器告诉计算机哪个TSS将接收旧的CPU状态。 当TR寄存器加载 “LTR” 指令时,CPU会查看GDT条目 (用LTR指定),并用GDT条目加载TR的可见部分,并用GDT条目的基础和限制加载隐藏部分。 保存CPU状态时,将使用TR的隐藏部分。

硬件切换更进一步...

除了CALL和JMP指令外,还可以通过使用任务门描述符来触发上下文切换。 与TSS描述符不同,任务门描述符可以在GDT、LDT或IDT中。 通常,在IDT中使用任务门描述符,因此异常 (或IRQ) 会导致上下文切换,这是处理具有完全可靠性的双重故障异常的唯一方法。

因为TSS描述符仅可以在GDT中 (理论限制是8190任务)出现,基本硬件机制的设计受到GDT中可用条目的数量的限制。 然而,通过在每个上下文切换之前设置TSS描述符的基数,可以通过动态地改变TSS描述符来避免这种限制。 当还使用IDT中的任务门描述符时,使用此方法时必须小心 (每个任务门描述符引用的TSS描述符必须是恒定的)。 同样,无法使用调用指令启动上下文切换,因为CPU会保存GDT条目以用于TSS的 “反向链接” 字段中的返回。

如果在上下文切换期间还需要更改FPU/MMX和SSE状态,则有一些选项。 可以通过导致上下文切换的任何代码显式保存数据,或者CPU可以在首次使用FPU/MMX或SSE指令时生成异常。 使用第二个选项,异常处理程序将保存旧的FPU/MMX/SSE状态并重新加载新状态。 此选项可以防止在不必要时更改此数据 (例如,当没有任务或只有一个任务正在使用它们时),但是在没有附加同步的情况下无法在多处理器环境中正确工作,这可能比使用第一个选项更昂贵。

性能考虑

由于硬件机制保存几乎所有CPU状态,因此它可能比所理想的速度慢。 例如,当CPU加载新的段寄存器时,它会执行所涉及的所有访问和权限检查。 由于大多数现代操作系统不使用分段,因此可能不需要在上下文切换期间加载段寄存器,因此出于性能原因,有些操作系统倾向于不使用硬件上下文切换机制。 由于它没有被使用,因此CPU制造商也不再为此方法优化CPU (AFAIK)。 此外,新的64位CPU在64位/长模式下不支持硬件上下文切换。