上下文切换和模式切换

上下文切换和模式切换

上下文切换指的是换线程执行,模式切换是同一个线程获得不同的权限

这两句话完美地概括了这两个核心概念最本质的区别:

  1. 上下文切换 (Context Switching):指的是 换线程执行
    • 核心:从一个线程的执行流切换到另一个完全不同的线程的执行流。
    • 结果:CPU改变了服务对象。
    • 比喻CPU核心这个“工人”放下手里A项目的活儿,转而去干B项目的活儿。
  2. 模式切换 (Mode Switching):指的是 同一个线程获得不同的权限
    • 核心:同一个线程为了执行特殊操作,将其权限从用户态提升到内核态(或反之)。
    • 结果:线程的执行环境和能力发生了变化,但任务本身没有变。
    • 比喻同一个“工人”正在做A项目的活儿,为了使用一台高级机床(内核服务),他临时申请并获得了更高级别的安全权限(内核态)。用完以后,交还权限,继续做原来的A项目。

为什么需要区分两者?

因为它们的目的开销完全不同:

  • 模式切换的目的:是为了安全地访问硬件和内核服务。操作系统通过这种方式保护内核,防止用户程序直接进行危险操作。这是功能性的。
  • 上下文切换的目的:是为了实现多任务并发,让多个线程能共享CPU资源。当一个线程等待时(比如等待IO),CPU可以立刻去执行其他就绪的线程,从而提高CPU利用率。这是性能性的。
  • 模式切换的开销:相对较小,主要是寄存器组的部分切换(如堆栈指针)和CPU特权级的改变
  • 上下文切换的开销:非常大,需要保存和恢复整个线程的完整状态(所有寄存器),并且如果切换的是不同进程的线程,还会导致缓存和TLB(快表)失效,带来巨大的性能损失。

一个例子理清所有概念

假设一个Web服务器线程正在处理用户请求,需要从磁盘读取一个文件:

  1. 用户态 (初始):线程运行在用户态,执行Web服务器代码。
  2. 系统调用 (模式切换):线程调用 read() 函数。执行 syscall 指令,同一个线程从用户态陷入内核态,获得更高权限。
  3. 内核中检查 (仍在同一线程):内核态下的线程发现磁盘数据还没准备好。
  4. 线程阻塞 (引发上下文切换的原因):内核将这个线程标记为“睡眠”(阻塞状态)。
  5. 上下文切换:操作系统调度器发现当前线程睡了,于是执行上下文切换
    • 保存当前Web服务器线程的状态。
    • 恢复另一个就绪线程(比如另一个处理请求的线程)的状态。
    • CPU开始执行另一个线程
  6. 数据就绪:磁盘IO完成,发出中断。内核处理中断,将Web服务器线程标记为“就绪”。
  7. 又一次上下文切换:在某个时刻,调度器决定让Web服务器线程继续运行,于是再次执行上下文切换,换回原来的线程。
  8. 继续执行 (模式切换):该线程在内核态继续执行,将数据从内核缓冲区拷贝到用户缓冲区,然后调用 sysexit 指令,从内核态返回用户态
  9. 用户态 (返回):线程回到用户态,read() 调用返回,Web服务器代码继续处理收到的文件数据。

在这个流程中,一次IO操作引发了:

  • 两次模式切换(进入和退出内核)
  • 两次上下文切换(线程阻塞时被换下,数据就绪后被换上)
模式切换 (同一线程内) 上下文切换 (不同线程间)
包含关系 是上下文切换的一个子步骤 包含模式切换(如果要切换到用户进程)
发生场景 系统调用、中断 时间片用完、等待资源(上下文切换是在内核态下完成的
线程 是同一个线程 切换到另一个线程
代价 较小(主要切换CPU模式、少量寄存器) 较大(需保存/恢复大量寄存器、可能切换地址空间)
结果 线程进入内核执行服务 线程被挂起,另一个线程开始执行

上下文切换的整体流程

上下文切换通常是由时钟中断系统调用(如一个进程主动睡眠等待资源)触发的。其整体流程可以概括为以下几步,我们以一次由时钟中断引起的、从进程A切换到进程B的切换为例:

阶段一:进入内核态(模式切换)

  1. 触发中断:进程A在用户态执行时,CPU的时钟硬件产生了一个时钟中断。
  2. 硬件自动保存部分上下文:CPU硬件自动将当前的程序计数器(PC)、程序状态字(PSW/EFLAGS)等少量关键寄存器压入当前进程(进程A)的内核栈。这一步是硬件自动完成的。
  3. 切换模式:CPU根据中断向量表,跳转到对应的中断处理程序(属于内核代码),并自动将特权级提升为内核态
    至此,发生了从用户态到内核态的模式切换,但仍然是进程A的线程在内核中执行。

阶段二:内核决定进行切换

  1. 执行中断服务例程:内核开始处理时钟中断。
  2. 调用调度器:内核发现进程A的时间片已经用完,需要切换进程。于是调用schedule()函数,这是调度器的核心。

阶段三:执行上下文切换(核心步骤)

  1. 保存进程A的上下文
    • 内核代码(通常是schedule() -> switch_to()宏/函数)手动地将进程A的完整硬件上下文(包括所有通用寄存器、浮点寄存器、栈指针ESP、基址指针EBP等)保存到进程A的进程控制块(PCB或thread_struct)中
  2. 选择下一个进程
    • 调度器算法从就绪队列中选择下一个要运行的进程,这里是进程B。
  3. 恢复进程B的上下文
    • 内核代码从进程B的PCB中将其之前保存的完整硬件上下文加载到CPU的各个寄存器中
    • 一个最关键的操作是切换栈指针(ESP):将栈指针从进程A的内核栈切换到进程B的内核栈。一旦这一步完成,内核的执行流实际上就已经转移到了进程B的“上次被切换出去时”的内核状态。
  4. 切换地址空间(可选,对于进程切换是必须的)
    • 如果切换的是进程(而不是同一进程内的线程),内核还需要切换CR3寄存器(在x86架构下),从而更换页表,切换到进程B的虚拟地址空间。如果是线程切换(属于同一进程),则省略这一步,因为它们共享地址空间。

阶段四:返回用户态(模式切换)

  1. 中断返回
    • 此时,内核为进程B所做的中断处理已经完成(或者更准确地说,是从进程B上次被中断的地方继续执行)。
    • 内核执行iret(中断返回)指令。该指令会从进程B的内核栈中弹出阶段一中由硬件自动保存的上下文(对于进程B来说,这是它上次被中断时保存的),其中包括了程序计数器和状态字。
  2. 切换模式并跳转:将CPU特权级从内核态降回用户态,还会让CPU跳转到之前弹出的程序计数器所指向的地址,也就是进程B上次被中断打断的用户态代码位置。至此,CPU已经开始执行进程B的用户态代码,一次完整的上下文切换结束。
posted @ 2025-08-30 23:09  deyang  阅读(0)  评论(0)    收藏  举报