操作系统之线程和进程
本文档用于梳理操作系统中线程和进程的核心概念,查缺补漏,巩固基础知识。
进程与线程及应用场景
本质区别:
- 进程是资源分配的基本单位,相当于一个独立运行的程序,有独立的内存空间。
- 线程是CPU调度的基本单位,是进程内的执行单元,共享进程的内存空间。
主要区别:
- 资源开销:进程开销大(需要独立内存),线程开销小(共享内存)
- 通信方式:进程间通信复杂(需要IPC机制),线程间通信简单(直接访问共享变量)
- 稳定性:进程间互不影响,一个线程崩溃可能导致整个进程崩溃。
- 创建销毁:进程慢,线程快
进程适用场景:
- 需要高稳定性的场景
- 需要安全隔离的场景
- 独立应用之间的协作
线程适用场景:
- 需要频繁数据交互的场景
- 计算密集型任务的并行处理
- IO密集型任务
- 需要快速响应的场景
线程的实现方式(用户级 vs 内核级)
用户级线程(User-Level Thread,ULT)
实现原理:
- 线程管理由用户空间的线程库完成
- 内核不知道线程的存在,只看到进程
- 线程切换在用户态完成,不需要系统调用
优点:
- 切换快,无需陷入内核,不用上下文切换特权级
- 开销小,创建、销毁、调度都在用户空间
- 灵活,可以自定义调度算法
- 跨平台,不依赖内核支持
缺点:
- 无法利用多核,内核只调度进程,多个线程只能在一个CPU上跑
- 阻塞问题,一个线程阻塞,整个进程都阻塞
- 无时间片,内核不感知线程,无法给每个线程分配CPU时间
内核级线程(Kernel-Level Thread,KLT)
实现原理:
- 线程管理由操作系统内核完成
- 内核为每个线程维护TCB(线程控制块)
- 线程切换需要系统调度,涉及内核态切换
优点:
- 支持多核,内核可以把不同的线程调度到不同的CPU
- 不会阻塞整个进程,一个线程阻塞,其他线程继续运行
- 公平调度,内核给每个线程分配时间片
缺点:
- 切换慢。需要用户态和内核态切换,开销大
- 资源消耗大,每个线程都需要内核资源
- 数量限制,内核线程数量受系统限制
进程的状态转换图及各状态转换触发条件
进程三种基本状态
- 就绪态(Ready):进程已获得除CPU外的所有资源,等待CPU调度
- 运行态(Running):进程正在CPU上执行
- 阻塞态(Blocked/Waiting):进程等待某个事件(如IO完成),暂时无法运行
进程转换图
状态转换的触发条件
-
创建 → 就绪
进程创建完成,分配了PCB和必要资源。比如:fork()系统调用后,子进程进入就绪队列
-
就绪 → 运行
进程调度器选中该进程,分配CPU。比如:时间片轮询算法选中该进程
-
运行 → 就绪
遇到时间片用完(时间片轮转)或者更高优先级的进程抢占(抢占式调度)。比如:进程运行10ms后时间片到,被放回就绪队列
-
运行 → 阻塞
等待IO操作(读取文件,网络请求)或申请资源失败(信号量P操作失败)或等待事件(wait等待子进程,sleep()睡眠)。比如:进程执行read()读文件,等待磁盘IO完成。
注意:这是主动行为,进程自己请求阻塞。
-
阻塞 → 就绪
IO操作完成(磁盘中断通知)或等待的事件发送(子进程结束、锁释放)或睡眠时间到。比如:文件读取完成,磁盘控制器发出中断,进程被唤醒。
注意:不能直接到运行态,需要先进入就绪队列排队
-
运行 → 终止
正常结束(exit()系统调用)或异常终止(段错误,除零错误)或被杀死(kill信号)。比如:程序执行完return 0,进程结束。
五态模型(扩展)
新增了两个状态
新建态(New)
- 进程刚创建,还未进入就绪队列
- 正在申请资源
终止态(Terminated)
- 进程结束,等待系统收回资源
- 也叫僵尸态(Zombie)
七态模型(带挂起)
如果考虑内存交换,还有:
就绪挂起(Ready Suspended):进程在外存中等待,等待被调入内存
阻塞挂起(Blocked Suspended):进程在外存中等待事件
触发条件:
- 内存不足时,系统将低优先级进程换出到磁盘(挂起)
- 内存充足或优先级提高,换回内存(激活)
注意
- 阻塞态不可能直接到运行态,必须先到就绪态
- 就绪态不可能直接到阻塞态,只有运行的进程才能请求IO或资源
- 运行态只有一个CPU就只有一个进程,多核CPU可以多个进程同时运行
- 状态转换是原子操作,由操作系统内核完成,不会被打断。
实际例子
- 新建 → 就绪(下载线程创建)
- 就绪 → 运行(获得CPU)
- 运行 → 阻塞(发起网络IO,等待数据)
- 阻塞 → 就绪(网络数据到达,网卡中断)
- 就绪 → 运行(再次获得CPU)
- 运行 → 就绪(时间片到,让出CPU)
- …循环直到下载完成
- 运行 → 终止(下载完成,线程结束)
上下文切换的具体开销来源
直接开销
保存和恢复CPU寄存器
- 保存当前进程的通用寄存器,程序计数器,栈指针,状态寄存器
- 恢复下一个进程的寄存器
切换内核栈
- 每个进程有独立的内核栈
- 需要切换栈指针
切换页表(虚拟内存映射)
- 更新CR3寄存器(指向页表基址)
- 不同进程有不同的虚拟地址空间
内核态和用户态切换
- 进程切换需要陷入内核
- 涉及特权级切换
间接开销
TLB失效(Translation Lookaside Buffer)
- TLB是页表缓存,加速地址转换
- 切换进程后,TLB内容失效(除非支持ASID/PCID)
- 新进程运行时需要重新填充TLB
CPU缓存失效(Cache Miss)
- L1/L2/L3缓存中旧进程的数据失效
- 新进程需要从内存加载数据到缓存
- Cache冷启动问题
流水线清空
- CPU指令流水线需要重新填充
- 分支预测失控
调度器开销
- 选择下一个运行的进程
- 更新调度队列,统计信息
上下文切换开销优化策略
减少切换次数
- 增加时间片
- 使用亲和性
使用线程代替进程
- 同一进程的线程共享地址空间
- 切换不需要切换页表
- TLB和缓存可以部分保留
使用协程/纤程(Coroutine/Fiber)
- 用户态调度,不陷入内核
- 只保留栈指针和程序计数器
- 不涉及页表、TLB切换
减少线程/进程数量
- 使用IO多路复用,一个线程处理多个连接,减少上下文切换
- 线程池,复用线程,避免频繁创建和销毁
硬件层面优化
- 使用PCID,现代计算机支持,TLB条目自带进程标识,切换时不完全失效
- 使用ASID,ARM架构的类似技术,减少TLB刷新
算法层面优化
- 批处理
- 工作窃取
- 用户态调度器
操作系统配置
- 调整调度策略
- 禁用不必要的后台任务
实际案例
- Nginx(事件驱动):使用epoll + 少量worker进程,避免大量进程切换,单worker处理数千连接
- Redis(单线程):单线程 + IO多路复用,完全避免上下文切换,性能极高
- 数据库连接池:复用连接,减少进程/线程创建,降低切换频率
多线程与多进程的选择依据
| 维度 | 多线程 | 多进程 |
|---|---|---|
| 内存 | 共享内存空间 | 独立内存空间 |
| 通信 | 直接访问共享变量 | 需要IPC(管道/消息队列等) |
| 开销 | 小(创建/切换快) | 大(创建/切换慢) |
| 稳定性 | 一个线程崩溃全崩 | 进程间互不影响 |
| 多核利用 | 支持 | 支持 |
| 编程难度 | 复杂(需要同步) | 相对简单(天然隔离) |
数据共享需求
选择多线程:需要频繁共享数据,数据量大,拷贝成本高
选择多进程:数据相对独立,不需要频繁通信
稳定性和容错要求
选择多线程:可靠性要求不高,追求性能
选择多进程:可靠性要求高,一个任务失败不能影响其他任务
资源消耗考虑
选择多线程:资源有限,需要大量并发单元
选择多进程:资源充足,并发数量较少
开发和调式难度
选择多线程:团队有并发编程经验,能够处理竞态条件,死锁等问题
选择多进程:团队经验不足,追求简单可靠,不想处理复杂的锁和同步
任务特性
选择多线程:大量IO等待,线程可以等待时让出CPU
选择多进程:计算密集任务,避免GIL限制,充分利用多核
安全性和权限隔离
选择多线程:信任所有代码,同一用户/权限
选择多进程:需要权限隔离,安全敏感场景
进程间通信(IPC)的机制
共享内存(Shared Memory)
实现原理:操作系统分配一块物理内存,将这块内存映射到多个进程的虚拟地址空间,进程直接读写这块内存。
特点:需要配合信号量/互斥锁解决同步问题,无数据拷贝,效率最快,编程复杂度高
管道(Pipe)
实现原理:操作系统在内核中创建一个缓存区,一个进程写入,另一个进程读取,单向传输。半双工通信,数据先进先出
特点:只能用于亲缘关系的进程(父子进程),匿名管道随进程结束而消失,命名管道可无亲缘关系进程通信
消息队列(Message Queue)
实现原理:内核维护一个消息链表,进程可以按消息类型读取,不必按FIFO顺序,每条消息有类型标识和数据内容
特点:支持随机读取(可按类型选择),消息的固定格式和大小限制,消息持久化,进程结束后消息仍然存在
套接字(Socket)
实现原理:通过网络协议栈通信,可以跨主机通信(TCP/UDP),本地Socket(Unix Domain Socket)性能更高
特点:支持跨网络通信,双向通信,使用灵活但开销大
信号量(Semaphore)
实现原理:一个计数器,用于控制资源访问,P操作(Wait)计数减1,为0则阻塞。V操作(signal)计数加1,唤醒等待进程
特点:主要用于同步,不直接传递数据,解决资源共享的互斥访问问题
信号(Signal)
实现原理:操作系统向进程发送软件中断,进程收到信号后执行预定义的处理函数
特点:传递信号量少,只是通知,用于异步通知
死锁的四个必要条件
同时满足会发生死锁
- 互斥条件(Mutual Exclusion):资源不能被多个进程同时使用
- 持有并等待(Hold and Wait):进程已经持有至少一个资源,还等待获取其他资源
- 不可剥夺(No Preemption):资源不能被强制抢占,只能由持有者主动释放
- 循环等待(Circular Wait):存在进程链:P1等待P2的资源,P2等待P3的资源…Pn等待P1的资源
预防死锁策略
破坏任意必要条件
- 破坏互斥:让资源可以共享
- 破坏持有并等待:进程启动时申请所有需要的资源
- 破坏不可剥夺:申请新资源失败时,主动释放已持有的资源
- 破坏循环等待:给资源编号,进程必须按递增顺序申请(最常用)
避免死锁策略
银行家算法(Banker’s Algorithm)
核心思想:在分配资源前,先判断分配后系统是否处于安全状态
实现步骤:
- 进程申请资源时,先试探性分配
- 检查是否存在一个安全序列(所有进程都能顺序执行完)
- 如果安全,真正分配,不安全,拒绝并让进程等待
死锁检测与恢复
- 检查,定期运行死锁检查算法
- 恢复:终止一个或多个死锁进程,强制剥夺某些进程资源
孤儿进程和僵尸进程
孤儿进程:父进程先于子进程结束,子进程变成孤儿进程。(几乎没有危害)
僵尸进程:子进程已经结束,但父进程没有调用wait()、waitpid()回收,进程已死,但PCB(进程控制块)还在
| 特性 | 孤儿进程 | 僵尸进程 |
|---|---|---|
| 定义 | 父进程先死 | 子进程先死,父进程不回收 |
| 父进程 | 已退出 | 还活着 |
| 是否运行 | 可能还在运行 | 已经结束 |
| 危害 | 无害 | 占用进程表 |
| 处理 | 系统自动处理 | 需要父进程回收 |
僵尸进程预防
- 父进程主动回收
- 信号处理(推荐)
- 两次fork(守护进程)
僵尸进程清理
- 杀死父进程(推荐)
- 重启系统
协程和线程的本质区别
调度方式
线程:抢占式调度
协程:协作式调度
切换开销
线程:切换比较重
协程:切换比较轻
并发模型
线程:真并行
协程:伪并发
内存占用
线程:每个线程由独立的栈比较大
协程:协程栈很小
| 维度 | 协程(Coroutine) | 线程(Thread) |
|---|---|---|
| 调度者 | 用户态程序自己调度 | 内核调度 |
| 切换位置 | 用户态,不陷入内核 | 内核态,需要系统调用 |
| 切换时机 | 程序主动让出CPU | 被动(时间片到/阻塞) |
| 并发性 | 单线程内并发(伪并行) | 真正的多核并行 |
| 栈 | 可以很小(几KB) | 较大(MB级别) |
| 开销 | 极小(纳秒级) | 较大(微秒级) |
fork()的工作原理
fork()是Unix/Linux创建新进程的系统调用,调用一次,返回两次(父进程和子进程各返回一次),子进程是父进程的副本。
fork()的返回值含义
pid_t pid = fork();
if (pid < 0) {
// 返回值 < 0:fork失败
perror("fork failed");
} else if (pid == 0) {
// 返回值 = 0:当前是子进程
printf("我是子进程,PID=%d\n", getpid());
} else {
// 返回值 > 0:当前是父进程,pid是子进程的PID
printf("我是父进程,PID=%d,子进程PID=%d\n", getpid(), pid);
}
为什么这样做?
- 子进程返回0:子进程没有子进程,返回0作为标识
- 父进程返回子进程PID:父进程需要知道子进程的PID来管理它
- 失败返回-1:遵循Unix错误处理惯例
进程调度算法比较
| 算法 | 抢占 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| FCFS | 否 | 简单公平 | 护航效应 | 批处理 |
| SJF | 否/是 | 等待时间最短 | 饥饿、难预测 | 已知长度 |
| RR | 是 | 响应快、公平 | 切换开销 | 交互式 |
| 优先级 | 否/是 | 灵活 | 饥饿 | 实时系统 |
| 多级反馈 | 是 | 综合最优 | 复杂 | 通用OS |
进程与线程核心解析

被折叠的 条评论
为什么被折叠?



