进程、线程、协程 内存模型与上下文切换完整终极文档 (1)
约 4580 字大约 15 分钟
2026-08-08
前言
本文从零梳理操作系统进程、线程、协程三层任务模型,包含:内存布局、栈结构、控制块、调度主体、上下文保存位置、切换流程、开销差异、核心本质区别。
所有内容贴合 Linux 内核原理 + 用户态协程 Runtime 实现,适配面试、底层原理、并发架构理解。
一、进程 完整模型与上下文切换
1.1 进程内存模型(独立地址空间)
进程是操作系统资源分配的最小单位。
每个进程拥有完全独立的虚拟地址空间,由内核通过 mm_struct + 页表 管理。
进程用户空间布局:
代码段(.text)
全局数据段(.data / .bss)
堆(heap,malloc/mmap,向上增长)
用户栈(每个进程主线程独立)
1.2 进程内核资源(内核完全维护)
内核为每个进程在内核内存分配:
PCB(task_struct):进程控制块,内核态内存
mm_struct:虚拟内存管理结构体(页表基址、内存区间)
独立内核栈(8KB):系统调用、中断使用
文件描述符表、信号处理、进程状态、PID、父子关系等
1.3 进程上下文组成
进程上下文 = 用户空间上下文 + 内核硬件上下文
用户空间上下文
代码段、全局变量、堆、用户栈、页表映射关系。
内核硬件上下文(保存在 PCB->thread_struct)
CPU 寄存器、PC(指令指针)、RSP(栈指针)、状态寄存器。
1.4 进程上下文切换全过程(重量级)
触发时机:时间片耗尽、阻塞IO、主动让出、中断触发调度。
进入内核态:中断/系统调用陷入内核
保存旧进程硬件上下文:将当前 CPU 寄存器、PC、RSP 写入旧进程PCB
调度器选择新进程
切换虚拟地址空间:修改 CR3 寄存器(页表基址),刷新 TLB
恢复新进程硬件上下文:从新进程 PCB 加载寄存器、PC、内核栈
退出内核态,继续执行新进程用户代码
1.5 进程切换核心开销
态切换开销:必须用户态 ↔ 内核态 切换,产生特权级切换损耗
内存映射开销:修改 CR3 页表基址,全局刷新 TLB,造成大量虚拟地址翻译缺页
缓存失效开销:L1/L2 CPU 缓存全部失效,命中率暴跌
内核调度开销:内核操作就绪队列、修改进程状态、读写 PCB 内核结构体
结论:进程切换是操作系统最重的任务切换,无法支撑高并发高频切换场景。
二、线程 完整模型与上下文切换
2.1 线程核心定义
线程是操作系统CPU 调度最小单位,属于轻量级进程。同进程内所有线程 共享进程全部用户态资源,仅保留少量私有资源。
2.2 线程内存模型(共享 + 私有严格区分)
线程共享资源(进程全局唯一)
整个虚拟地址空间、页表、mm_struct、CR3 映射关系
代码段、全局变量、静态变量、堆内存、mmap 映射区
文件描述符表、工作目录、用户权限、信号处理规则
线程私有资源(每线程独立)
独立用户栈(默认 8MB):存放线程局部变量、函数栈帧
独立内核栈:系统调用、中断处理专属内核栈
TCB 线程控制块(内核 task_struct):内核态内存维护
独立硬件上下文:寄存器、PC、RSP、线程状态、调度优先级
2.3 线程上下文组成
线程上下文同样分为两部分,全部由内核管理、保存于 TCB:
用户上下文:线程私有用户栈栈帧、局部变量(无独立地址空间)
内核硬件上下文:通用寄存器、PC 指令指针、RSP 栈指针、状态寄存器、内核栈指针
2.4 线程上下文切换完整流程
触发时机:时间片轮转、线程阻塞、锁竞争、中断调度
触发中断/系统调用,陷入内核态
保存当前线程硬件上下文,写入自身 TCB(task_struct)
内核调度器选取就绪线程
不修改 CR3、不刷新 TLB(共享进程页表与地址空间)
从新线程 TCB 恢复寄存器、PC、内核栈现场
退出内核态,继续执行新线程用户代码
2.5 线程切换开销与痛点
线程切换远轻于进程,但依然属于内核级重型切换:
必须陷入内核态,存在特权级切换开销
需要读写内核 TCB、操作内核就绪队列
线程一旦 IO 阻塞,内核直接挂起整条 OS 线程,让出 CPU 时间片
结论:线程适合中等并发,不适合十万、百万级超高 IO 并发场景。
三、协程 完整模型与上下文切换(用户态终极并发)
3.1 协程核心本质
协程是 纯用户态软件实现的轻量级任务,操作系统内核完全无感知。内核只识别底层 OS 线程,完全看不到上层协程。
协程完全复刻内核任务模型,但全部搬到用户态、用户堆实现,包含三件套:
用户态协程控制块(模拟 PCB/TCB)
用户态协程私有栈(mmap/malloc 堆分配)
用户态协程调度器(模拟内核 CFS 调度)
3.2 协程内存模型(核心重难点)
协程共享资源
完全复用宿主线程所有内核与用户资源:
进程虚拟地址空间、页表、mm_struct、CR3
全局变量、堆、代码段、所有进程共享资源
宿主线程唯一内核栈(所有协程共用)
协程私有资源(用户堆分配)
协程控制块:状态、断点 PC、栈指针、寄存器快照、等待状态
协程私有小栈:几 KB~几十 KB,动态伸缩(Go)
关键纠正(高频面试误区)
✅ 宿主线程原生用户栈、内核栈、TCB 依然存在、不会销毁
✅ 协程运行时,通过用户态汇编 手动改写 CPU RSP 栈指针,临时放弃线程原生用户栈,使用协程私有栈执行业务
✅ 协程无独立内核栈,系统调用时临时借用宿主线程内核栈
3.3 协程上下文存储位置
全部存储在 用户堆内存,内核零参与、零感知:
协程运行状态:就绪 / 运行 / 阻塞
任务断点:暂停时的 PC 指令地址
硬件现场:通用寄存器、RSP 栈指针快照
私有栈地址与栈边界
3.4 协程上下文切换完整流程(纯用户态)
触发时机:await、IO 等待、主动让出、runtime 调度
全程无系统调用、无内核态陷入、无内核参与
用户态代码触发切换(yield / Gosched / await)
保存旧协程现场:用户态汇编将当前 CPU 寄存器、PC、RSP 写入【旧协程用户态控制块】
用户态调度器从就绪队列选取新协程
篡改当前 OS 线程硬件上下文:直接改写 CPU 物理寄存器 RSP = 新协程私有栈地址
PC = 新协程上次暂停的断点指令地址
CPU 直接在当前宿主线程上继续执行新协程代码
3.5 核心真相(面试必问)
协程切换 = 用户态直接修改 OS 线程的 PC / RSP / 寄存器
修改的就是真实 CPU 硬件上下文,但是:
不走内核
不更新 TCB
不刷新 TLB
不产生态切换开销
3.6 协程优势与局限
优势
切换开销极低(纳秒级),支持百万级并发
IO 阻塞只挂起协程,不阻塞底层 OS 线程
无 TLB 失效、无缓存失效
创建销毁成本极低(仅堆内存分配释放)
局限
单线程协程无法利用多核 CPU,需多 M 多 P 模型(Go)/ 多线程混合
协程为协作式调度,死循环 CPU 密集代码会卡死整体调度
阻塞型裸系统调用会阻塞宿主线程
四、进程 / 线程 / 协程 终极横向对比(面试背诵版)
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度主体 | 内核调度器 | 内核调度器 | 用户态 Runtime |
| 资源隔离 | 完全独立地址空间 | 共享进程地址空间 | 共享线程地址空间 |
| 控制块位置 | 内核 PCB | 内核 TCB | 用户堆自定义结构体 |
| 栈结构 | 独立用户栈 + 独立内核栈 | 独立用户栈 + 独立内核栈 | 用户态私有协程栈 + 共享线程内核栈 |
| 切换是否陷内核 | 是 | 是 | 否 |
| 是否刷新 TLB | 是 | 否 | 否 |
| 阻塞影响 | 阻塞整个进程 | 阻塞整条线程 | 仅阻塞当前协程 |
| 并发量级 | 百级 | 千级 | 百万级 |
五、一句话终极总结(面试绝杀)
进程:内核实现的资源隔离模型,有独立地址空间、独立双栈,切换最重,用于程序资源隔离。
线程:内核实现的轻量调度模型,共享进程资源、独有双栈,无需刷新 TLB,用于中等并发多核调度。
协程:用户态自研的超轻任务模型,复用线程所有内核资源,自己实现控制块、栈、调度器,用户态直接修改线程 PC/RSP 完成切换,无内核开销,支撑超高 IO 并发。
六、协程遇到 IO 阻塞时的核心处理机制(面试高频)
6.1 核心前提:协程最怕「内核阻塞」
OS 线程一旦执行阻塞式系统调用(read/recv/connect/sleep),内核会直接挂起整条线程,让出 CPU,导致该线程上所有协程全部卡死。
因此协程框架 绝对不会直接调用阻塞系统调用,所有协程 IO 全部基于:用户态事件驱动 + IO 多路复用(epoll/kqueue) 实现。
6.2 协程 IO 处理完整流程(标准流程)
以网络 recv 读取阻塞场景为例:
协程发起 IO 请求 协程调用封装好的异步读接口,不直接调用阻塞 read。
Runtime 将 fd 托管给多路复用器 Runtime 将当前文件描述符 fd 注册到 epoll,监听「可读/可写」事件。
用户态主动让出 CPU(关键) Runtime 将当前协程标记为 阻塞等待状态,保存该协程上下文(PC/RSP/栈现场),不陷入内核、不挂起线程。
调度其他就绪协程运行 宿主线程立刻调度执行其他就绪协程,CPU 持续工作,无空闲、无阻塞。
IO 事件就绪触发回调 当内核缓冲区数据到达,epoll 感知事件就绪,唤醒对应等待的协程。
恢复协程上下文继续执行 Runtime 将该协程重新加入就绪队列,用户态恢复 PC/RSP 现场,协程从阻塞断点继续执行业务逻辑。
6.3 和线程阻塞的本质区别
线程阻塞(内核级阻塞)
线程调用阻塞 syscall → 内核直接挂起线程
线程彻底让出 CPU,内核重新调度其他线程
开销大、并发上限低
协程阻塞(用户态阻塞)
协程 IO 阻塞 仅阻塞当前协程
宿主线程持续运行,继续调度其他协程
无内核态切换、无线程挂起、CPU 利用率极高
6.4 特殊场景:协程遇到真阻塞系统调用怎么处理?
如果协程内不可避免执行原生阻塞调用(文件IO、阻塞sleep、第三方阻塞库),会卡住整条宿主线程。
成熟协程框架解决方案(Go MPG 模型核心设计):
协程 G 执行阻塞 syscall,绑定的工作线程 M 进入内核阻塞;
Go Runtime 主动解绑 P(调度器上下文);
调度器 P 绑定新的空闲 M(OS线程),继续调度其他 G 协程;
阻塞结束后,原 M 唤醒,归还 P 或继续休眠。
通过 M 与 P 动态解绑,彻底解决协程阻塞系统调用卡死调度的问题。
6.5 一句话面试绝杀总结
协程遇到 IO 不会阻塞线程,而是通过用户态 epoll 事件监听 + 主动让出调度,将自身标记为阻塞等待,线程继续跑其他协程;IO 就绪后重新唤醒协程恢复执行,全程无内核线程阻塞、无上下文切换开销,是超高并发 IO 模型的核心原理。
七、Java 如何实现用户态 Epoll + 主动让出调度(Java 协程底层原理)
7.1 核心前置认知
Java 原生传统线程是内核级线程,没有原生协程、没有用户态调度。但 Java 可以完全复刻前面讲解的「协程三件套 + Epoll IO 多路复用 + 用户态让出调度」模型,对应技术栈:
用户态事件监听:Java NIO Epoll(Linux)/ Kqueue(Mac)
用户态任务控制块/栈/调度:线程池 + 任务队列 + 状态机模拟协程
主动让出CPU:异步回调 / CompletableFuture / 虚拟线程挂起机制
本质:Java 高性能并发框架(Netty、Jetty、JDK21虚拟线程),全部是OS线程 + 用户态Epoll + 用户态任务调度 的协程模型。
7.2 Java Epoll 到底是什么(内核多路复用、用户态监听)
7.2.1 Epoll 核心能力
Epoll 是 Linux 内核提供的事件通知机制,解决阻塞IO、select/poll 低效问题:
用户态将大量 fd(连接)注册到内核 Epoll 实例
内核全程监听 fd 读写状态,无事件时不占用CPU
有事件就绪时,内核批量返回就绪 fd 列表给用户态
关键:Epoll 阻塞只阻塞空轮询的这条OS线程,不会阻塞所有连接,完美实现「一个线程管理上万IO任务」。
7.2.2 Java NIO Epoll 调用链路
Java NIO 的 Selector 底层:
Linux 系统:
epoll_create / epoll_ctl / epoll_waitJava 层:
Selector.open()创建内核Epoll实例channel.register(selector, OP_READ)注册fd到内核selector.select()用户态阻塞等待事件
7.3 Java 如何实现「协程式主动让出调度」
Java 没有原生 yield 协程指令,但通过 任务状态剥离 + 回调切走执行权 实现用户态主动让出,和 Go/Python 协程让出逻辑 100% 对齐。
7.3.1 传统BIO(内核阻塞,无法让出)
BIO 调用 read 阻塞系统调用 → 内核挂起整条Java线程 → 线程彻底卡死,无法处理其他任务,是内核级阻塞。
7.3.2 NIO+Epoll 用户态让出模型(核心)
Java 通过拆分任务执行断点实现协程式让出,完整流程:
任务执行到IO断点:业务线程读取数据,发现内核缓冲区无数据
放弃执行权(主动让出):不阻塞线程,直接退出当前任务逻辑
保存任务上下文(用户态):将当前连接、业务状态、回调函数存入用户内存(任务队列/Attachment)
线程复用处理其他任务:当前OS线程立刻去处理其他就绪IO任务
内核Epoll监听事件:数据到达后内核唤醒select
用户态恢复任务:取出之前保存的上下文,继续执行后续业务逻辑
本质等价协程:IO阻塞时仅挂起任务,不挂起线程,纯用户态调度切换。
7.4 Netty 完整复刻「协程三件套」(面试绝杀)
Netty 是 Java 最标准的用户态协程调度模型落地实现,完美对应前文协程机制:
协程控制块(用户态):
Channel对象 + 自定义附件 Attachment(保存任务状态、断点数据)协程私有栈/上下文:每次IO任务的回调栈、业务状态机(用户堆内存维护)
用户态调度器:NioEventLoop(单线程轮询 + 任务队列调度)
Epoll用户态IO监听:底层Epoll多路复用
主动让出CPU:IO未就绪直接退出任务,线程轮转下一个任务
Netty 单线程可以支撑10w+并发连接,原理和协程完全一致。
7.5 JDK21 虚拟线程(真正Java用户态协程)
虚拟线程是 JDK 官方补齐的完整用户态协程模型,彻底对标 Go goroutine:
虚拟线程是用户态任务,内核完全无感知
基于 Carrier Thread(载体OS线程)运行,复用内核栈、页表
遇到阻塞API自动用户态让出:JDK 底层改写了所有阻塞IO、sleep、锁逻辑
阻塞时 JVM 保存虚拟线程上下文(用户态),载体线程立刻调度其他虚拟线程
完全等价前文协程原理:用户态保存上下文、不陷内核、不阻塞OS线程、用户态调度。
7.6 核心对比:Java 三种并发模型层级
BIO 内核线程模型:一线程一连接,内核阻塞、开销极大、并发上限低
NIO/Netty 用户态多路复用模型:OS线程+Epoll+用户态任务调度,手动实现协程让出逻辑,高性能IO并发
JDK21 虚拟线程:JVM 原生用户态协程,自动挂起/让出/恢复,完整协程模型
7.7 面试一句话总结(Java专属)
Java 通过 Epoll 内核IO多路复用实现事件监听,在用户态通过任务状态保存 + 回调断点退出实现主动让出调度,不阻塞底层OS线程,单线程可管理上万IO任务;Netty 手动实现该用户态协程模型,JDK21虚拟线程由JVM底层原生实现协程挂起与调度,彻底摆脱内核线程阻塞开销。
(注:部分内容可能由 AI 生成)