【问题标题】:Steps in Context Switching上下文切换步骤
【发布时间】:2011-11-18 08:53:29
【问题描述】:

我被要求描述上下文切换所涉及的步骤 (1) 在两个不同进程之间以及 (2) 在同一进程中的两个不同线程之间。

  1. 在上下文切换期间,内核会将旧进程的上下文保存在其 PCB 中,然后加载计划运行的新进程的已保存上下文。
  2. 操作系统可以调度同一进程中两个不同线程之间的上下文切换,使它们看起来是并行执行的,因此通常比两个不同进程之间的上下文切换更快。

这是否过于笼统,或者您会添加什么来更清楚地解释流程?

【问题讨论】:

    标签: multithreading process context-switch


    【解决方案1】:

    希望能提供更详细/清晰的图片。

    首先,操作系统调度线程,而不是进程,因为线程是系统中唯一的可执行单元。进程切换只是线程属于不同进程的线程切换,因此过程基本相同。

    1. 调度程序被调用。在三种基本情况下可能会发生这种情况:

      • 非自愿切换。在当前运行的线程之外发生了一些影响调度的外部事件。例如,一个过期的定时器唤醒了一个高优先级的线程;或者磁盘控制器报告文件的请求部分已读入内存,等待它的线程可以继续执行;或者系统定时器告诉内核你的线程已经用完了它的时间片;等等。
      • 自动切换。线程通过系统调用显式请求重新调度。例如,它可能已请求将 CPU 让给其他线程、进入休眠状态或等待互斥体释放。
      • 半自愿切换。该线程通过执行一些不相关的系统调用来隐式触发重新调度。例如,它要求读取文件。操作系统已将此请求转发给磁盘控制器,为了避免让调用线程忙于等待而浪费时间,它决定切换到另一个线程。
    2. 在所有情况下,为了能够执行上下文切换,控制权应该传递给内核。在非自愿切换的情况下,这是由中断执行的。在自愿(和半自愿)上下文切换的情况下,控制通过系统调用传递给内核。

    3. 在这两种情况下,内核条目都是由 CPU 辅助的。处理器执行权限检查,保存指令指针(以便以后可以从正确的指令继续执行),从用户用户模式切换到内核模式,激活内核堆栈(特定于当前线程)并跳转到预定义的和内核代码中众所周知的一点。

    4. 内核执行的第一个操作是保存 CPU 寄存器的内容,它需要将其用于自己的目的。通常内核只使用通用 CPU 寄存器并通过将它们压入堆栈来保存它们。

    5. 如果需要,内核随后会处理主请求。它可以处理中断、准备文件读取请求、重新加载计时器等。

    6. 在请求处理过程中的某个时刻,内核执行的操作会影响当前线程的状态(决定当前在该线程中没有什么可做的,因为它正在等待某事)或另一个线程(或多个线程)(线程准备好运行,因为它正在等待的事件发生 - 例如,互斥体已释放)。

    7. 内核调用调度器。调度器必须做出两个决定。

      • 如何处理当前线程?是否应该被阻止?如果是这样,它应该放在哪个等待队列中?如果切换是非自愿的,则将其置于就绪队列的末尾。否则,线程将被放置在等待队列之一中。
      • 接下来应该运行哪个线程?
    8. 一旦做出两个决定,调度程序就会使用当前线程的 TCB 以及接下来要运行的线程的 TCB 执行上下文切换。

    9. 上下文切换本身包含三个主要步骤。

      • 内核计算出线程实际使用的 CPU 寄存器,并将其内容保存在堆栈或未调度线程的 TCB 中。在 IA-32 CPU 平台的情况下,如果线程不使用 FPU 和 SSE 寄存器,它们的内容将不会被保存。
      • 内核将指令指针压入堆栈,并将堆栈指针的值保存在未调度线程的TCB中。然后它从调度线程的 TCB 加载堆栈指针,并从堆栈顶部弹出指令指针。
      • 内核会计算出调度线程实际使用了哪些寄存器,并使用它们之前存储的内容加载它们(参见上面的步骤 1)。
    10. 此时内核检查已调度和未调度的线程是否属于同一个进程。如果不是(“进程”而不是“线程”切换),内核通过将 MMU(内存管理单元)指向调度进程的页表来重置当前地址空间。 TLB(Translation Lookaside Buffer)是一个缓存,包含最近的虚拟地址到物理地址的转换,也被刷新以防止错误的地址转换。请注意,这是整个上下文切换操作集中关心进程的唯一步骤!

    11. 内核为调度线程准备线程本地存储。例如,它将各个内存页面映射到指定的地址。再举一个例子,在 IA-32 平台上,一种常见的方法是加载一个指向传入线程的 TLS 数据的新段。

    12. 内核将当前线程的内核堆栈地址加载到 CPU 中。此后,每次内核调用都会使用这个内核栈,而不是非调度线程的内核栈。

    13. 内核可能执行的另一个步骤是重新编程系统计时器。当定时器触发时,控制权返回给内核。上下文切换和计时器触发之间的时间段称为时间片,它指示当时给定当前线程的执行时间。这称为抢占式调度。

    14. 内核通常在上下文切换期间收集统计信息以改进调度以及向系统管理员和用户显示系统中发生的情况。这些统计信息可能包括诸如线程消耗了多少 CPU 时间、调度了多少次、其时间片已过期多少次、系统中发生上下文切换的频率等信息。

      李>
    15. 此时可以认为上下文切换已准备就绪,内核继续先前中断的系统操作。例如,如果线程在系统调用期间试图获取一个互斥锁,而该互斥锁现在是空闲的,那么内核可能会完成被中断的操作。

    16. 在某个时刻,线程完成了其系统活动并希望返回到用户模式以执行非系统代码。内核从内核进入时保存的通用寄存器的内核栈内容中弹出,使CPU执行一条特殊指令返回用户态。

    17. CPU 捕获先前保存的进入内核模式的指令指针和堆栈指针的值,并恢复它们。此时线程的用户模式堆栈也被激活并退出内核模式(这禁止使用特殊系统指令)。

    18. 最后,CPU 从线程未调度时的位置继续执行。如果它发生在系统调用期间,线程将从调用系统调用的点开始,通过捕获和处理其结果。在中断抢占的情况下,线程将继续执行,就好像什么都没发生一样。

    一些总结说明:

    1. 内核只调度和执行线程,而不是进程——上下文切换发生在线程之间。

    2. 从另一个进程切换到线程上下文的过程在属于同一进程的线程之间的上下文切换中基本相同。只需要一个额外的步骤:更改页表(并刷新 TLB)。

    3. 线程上下文存储在内核堆栈或 TCB(不是 PCB!)中。

    4. 上下文切换是一项代价高昂的操作——它在性能上具有显着的直接成本,而由缓存污染(如果在进程之间发生切换,还有 TLB 刷新)造成的间接成本更大。

    【讨论】:

      【解决方案2】:

      1.保存当前在CPU上运行的进程的上下文。更新进程控制块和其他重要字段。

      2.将上述进程的进程控制块移动到相关队列中,如就绪队列、I/O队列等

      3.选择一个新的进程来执行。

      4.更新所选进程的进程控制块。这包括将进程状态更新为正在运行。

      5.根据需要更新内存管理数据结构。

      6.在处理器上再次加载时恢复先前运行的进程的上下文。这是通过加载进程控制块和寄存器的先前值来完成的。

      【讨论】:

        【解决方案3】:
        1. 在 switch 中,必须以某种方式保存当前正在执行的进程的状态,以便在重新调度时恢复该状态。
        2. 进程状态包括进程可能正在使用的所有寄存器,尤其是程序计数器,以及任何其他可能需要的操作系统特定数据。这通常存储在称为 process control block (PCB) 或 switchframe 的数据结构中。
        3. PCB 可能存储在内核内存中的每个进程堆栈中(与用户模式调用堆栈相反),或者可能有一些特定的操作系统定义的数据结构用于此信息。 PCB 的句柄被添加到准备运行的进程队列中,通常称为就绪队列。
        4. 由于操作系统有效地暂停了一个进程的执行,它可以通过从就绪队列中选择一个进程并恢复其 PCB 来切换上下文。这样做时,来自 PCB 的程序计数器被加载,因此可以在所选进程中继续执行。进程和线程优先级会影响从就绪队列中选择哪个进程(即,它可能是优先级队列)。

        (来源:Context switch

        【讨论】:

          【解决方案4】:

          以相反的顺序解释它们要容易得多,因为进程切换总是涉及线程切换。

          单核 CPU 上的典型线程上下文切换是这样发生的:

          1. 所有上下文切换均由“中断”启动。这可能是运行驱动程序的实际硬件中断(例如,来自网卡、键盘、内存管理或计时器硬件),也可能是执行类似硬件中断的调用序列的软件调用(系统调用)进入操作系统。在驱动程序中断的情况下,操作系统提供了一个驱动程序可以调用的入口点,而不是执行“正常”直接中断返回,因此如果需要操作系统设置线程,则允许驱动程序通过操作系统调度程序退出准备好,(例如,它已经发出信号量)。

          2. 重要的系统必须启动硬件保护级别更改才能进入内核状态,以便可以访问内核代码/数据等。

          3. 必须保存中断线程的核心状态。在一个简单的嵌入式系统上,这可能只是将所有寄存器推入线程堆栈并将堆栈指针保存在其线程控制块 (TCB) 中。

          4. 许多系统在此阶段切换到 OS 专用堆栈,因此大部分 OS 内部堆栈要求不会影响到每个线程的堆栈。

          5. 可能需要标记发生中断状态更改的线程堆栈位置以允许嵌套中断。

          6. 驱动程序/系统调用运行并可能通过从内部队列中添加/删除 TCB 来更改就绪线程集以用于不同的线程优先级,例如。网卡驱动程序可能已经设置了一个事件或发出另一个线程正在等待的信号量,以便将线程添加到就绪集中,或者正在运行的线程可能调用了 sleep() 并因此选择将自己从就绪集中移除.

          7. 运行 OS 调度程序算法来决定接下来运行哪个线程,通常是处于该优先级的队列前面的最高优先级就绪线程。如果下一个运行的线程与之前运行的线程属于不同的进程,那么这里需要一些额外的东西,(见下文)。

          8. 从 TCB 中为该线程保存的堆栈指针被检索并加载到硬件堆栈指针中。

          9. 所选线程的核心状态已恢复。在我的简单系统上,寄存器将从所选线程的堆栈中弹出。更复杂的系统将不得不处理返回到用户级保护。

          10. 执行中断返回,因此将执行转移到选定的线程。

          在多核 CPU 的情况下,事情会更加复杂。调度程序可能决定当前在另一个内核上运行的线程可能需要停止并由刚刚准备好的线程替换。它可以通过使用其处理器间驱动程序来硬件中断运行必须停止的线程的核心来做到这一点。除了所有其他内容之外,此操作的复杂性是避免编写 OS 内核的一个很好的理由 :)

          典型的进程上下文切换是这样发生的:

          1. 进程上下文切换由线程上下文切换启动,因此上述 1-9 的所有操作都需要发生。

          2. 在上面的步骤 5 中,调度程序决定运行一个线程,该线程属于与之前运行的线程所属进程不同的进程。

          3. 内存管理硬件必须加载新进程的地址空间,即任何选择器/段/标志/允许新进程的线程访问其内存的任何内容。

          4. 任何 FPU 硬件的上下文都需要从 PCB 中保存/恢复。

          5. 可能还有其他进程专用硬件需要保存/恢复。

          在任何实际系统上,这些机制都依赖于架构,以上是对任一上下文切换含义的粗略且不完整的指南。进程切换产生的其他开销并不是严格意义上的切换的一部分 - 在进程切换之后可能会有额外的缓存刷新和页面错误,因为它的一些内存可能已经被分页以支持属于的页面到拥有之前运行的线程的进程。

          【讨论】:

          • 能详细解释一下单核上下文切换的第四步吗?为什么嵌套中断需要一个“标记”?另外,寄存器到底保存在哪里? (假设 Linux)
          猜你喜欢
          • 1970-01-01
          • 2021-04-07
          • 2011-07-23
          • 1970-01-01
          • 2013-04-21
          • 2017-06-13
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多