进程切换
于是你开始“切换工作”:你迅速收拾桌面上的文案工具,把它们放进抽屉(保存当前进程的上下文到进程控制块PCB),然后拿起咖啡师的围裙、咖啡豆、杯子等咖啡店工作需要的工具(加载新进程的上下文到寄存器),赶往咖啡店继续工作。(时间片到了,操作系统就要切换他)于是你再次收拾咖啡店的工具,把它们放回咖啡店的储物柜(保存咖啡店进程的上下文),然后重新拿出文案工具,回到办公室继续之前未完成的文案工作(恢复文案进

死循环进程如何运行?
一旦一个进程占有CPU,会把自己的代码跑完吗?
一旦一个进程占有CPU,它不会一直运行直到代码跑完。现代操作系统采用时间片轮转等调度机制,会强制中断进程的运行,剥夺其CPU使用权,然后切换到其他进程。进程是否能 “跑完” 取决于其代码逻辑(如是否存在死循环)以及操作系统的调度策略。(时间片到了,操作系统就要切换他)
所以死循环进程,不会打死系统,不会一直占有 CPU!
死循环进程是指一个进程在其代码中包含一个无法正常退出的循环逻辑。例如,一个简单的死循环代码在C语言中可以表示为:
while(1)
{
// 循环体代码
}
当这样的进程运行时,它会不断地执行循环体中的代码,不会主动退出循环。
从操作系统调度的角度来看,进程的运行时间是由操作系统的调度策略决定的。操作系统会根据一定的调度算法(如时间片轮转调度算法)来分配CPU时间给各个进程。
在时间片轮转调度算法中,每个进程会被分配一个时间片(例如几十毫秒)。当一个死循环进程获得CPU时间片后,它会尽可能地在自己的时间片内一直运行循环代码。当时间片用完后,操作系统会剥夺它的CPU使用权,将CPU分配给其他进程。
如果操作系统采用的是优先级调度算法,死循环进程的优先级会影响它获得CPU的频率。即使它是死循环进程,如果优先级较低,它可能会长时间得不到CPU时间,而优先级高的进程会优先获得CPU执行。
聊聊 CPU 中的寄存器
CPU里有许多寄存器,32 位下,正常是 32 个,64 位下,正常是 64 个。
CPU寄存器是CPU内部的临时存储单元,用于存放当前正在执行的进程的临时数据,例如程序计数器、指令寄存器、数据寄存器等。
在进程切换过程中,寄存器的内容被称为进程的上下文数据。当一个进程被中断或让出CPU时,操作系统会将当前寄存器中的上下文数据保存到该进程的进程控制块(PCB)中;当该进程再次被调度到CPU上运行时,操作系统会将保存的上下文数据恢复到寄存器中,从而让进程能够从上次中断的地方继续执行。
# CPU 硬件层(物理寄存器)
┌──────────────────────────────────────────────────┐
│ CPU 内部硬件寄存器集合 │
│ 【通用寄存器】 │
│ R0 R1 R2 R3 R4 R5 R6 R7 (x86-32) │
│ 【特殊关键寄存器】 │
│ PC/EIP 程序计数器 → 下一条指令地址 │
│ SP/ESP 栈指针 → 进程用户栈/内核栈栈顶 │
│ FLAGS 状态标志位 → 运算进位、零标志、中断标志 │
│ CS/DS/ES 段寄存器 │
│ 浮点/扩展寄存器 │
└──────────────────────┬─────────────────────────────┘
│
│ 进程切换:保存/恢复上下文
│ 寄存器值 双向拷贝
▼
# 内核态:进程PCB task_struct
┌─────────────────────────────────────────────────────────────────┐
│ task_struct (Linux 进程控制块) │
│ │
│ 基础字段:pid、进程状态R/T/S/Z、优先级、父进程指针、调度策略 │
│ 内存字段:mm_struct *mm // 指向进程虚拟内存描述符 │
│ 栈指针: void *stack // 指向进程内核栈起始地址 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ struct thread_struct // 【核心:硬件上下文保存区】 │
│ │ 专门用来备份整套 CPU 寄存器 │
│ │ │
│ │ unsigned long regs[16]; // 通用寄存器备份数组 │
│ │ unsigned long pc; // 备份 PC/EIP │
│ │ unsigned long sp; // 备份 SP/ESP │
│ │ unsigned long flags; // 备份 FLAGS 状态寄存器 │
│ │ unsigned long cs,ds,es; // 备份段寄存器 │
│ │ ... 浮点、扩展寄存器备份 │
│ └───────────────┬─────────────────────────────────────────┘ │
└───────────────────┼──────────────────────────────────────────┘
│
│ 指针关联
▼
# 就绪队列 双向链表
┌──────────┐ ┌──────────┐ ┌──────────┐
│task_struct│◄──►│task_struct│◄──►│task_struct│
│ 进程A │ │ 进程B │ │ 进程C │
└──────────┘ └──────────┘ └──────────┘
核心:Linux 不在 task_struct 直接裸存寄存器,而是内嵌 thread_struct 专门存放硬件上下文。
场景:进程 A 时间片用完 → 切 进程 B
步骤 1:时间片耗尽,触发时钟硬件中断
CPU 停下当前执行的 进程 A,进入内核中断处理函数。
步骤 2:保存进程 A 上下文(CPU → task_struct)
把 CPU 里所有硬件寄存器 的值,逐个读取
拷贝到 进程 A 的 task_struct 内嵌的 thread_struct 对应字段里
- CPU.PC → A.thread_struct.pc
- CPU.SP → A.thread_struct.sp
- CPU.regs → A.thread_struct.regs[]
保存完后:CPU 寄存器现场被封存,进程 A 挂起
步骤 3:操作系统调度器工作
把 进程 A 重新挂载到就绪队列尾部
调度器从就绪队列头部选出下一个可运行进程 进程 B
步骤 4:恢复进程 B 上下文(task_struct → CPU)
从 进程 B 的 task_struct.thread_struct 取出所有备份寄存器值
逐个写回 CPU 物理寄存器
- B.thread_struct.pc → CPU.PC
- B.thread_struct.sp → CPU.SP
- B.thread_struct.regs [] → CPU 通用寄存器
CPU 寄存器完全被换成进程 B 的运行现场
步骤 5:CPU 继续执行
CPU 按新的 PC 地址,从进程 B 上次中断的位置继续往下执行,用户无感知。
结论:
寄存器就是 CPU 内部的临时空间!寄存器不等于寄存器里面的数据!(这是空间与内容的本质区别)
如何切换
进程切换可以类比为生活中“换工作”的场景。
假设你是一位多面手,同时负责两份工作:一份是办公室文案工作,另一份是咖啡店的兼职。当你在办公室工作时,你的桌面(可以类比为CPU寄存器)上摆放着文案工具,比如笔记本、笔、电脑文档等(这些是当前进程的上下文)。突然,咖啡店老板打电话来,说店里很忙需要你过去帮忙。于是你开始“切换工作”:你迅速收拾桌面上的文案工具,把它们放进抽屉(保存当前进程的上下文到进程控制块PCB),然后拿起咖啡师的围裙、咖啡豆、杯子等咖啡店工作需要的工具(加载新进程的上下文到寄存器),赶往咖啡店继续工作。
当你在咖啡店工作一段时间后,办公室又有紧急任务需要你回去处理。于是你再次收拾咖啡店的工具,把它们放回咖啡店的储物柜(保存咖啡店进程的上下文),然后重新拿出文案工具,回到办公室继续之前未完成的文案工作(恢复文案进程的上下文并继续执行)。这个过程就是进程切换,操作系统通过保存和恢复上下文,让不同的进程能够在CPU上交替运行,就像你在两份工作之间切换一样。

CPU调度器是操作系统的组件,负责决定何时将哪个进程分配给CPU运行。它会根据调度算法(如先来先服务、时间片轮转、优先级调度等)选择一个就绪的进程运行。
当一个进程被调度到CPU上运行时,CPU寄存器会加载该进程的运行临时数据(如程序计数器、指令寄存器、通用寄存器等),这些数据构成了进程的上下文。寄存器中的内容是进程运行的 “现场”,记录了进程执行到哪里、使用了哪些数据等关键信息。
如果进程运行过程中发生了中断(如时间片用完、I/O请求完成等),或者有更高优先级的进程需要运行,调度器会将当前进程从CPU上剥离下来。此时,CPU寄存器中的内容(即进程上下文)会被保存到进程控制块(PCB)中,这个过程称为保存上下文。
当该进程再次被调度到CPU上运行时,调度器会从PCB中恢复之前保存的上下文数据到CPU寄存器中,这个过程称为恢复上下文。一旦上下文恢复,进程就可以从上次中断的地方继续执行。
简单来说,CPU寄存器是进程运行的 “工作台”,保存上下文就是把工作台上的东西收拾好存起来,恢复上下文就是把之前存的东西重新摆到工作台上,让进程继续工作。
上面生活例子描述的是一次完整的进程上下文切换过程,从一个进程被中断并保存上下文,到另一个进程被调度并恢复上下文,再到原进程再次被调度并恢复上下文继续运行。整个过程可以分为以下几个关键阶段:
当前进程被中断:CPU正在运行的进程由于时间片用完、I/O请求或其他事件被中断。
保存上下文:操作系统将当前进程的寄存器内容(包括程序计数器、栈指针、通用寄存器等)保存到该进程的进程控制块(PCB)中。
选择新进程:CPU调度器根据调度算法选择一个新的就绪进程。
恢复上下文:操作系统将新进程的上下文数据从其PCB恢复到CPU寄存器中。
新进程运行:新进程从上次中断的地方继续运行。
再次切换(可选):如果原进程再次被调度,其上下文会从PCB恢复到寄存器中,继续运行。
通常我们说 “一次上下文切换” 时,更多是指从一个进程切换到另一个进程的完整过程,即从保存上下文到恢复新进程上下文的整个过程。
进程切换,最核心的,就是保存和恢复当前进程的硬件上下文数据,即 CPU 内寄存器的内容!!!
在现代操作系统(如 Linux 2.4+ 内核)中,进程的上下文数据核心存储于进程控制块(PCB)——task_struct 结构中,但需明确:硬件上下文(寄存器状态)并非直接存于 task_struct 顶层,而是内嵌在 task_struct->thread_struct 子结构中,软件上下文则直接通过 task_struct 的各类字段描述,二者共同构成进程的完整状态。
task_struct 作为 Linux 内核描述进程的核心数据结构,囊括了进程运行所需的全部状态信息,具体可分为两类:
- 硬件上下文:即 CPU 寄存器的完整镜像,包括程序计数器(PC/RIP)、栈指针(SP/RSP)、通用寄存器(R0~R15 等)、状态标志寄存器(FLAGS/EFLAGS)、段寄存器等。这些数据是进程 “运行现场” 的核心,专门由
task_struct内嵌的thread_struct结构存储,而非task_struct顶层直接承载; - 软件上下文:涵盖进程的逻辑状态(如运行态 R、就绪态 T、阻塞态 S 等)、调度优先级、调度策略、内存资源描述(
mm_struct指针)、文件描述符表、信号处理机制、进程间关系(父 / 子进程指针)、资源限制等,直接通过task_struct的对应字段记录。
当进程因时间片耗尽、中断或主动放弃 CPU 时,操作系统的调度机制会触发上下文保存:内核通过汇编指令将当前 CPU 所有寄存器的实时值,逐一拷贝到该进程 task_struct->thread_struct 的对应字段中,完成硬件上下文的 “封存”;同时更新 task_struct 中的进程状态(如从运行态转为就绪态),并将进程挂载到就绪队列。
当该进程再次被调度器选中占用 CPU 时,恢复流程反向执行:内核从 task_struct->thread_struct 中读出之前保存的寄存器镜像,逐一写回 CPU 的物理寄存器;同时切换进程的地址空间(更新 CR3 寄存器指向新进程页表),并更新 task_struct 中的进程状态(转为运行态),使进程能从上次中断的指令位置无缝继续执行。
需特别说明 TSS(Task State Segment)的角色:在 x86 架构中,TSS 并未被现代 Linux 弃用,但作用已大幅简化 ——不再承担完整硬件上下文的保存 / 恢复工作(这一核心任务由 thread_struct 配合软件机制完成)。现代 Linux 中,每个 CPU 仅维护一个全局共享的 tss_struct(而非每个进程一个),其核心作用是:在用户态与内核态切换(如中断、系统调用)时,为 CPU 提供当前进程的内核栈顶指针(esp0/rsp0),辅助完成栈空间的快速切换;同时存储 I/O 权限位图等硬件级权限信息,而非参与进程切换的核心上下文管理。
综上,现代 Linux 的进程上下文切换以软件机制为主导:核心依赖 task_struct(含 thread_struct)完成完整上下文的存储与恢复,灵活且易于优化;TSS 仅作为硬件辅助机制,在特权级切换等特定场景中发挥作用,并非上下文切换的核心载体,其功能已从 “完整任务状态存储” 弱化到 “硬件级切换辅助”。

补充知识点:struct task_struct* current
在我们的操作系统内部存在一个全局的指针,这个指针称为 struct task_struct* current,是在内核层面上的全局指针,永远指向当前正在运行的进程。
也就是说进程在调度的时候,调度器选好要运行的进程后,就会把该进程 task_struct 的地址赋值到 current 当中。CPU 后续调度、访问当前进程的所有信息时,直接通过 current 指针找到它指向的进程即可。
# ==============================================
# 1. CPU 硬件层(每核独立)
# ==============================================
┌──────────────────────────────────────────────────┐
│ 单个CPU核心 │
│ 物理硬件寄存器组 │
│ 通用寄存器:R0~R15 │
│ 特殊寄存器:RIP(PC)、RSP(SP)、EFLAGS、CR3 │
│ 硬件段寄存器:TR 指向 本CPU的 tss_struct │
│ │
│ 【Per-CPU 私有】 │
│ struct task_struct *current ← 核心指针 │
│ 永远指向:本CPU正在运行的进程 │
└───────────────┬──────────────────────────────────┘
│
│ 1.保存现场:寄存器 → 旧进程thread_struct
│ 2.恢复现场:新进程thread_struct → 寄存器
▼
# ==============================================
# 2. 硬件辅助:TSS 任务状态段(每CPU 1个,全局共享)
# ==============================================
┌──────────────────────────────────────────────────┐
│ struct tss_struct (本CPU独有) │
│ 仅保存: │
│ rsp0/esp0 → 当前进程内核栈顶地址 │
│ ss0 → 内核栈段选择子 │
│ I/O 权限位图 │
│ 作用:用户态→内核态 硬件自动切栈 │
│ 不保存:任何通用寄存器、程序计数器 │
└───────────────┬──────────────────────────────────┘
│
│ 进程切换时:更新 rsp0 为新进程内核栈
▼
# ==============================================
# 3. 进程PCB:task_struct 进程控制块(每个进程1个)
# ==============================================
┌──────────────────────────────────────────────────┐
│ task_struct │
│ 软件上下文字段: │
│ pid、进程状态、优先级、mm_struct *mm │
│ 文件描述符、信号处理、进程链表指针等 │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ struct thread_struct 硬件上下文备份区 │ │
│ │ 专门存放一套CPU寄存器镜像 │ │
│ │ regs[] 通用寄存器备份 │ │
│ │ rip 程序计数器备份 │ │
│ │ rsp 栈指针备份 │ │
│ │ flags 状态寄存器备份 │ │
│ └────────────────────────────────────────────┘ │
│ │
│ 内核栈指针:void *stack 指向本进程内核栈 │
└───────────────┬──────────────────────────────────┘
│
│ 链表指针挂载
▼
# ==============================================
# 4. 内核调度:就绪队列 双向链表
# ==============================================
┌──────────┐ ┌──────────┐ ┌──────────┐
│task_struct│◄──►│task_struct│◄──►│task_struct│
│ 进程A │ │ 进程B │ │ 进程C │
└──────────┘ └──────────┘ └──────────┘
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)