以相反的顺序解释它们要容易得多,因为进程切换总是涉及线程切换。
单核CPU上的典型线程上下文切换如下所示:
-
所有上下文切换都由“中断”启动。这可能是运行驱动程序的实际硬件中断(例如,来自网卡,键盘,内存管理或计时器硬件),或者是执行类似硬件中断的调用序列的软件调用(系统调用)进入操作系统在驱动程序中断的情况下,OS提供驱动程序可以调用的入口点,而不是执行“正常”直接中断返回&因此,如果操作系统需要操作系统设置线程就绪,则允许驱动程序通过OS调度程序退出(例如,它已标记信号量)。
-
非平凡的系统必须启动硬件保护级别更改才能进入内核状态,以便可以访问内核代码/数据等。
-
必须保存中断线程的核心状态。在一个简单的嵌入式系统中,这可能只是将所有寄存器推送到线程堆栈并将堆栈指针保存在其线程控制块(TCB)中。
-
许多系统在此阶段切换到OS专用堆栈,这样就不会在每个线程的堆栈上产生大量的OS内部堆栈需求。
-
可能需要标记发生中断状态更改的线程堆栈位置以允许嵌套中断。
-
驱动程序/系统调用运行,并且可以通过从内部队列添加/删除TCB来更改准备好的线程集,例如,用于不同的线程优先级。网卡驱动程序可能已设置事件或发信号通知另一个线程正在等待的信号量,因此该线程将被添加到就绪集,或者正在运行的线程可能已调用sleep(),因此选择将其自身从就绪集中移除。
-
运行OS调度程序算法以决定下一个要运行的线程,通常是位于该优先级队列前端的最高优先级就绪线程。如果下一个运行的线程属于与先前运行的线程不同的进程,那么这里需要一些额外的东西,(见后面)。
-
检索来自该线程的TCB的已保存堆栈指针并将其加载到硬件堆栈指针中。
-
恢复所选线程的核心状态。在我的简单系统上,寄存器将从所选线程的堆栈中弹出。更复杂的系统必须处理返回用户级保护。
-
执行中断返回,将执行转移到选定的线程。
醇>
对于多核CPU,情况更复杂。调度程序可以决定当前在另一个核上运行的线程可能需要被停止并由刚刚准备就绪的线程替换。它可以通过使用其处理器间驱动程序硬件中断运行必须停止的线程的核心来实现此目的。除了所有其他内容之外,此操作的复杂性是避免编写OS内核的一个很好的理由:)
典型的进程上下文切换如下:
-
进程上下文切换由线程上下文切换启动,因此上述所有内容1-9都需要发生。
-
在上面的步骤5中,调度程序决定运行属于与拥有先前运行的线程的进程不同的进程的线程。
-
内存管理硬件必须加载新进程的地址空间,即任何选择器/段/标志/允许新进程的线程访问其内存的任何内容。 / p>
-
需要从PCB保存/恢复任何FPU硬件的上下文。
-
可能还有其他需要保存/恢复的过程专用硬件。
醇>
在任何真实的系统中,这些机制都是依赖于体系结构的,上面是对上下文切换的含义的粗略和不完整的指导。进程交换机生成的其他开销并不是交换机的严格组成部分 - 在进程切换后可能会有额外的缓存刷新和页面错误,因为它的某些内存可能已经被分页以支持页面归属拥有之前运行的线程的进程。
我希望我能提供更详细/清晰的图片。
首先,OS调度线程而不是进程,因为线程是系统中唯一的可执行单元。进程切换只是一个线程切换,其中线程属于不同的进程。由于这一点,一般的开关程序很常见。
-
应调用调度程序。有三个基本的风格:
- 非自愿切换。你的线程事件的一些外部事件已经发生,影响了schedulling。例如,定时器响铃已经唤醒了一个具有高优先级的线程,HDD控制器报告已经将所请求的部分文件读入内存,并且线程等待它可以继续执行,系统定时器告诉内核你的线程有耗尽了时间量等等。
- 自愿。线程明确请求通过系统调用重新安排。例如,请求产量,或者请求睡眠,或者要求等待mutex被释放。
- 半自愿。线程隐式触发重新安排执行一些不相关的系统调用。例如,它要求系统读取文件。操作系统已将此请求路由到磁盘控制器,并且不会浪费时间忙于等待决定切换到另一个线程的结果。
-
在所有情况下,为了能够执行线程切换,应该将控制传递给内核。在非自愿开关的情况下,这种控制通过是通过中断执行的,在自愿(和半自愿)的情况下,控制通过系统调用传递。
-
在这两种情况下,进入内核都是CPU辅助的。处理器执行权限检查,记住线程被抢占的点(以便将来能够恢复它),从线程堆栈的用户部分切换到其内核对应部分,并将控件传递给预定义且众所周知的点。内核代码。
- 内核执行的第一个操作是保存CPU寄存器的内容,内核将为其自己的任务重用。通常内核只使用通用CPU寄存器,并通过推入堆栈来保存它们。
- 然后内核处理主要请求 - 处理中断,或准备文件读取请求或执行定时器设置。
- 在请求处理的某个时刻,内核执行的操作要么影响当前线程的状态(判断此线程中没有任何待办事项,我们应该等待某事)或影响另一个线程的状态(由于收到中断或由于释放互斥锁等原因,新线程变得可运行。)
- 内核调用一个scheduller。调度程序必须做出两个决定。第一个是关于如何处理当前线程。应该被阻止吗?如果是这样,应该放入什么等待队列?如果线程被非自愿地切换,则它被置于runqueue的末尾,如果线程被阻塞,因为它等待某些东西,它被放入其中一个等待队列中。第二个决定是关于下一个运行的线程。
- 一旦做出两个决定,scheduller就会执行上下文swicth传递给它的两个参数 - 当前和下一个线程的线程控制块。
- 上下文切换本身包含三个主要步骤。在第一个,内核确定CPU寄存器线程实际使用的是什么,并将其内容保存在堆栈或outgoint线程的TCB中。如果线程不使用FPU和SSE寄存器(IA-32平台的例子),则不会保存其内容。
- 第二步是上下文切换的核心。内核将当前指令指针推送到堆栈,堆栈指针的值保存在传出线程的TCB中。然后它从传入线程的TCB加载到CPU新的堆栈指针,并从其顶部弹出指令指针。那个!新的活动堆栈意味着新的活动线程。从这一点开始,系统的其余部分将认为它在传入线程的上下文中工作。
- 在第三步,内核确定传入线程实际使用的寄存器,并将之前保存的内容(请参阅步骤1)加载回CPU。
- 然后内核检查两个线程(传入和传出)是否属于同一进程。如果它们属于不同的进程(人们调用进程切换的情况),则内核将指向MMU的当前地址空间重置为新的虚拟到物理地址转换表集。作为此过程的一部分,CPU会刷新Translate Lookaside Buffer(TLB),它将虚拟缓存到物理地址转换规则。新的虚拟地址空间意味着以前缓存的规则现在不正确。请注意,这是关注流程的整组上下文切换操作中的唯一步骤!
- 内核为传入线程准备线程本地存储。例如,将相应的内存页面映射到指定的地址,或者例如在IA-32上常见的方法是加载指向传入线程的TLS数据的新段。
- 最后内核加载到传入线程的内核部分的CPU地址。在此之后,每个新的内核调用都将使用传入线程堆栈的内核部分,并且不会破坏存储在传出线程堆栈上的数据。
- 可以由内核执行的另一个步骤是重新编程系统计时器。执行此内核会要求计时器响铃并在一段时间后传递控制权到内核。这个时间段称为线程的时间量。
- 最后内核喜欢在上下文切换期间收集统计信息,包括soch信息,因为线程消耗的CPU时间,系统中实时上下文切换的方式,调用线程的次数,有多少次释放CPU自愿和非自愿,他们被用尽了多少次量子。部分统计数据由scheduller使用,后者试图做出更优化的决策。统计数据的另一个目的是交付给系统管理员和用户,以向他们展示系统内部的情况。
- 此时可以认为线程切换已完成,并且内核继续先前中断的系统操作。例如,等待文件读取的线程检查内存中的读取结果并处理它。或者在一些大型系统活动中间在互斥锁上被阻塞的线程继续该活动。
- 最后,稍后或更早的线程完成其系统活动并希望返回到用户模式以继续其最初使用的主要任务。此时,内核从通用寄存器的内核堆栈内容中弹出,这些内容先前在进入内核时保存,并要求CPU执行返回用户模式。
- CPU捕获指令指针和堆栈指针的值,这些值先前在进入内核模式期间保存并恢复它们。这样做它还会将线程从其堆栈的内核部分切换回堆栈的用户部分。最后,CPU将要执行的代码的权限重置为更有限的集合(例如,禁止使用特殊系统指令,或禁止访问kerel代码和数据)。最后,CPU将控制权传递回线程最初被抢占的点。在系统调用的情况下,线程将通过捕获和处理其结果来进行调用系统调用的点。在通过中断抢占的情况下,线程将完全在中断发生的同一点继续执行。在这种情况下,它甚至会完全没有意识到它被打断了。
醇>
一些摘要说明:
- 内核调度并仅执行线程,而不是进程。由于这种情况,线程之间发生了swicth。
- 属于不同进程的胎面之间的上下文切换的过程基本上与属于同一进程的线程之间的相同。在第一种情况下只有一个额外步骤 - 加载新的虚拟地址空间(导致TLB刷新)。
- 线程的上下文存储在线程堆栈的内核部分或线程TCB中(不在PCB中!)。
- 线程切换会降低性能。线程切换有很大的直接成本。甚至由缓存污染和TLB刷新产生的更大的直接成本(如果在切换期间重新加载虚拟地址空间)。
醇>
1。保存当前在CPU上运行的进程的上下文。更新过程控制块和其他重要字段。
2。将上述过程的过程控制块移至相关队列,如就绪队列,I / O队列等。
3。选择一个新的执行流程。
4。更新所选过程的过程控制块。这包括将进程状态更新为正在运行。
5。根据需要更新内存管理数据结构。
6。恢复在处理器上再次加载时先前正在运行的进程的上下文。这是通过加载过程控制块和寄存器的先前值来完成的。