我被要求描述上下文切换所涉及的步骤 (1) 在两个不同进程之间和 (2) 在同一进程中的两个不同线程之间。
- 在上下文切换期间,内核会将旧进程的上下文保存在其 PCB 中,然后加载计划运行的新进程的已保存上下文。
- 同一进程中两个不同线程之间的上下文切换可以由操作系统调度,以便它们看起来是并行执行的,因此通常比两个不同进程之间的上下文切换更快。
这是否过于笼统,或者您会添加什么来更清楚地解释该过程?
我被要求描述上下文切换所涉及的步骤 (1) 在两个不同进程之间和 (2) 在同一进程中的两个不同线程之间。
这是否过于笼统,或者您会添加什么来更清楚地解释该过程?
以相反的顺序解释它们要容易得多,因为进程切换总是涉及线程切换。
单核 CPU 上的典型线程上下文切换如下所示:
所有上下文切换都由“中断”启动。这可能是运行驱动程序的实际硬件中断(例如,来自网卡、键盘、内存管理或计时器硬件),也可能是执行类似硬件中断的调用序列的软件调用(系统调用)进入操作系统。在驱动程序中断的情况下,操作系统提供了一个驱动程序可以调用的入口点,而不是执行“正常”的直接中断返回,因此如果需要操作系统设置线程,则允许驱动程序通过操作系统调度程序退出准备好,(例如,它已经发出信号量)。
重要的系统必须启动硬件保护级别更改才能进入内核状态,以便可以访问内核代码/数据等。
必须保存中断线程的核心状态。在一个简单的嵌入式系统上,这可能只是将所有寄存器推入线程堆栈并将堆栈指针保存在其线程控制块 (TCB) 中。
许多系统在此阶段切换到 OS 专用堆栈,以便不会对每个线程的堆栈造成大量 OS 内部堆栈要求。
可能需要标记发生中断状态更改的线程堆栈位置以允许嵌套中断。
驱动程序/系统调用运行并可以通过从内部队列中添加/删除 TCB 来更改就绪线程集以用于不同的线程优先级,例如。网卡驱动程序可能已经设置了一个事件或发出另一个线程正在等待的信号量,因此该线程将被添加到就绪集中,或者正在运行的线程可能已调用 sleep() 并因此选择将自己从就绪集中删除.
运行 OS 调度程序算法来决定接下来运行哪个线程,通常是处于该优先级队列前面的最高优先级就绪线程。如果下一个运行的线程与之前运行的线程属于不同的进程,那么这里需要一些额外的东西,(见下文)。
从 TCB 中为该线程保存的堆栈指针被检索并加载到硬件堆栈指针中。
所选线程的核心状态已恢复。在我的简单系统上,寄存器将从所选线程的堆栈中弹出。更复杂的系统将不得不处理返回到用户级保护的问题。
执行中断返回,因此将执行转移到选定的线程。
在多核 CPU 的情况下,事情会更加复杂。调度程序可能决定当前在另一个内核上运行的线程可能需要停止并由刚刚准备好的线程替换。它可以通过使用其处理器间驱动程序来硬件中断运行必须停止的线程的核心来做到这一点。除了所有其他内容之外,此操作的复杂性是避免编写 OS 内核的一个很好的理由 :)
典型的进程上下文切换是这样发生的:
进程上下文切换是由线程上下文切换启动的,因此上述所有 1-9 都将需要发生。
在上面的步骤 5 中,调度程序决定运行一个属于与拥有先前运行的线程的进程不同的进程的线程。
内存管理硬件必须加载新进程的地址空间,即任何选择器/段/标志/允许新进程的线程访问其内存的任何东西。
任何 FPU 硬件的上下文都需要从 PCB 中保存/恢复。
可能还有其他需要保存/恢复的进程专用硬件。
在任何实际系统上,这些机制都是依赖于架构的,以上是对任一上下文切换的含义的粗略且不完整的指南。进程切换产生的其他开销并不是严格意义上的切换的一部分——在进程切换之后可能会有额外的缓存刷新和页面错误,因为它的一些内存可能已经被分页以支持属于的页面到拥有之前运行的线程的进程。
我希望我可以提供更详细/清晰的图片。
首先,操作系统调度线程,而不是进程,因为线程是系统中唯一的可执行单元。进程切换只是线程属于不同进程的线程切换,因此过程基本相同。
调度程序被调用。这可能发生在三种基本情况下:
在所有情况下,为了能够执行上下文切换,控制权应该传递给内核。在非自愿切换的情况下,这是由中断执行的。在自愿(和半自愿)上下文切换的情况下,控制通过系统调用传递给内核。
在这两种情况下,内核入口都是 CPU 辅助的。处理器执行权限检查,保存指令指针(以便以后可以从正确的指令继续执行),从用户用户模式切换到内核模式,激活内核堆栈(特定于当前线程)并跳转到预定义的和内核代码中的著名点。
内核执行的第一个操作是保存 CPU 寄存器的内容,它需要用于自己的目的。通常内核只使用通用 CPU 寄存器并通过将它们压入堆栈来保存它们。
如果需要,内核然后处理主要请求。它可以处理中断、准备文件读取请求、重新加载计时器等。
在请求处理期间的某个时刻,内核执行一个操作,该操作会影响当前线程的状态(决定当前在该线程中没有任何事情可做,因为它正在等待某事)或另一个线程(或多个线程)的状态(线程准备好运行是因为它正在等待的事件发生 - 例如,互斥锁已释放)。
内核调用调度程序。调度器必须做出两个决定。
一旦做出了两个决定,调度程序就会使用当前线程的 TCB 以及接下来要运行的线程的 TCB 执行上下文切换。
上下文切换本身包括三个主要步骤。
此时内核会检查调度线程和非调度线程是否属于同一个进程。如果不是(“进程”而不是“线程”切换),内核通过将 MMU(内存管理单元)指向调度进程的页表来重置当前地址空间。TLB(Translation Lookaside Buffer)是一个缓存,包含最近的虚拟地址到物理地址的转换,也被刷新以防止错误的地址转换。请注意,这是整个上下文切换操作集中关心进程的唯一步骤!
内核为调度线程准备线程本地存储。例如,它将各个内存页面映射到指定的地址。作为另一个例子,在 IA-32 平台上,一种常见的方法是加载一个指向传入线程的 TLS 数据的新段。
内核将当前线程的内核堆栈地址加载到 CPU 中。此后,每次内核调用都将使用这个内核堆栈,而不是未调度线程的内核堆栈。
内核可能执行的另一个步骤是重新编程系统计时器。当定时器触发时,控制权返回给内核。上下文切换和计时器触发之间的时间段称为时间片,它指示当时给定当前线程的执行时间。这称为抢先调度。
内核通常在上下文切换期间收集统计信息以改进调度以及向系统管理员和用户显示系统中正在发生的事情。这些统计信息可能包括诸如线程消耗了多少 CPU 时间、调度了多少次、其时间片已过期多少次、系统中发生上下文切换的频率等信息。
此时可以认为上下文切换已准备就绪,内核继续先前中断的系统操作。例如,如果线程在系统调用期间试图获取一个互斥锁,而该互斥锁现在是空闲的,那么内核可能会完成被中断的操作。
在某个时刻,线程完成了它的系统活动并希望返回到用户模式以执行非系统代码。内核从以前在内核进入时保存的通用寄存器的内核堆栈内容中弹出,并使 CPU 执行一条特殊指令以返回用户模式。
CPU 捕获先前保存的进入内核模式的指令指针和堆栈指针的值,并恢复它们。此时线程的用户模式堆栈也被激活并退出内核模式(这禁止使用特殊的系统指令)。
最后,CPU 从线程未调度时的位置继续执行。如果它发生在系统调用期间,线程将从调用系统调用的点开始,通过捕获和处理其结果。在中断抢占的情况下,线程将继续执行,就好像什么都没发生一样。
一些总结说明:
内核只调度和执行线程,而不是进程——上下文切换发生在线程之间。
从另一个进程切换到一个线程的上下文的过程在属于同一进程的线程之间的上下文切换中基本相同。只需要一个额外的步骤:更改页表(并刷新 TLB)。
线程上下文存储在内核堆栈或 TCB(不是 PCB!)中。
上下文切换是一项代价高昂的操作——它在性能上具有显着的直接成本,而由缓存污染(如果在进程之间发生切换,还有 TLB 刷新)造成的间接成本甚至更大。
(来源:上下文切换)
1.保存当前在CPU上运行的进程的上下文。更新过程控制块和其他重要字段。
2.将上述进程的进程控制块移动到相关队列中,如就绪队列、I/O队列等。
3.选择一个新的流程来执行。
4.更新选定进程的进程控制块。这包括将进程状态更新为正在运行。
5.根据需要更新内存管理数据结构。
6.在处理器上再次加载时恢复先前运行的进程的上下文。这是通过加载过程控制块和寄存器的先前值来完成的。