上下文切换和模式切换
目录
上下文切换和模式切换
上下文切换指的是换线程执行,模式切换是同一个线程获得不同的权限
这两句话完美地概括了这两个核心概念最本质的区别:
- 上下文切换 (Context Switching):指的是 换线程执行。
- 核心:从一个线程的执行流切换到另一个完全不同的线程的执行流。
- 结果:CPU改变了服务对象。
- 比喻:CPU核心这个“工人”放下手里A项目的活儿,转而去干B项目的活儿。
- 模式切换 (Mode Switching):指的是 同一个线程获得不同的权限。
- 核心:同一个线程为了执行特殊操作,将其权限从用户态提升到内核态(或反之)。
- 结果:线程的执行环境和能力发生了变化,但任务本身没有变。
- 比喻:同一个“工人”正在做A项目的活儿,为了使用一台高级机床(内核服务),他临时申请并获得了更高级别的安全权限(内核态)。用完以后,交还权限,继续做原来的A项目。
为什么需要区分两者?
因为它们的目的和开销完全不同:
- 模式切换的目的:是为了安全地访问硬件和内核服务。操作系统通过这种方式保护内核,防止用户程序直接进行危险操作。这是功能性的。
- 上下文切换的目的:是为了实现多任务并发,让多个线程能共享CPU资源。当一个线程等待时(比如等待IO),CPU可以立刻去执行其他就绪的线程,从而提高CPU利用率。这是性能性的。
- 模式切换的开销:相对较小,主要是寄存器组的部分切换(如堆栈指针)和CPU特权级的改变。
- 上下文切换的开销:非常大,需要保存和恢复整个线程的完整状态(所有寄存器),并且如果切换的是不同进程的线程,还会导致缓存和TLB(快表)失效,带来巨大的性能损失。
一个例子理清所有概念
假设一个Web服务器线程正在处理用户请求,需要从磁盘读取一个文件:
- 用户态 (初始):线程运行在用户态,执行Web服务器代码。
- 系统调用 (模式切换):线程调用
read()
函数。执行syscall
指令,同一个线程从用户态陷入内核态,获得更高权限。 - 内核中检查 (仍在同一线程):内核态下的线程发现磁盘数据还没准备好。
- 线程阻塞 (引发上下文切换的原因):内核将这个线程标记为“睡眠”(阻塞状态)。
- 上下文切换:操作系统调度器发现当前线程睡了,于是执行上下文切换:
- 保存当前Web服务器线程的状态。
- 恢复另一个就绪线程(比如另一个处理请求的线程)的状态。
- CPU开始执行另一个线程。
- 数据就绪:磁盘IO完成,发出中断。内核处理中断,将Web服务器线程标记为“就绪”。
- 又一次上下文切换:在某个时刻,调度器决定让Web服务器线程继续运行,于是再次执行上下文切换,换回原来的线程。
- 继续执行 (模式切换):该线程在内核态继续执行,将数据从内核缓冲区拷贝到用户缓冲区,然后调用
sysexit
指令,从内核态返回用户态。 - 用户态 (返回):线程回到用户态,
read()
调用返回,Web服务器代码继续处理收到的文件数据。
在这个流程中,一次IO操作引发了:
- 两次模式切换(进入和退出内核)
- 两次上下文切换(线程阻塞时被换下,数据就绪后被换上)
模式切换 (同一线程内) | 上下文切换 (不同线程间) | |
---|---|---|
包含关系 | 是上下文切换的一个子步骤 | 包含模式切换(如果要切换到用户进程) |
发生场景 | 系统调用、中断 | 时间片用完、等待资源(上下文切换是在内核态下完成的) |
线程 | 是同一个线程 | 切换到另一个线程 |
代价 | 较小(主要切换CPU模式、少量寄存器) | 较大(需保存/恢复大量寄存器、可能切换地址空间) |
结果 | 线程进入内核执行服务 | 线程被挂起,另一个线程开始执行 |
上下文切换的整体流程
上下文切换通常是由时钟中断或系统调用(如一个进程主动睡眠等待资源)触发的。其整体流程可以概括为以下几步,我们以一次由时钟中断引起的、从进程A切换到进程B的切换为例:
阶段一:进入内核态(模式切换)
- 触发中断:进程A在用户态执行时,CPU的时钟硬件产生了一个时钟中断。
- 硬件自动保存部分上下文:CPU硬件自动将当前的程序计数器(PC)、程序状态字(PSW/EFLAGS)等少量关键寄存器压入当前进程(进程A)的内核栈。这一步是硬件自动完成的。
- 切换模式:CPU根据中断向量表,跳转到对应的中断处理程序(属于内核代码),并自动将特权级提升为内核态。
至此,发生了从用户态到内核态的模式切换,但仍然是进程A的线程在内核中执行。
阶段二:内核决定进行切换
- 执行中断服务例程:内核开始处理时钟中断。
- 调用调度器:内核发现进程A的时间片已经用完,需要切换进程。于是调用
schedule()
函数,这是调度器的核心。
阶段三:执行上下文切换(核心步骤)
- 保存进程A的上下文:
- 内核代码(通常是
schedule()
->switch_to()
宏/函数)手动地将进程A的完整硬件上下文(包括所有通用寄存器、浮点寄存器、栈指针ESP、基址指针EBP等)保存到进程A的进程控制块(PCB或thread_struct)中。
- 内核代码(通常是
- 选择下一个进程:
- 调度器算法从就绪队列中选择下一个要运行的进程,这里是进程B。
- 恢复进程B的上下文:
- 内核代码从进程B的PCB中将其之前保存的完整硬件上下文加载到CPU的各个寄存器中。
- 一个最关键的操作是切换栈指针(ESP):将栈指针从进程A的内核栈切换到进程B的内核栈。一旦这一步完成,内核的执行流实际上就已经转移到了进程B的“上次被切换出去时”的内核状态。
- 切换地址空间(可选,对于进程切换是必须的):
- 如果切换的是进程(而不是同一进程内的线程),内核还需要切换CR3寄存器(在x86架构下),从而更换页表,切换到进程B的虚拟地址空间。如果是线程切换(属于同一进程),则省略这一步,因为它们共享地址空间。
阶段四:返回用户态(模式切换)
- 中断返回:
- 此时,内核为进程B所做的中断处理已经完成(或者更准确地说,是从进程B上次被中断的地方继续执行)。
- 内核执行
iret
(中断返回)指令。该指令会从进程B的内核栈中弹出阶段一中由硬件自动保存的上下文(对于进程B来说,这是它上次被中断时保存的),其中包括了程序计数器和状态字。
- 切换模式并跳转:将CPU特权级从内核态降回用户态,还会让CPU跳转到之前弹出的程序计数器所指向的地址,也就是进程B上次被中断打断的用户态代码位置。至此,CPU已经开始执行进程B的用户态代码,一次完整的上下文切换结束。