Skip to content

进程线程

一、进程与线程的本质区别

进程是资源分配的基本单位,相当于一个独立运行的程序,有独立的内存空间。

线程是 CPU 调度的基本单位,是进程内的执行单元,共享进程的内存空间。

主要区别:

  1. 资源开销:进程开销大(需要独立内存),线程开销小(共享内存)
  2. 通信方式:进程间通信复杂(需要 IPC 机制),线程间通信简单(直接访问共享变量)
  3. 稳定性:进程间互不影响,一个线程崩溃可能导致整个进程崩溃
  4. 创建销毁:进程慢,线程快

进程适用场景:

  • 需要高稳定性的场景
  • 需要安全隔离的场景
  • 独立应用之间的协作

线程适用场景:

  • 需要频繁数据交互的场景
  • 计算密集型任务的并行处理
  • IO 密集型任务
  • 需要快速响应的场景

二、线程的实现方式

用户级线程(ULT)

线程管理由用户空间的线程库完成,内核不知道线程的存在,只看到进程。

优点:

  • 切换快,无需陷入内核
  • 开销小,创建、销毁、调度都在用户空间
  • 灵活,可以自定义调度算法
  • 跨平台,不依赖内核支持

缺点:

  • 无法利用多核,多个线程只能在一个 CPU 上跑
  • 阻塞问题,一个线程阻塞,整个进程都阻塞
  • 无时间片,内核不感知线程,无法给每个线程分配 CPU 时间

内核级线程(KLT)

线程管理由操作系统内核完成,内核为每个线程维护 TCB(线程控制块)。

优点:

  • 支持多核,内核可以把不同的线程调度到不同的 CPU
  • 不会阻塞整个进程,一个线程阻塞,其他线程继续运行
  • 公平调度,内核给每个线程分配时间片

缺点:

  • 切换慢,需要用户态和内核态切换,开销大
  • 资源消耗大,每个线程都需要内核资源
  • 数量限制,内核线程数量受系统限制

三、进程的状态转换

三种基本状态

  • 就绪态(Ready):进程已获得除 CPU 外的所有资源,等待调度
  • 运行态(Running):进程正在 CPU 上执行
  • 阻塞态(Blocked):进程等待某个事件(如 IO 完成),暂时无法运行

状态转换触发条件

  1. 创建 → 就绪:进程创建完成,分配了 PCB 和必要资源。比如 fork() 后,子进程进入就绪队列
  2. 就绪 → 运行:调度器选中该进程,分配 CPU
  3. 运行 → 就绪:时间片用完或更高优先级进程抢占
  4. 运行 → 阻塞:等待 IO、申请资源失败、sleep() 等。这是主动行为,进程自己请求阻塞
  5. 阻塞 → 就绪:IO 完成、等待的事件发生。不能直接到运行态,需要先进入就绪队列排队
  6. 运行 → 终止exit() 正常结束,或异常终止(段错误),或被 kill

五态模型

在三种基本状态基础上新增:

  • 新建态(New):进程刚创建,还未进入就绪队列,正在申请资源
  • 终止态(Terminated):进程结束,等待系统收回资源,也叫僵尸态(Zombie)

七态模型(带挂起)

考虑内存交换:

  • 就绪挂起(Ready Suspended):进程在外存中等待被调入内存
  • 阻塞挂起(Blocked Suspended):进程在外存中等待事件

触发条件:内存不足时换出到磁盘(挂起),内存充足或优先级提高时换回内存(激活)。

注意事项

  • 阻塞态不可能直接到运行态,必须先到就绪态
  • 就绪态不可能直接到阻塞态,只有运行的进程才能请求 IO 或资源
  • 状态转换是原子操作,由操作系统内核完成,不会被打断

实际例子:下载线程的生命周期

  1. 新建 → 就绪(下载线程创建)
  2. 就绪 → 运行(获得 CPU)
  3. 运行 → 阻塞(发起网络 IO,等待数据)
  4. 阻塞 → 就绪(网络数据到达,网卡中断)
  5. 就绪 → 运行(再次获得 CPU)
  6. 运行 → 就绪(时间片到,让出 CPU)
  7. …循环直到下载完成
  8. 运行 → 终止(下载完成,线程结束)

四、上下文切换的开销

直接开销

  • 保存和恢复 CPU 寄存器:通用寄存器、程序计数器、栈指针、状态寄存器
  • 切换内核栈:每个进程有独立的内核栈,需要切换栈指针
  • 切换页表:更新 CR3 寄存器,不同进程有不同的虚拟地址空间
  • 内核态和用户态切换:涉及特权级切换

间接开销

  • TLB 失效:TLB 是页表缓存,切换进程后内容失效(除非支持 ASID/PCID),新进程需要重新填充
  • CPU 缓存失效:L1/L2/L3 缓存中旧进程的数据失效,新进程需要从内存加载
  • 流水线清空:CPU 指令流水线重新填充,分支预测失控
  • 调度器开销:选择下一个进程、更新调度队列和统计信息

优化策略

减少切换次数:

  • 增加时间片
  • 使用 CPU 亲和性

使用线程代替进程:

  • 同一进程的线程共享地址空间
  • 切换不需要切换页表
  • TLB 和缓存可以部分保留

使用协程:

  • 用户态调度,不陷入内核
  • 只保留栈指针和程序计数器
  • 不涉及页表、TLB 切换

其他优化:

  • IO 多路复用,一个线程处理多个连接
  • 线程池,复用线程,避免频繁创建和销毁
  • 硬件层面:PCID(x86)/ ASID(ARM),减少 TLB 刷新

实际案例:

  • Nginxepoll + 少量 worker 进程,单 worker 处理数千连接
  • Redis:单线程 + IO 多路复用,完全避免上下文切换
  • 数据库连接池:复用连接,减少线程创建

五、多线程 vs 多进程

维度多线程多进程
内存共享内存空间独立内存空间
通信直接访问共享变量需要 IPC
开销小(创建/切换快)大(创建/切换慢)
稳定性一个线程崩溃全崩进程间互不影响
多核利用支持支持
编程难度复杂(需要同步)相对简单(天然隔离)

选择依据:

  • 频繁共享数据 → 多线程
  • 高可靠性要求 → 多进程
  • 资源有限、大量并发 → 多线程
  • 权限隔离、安全敏感 → 多进程
  • 大量 IO 等待 → 多线程
  • 计算密集、避免 GIL → 多进程

六、进程间通信(IPC)

共享内存(Shared Memory)

操作系统分配一块物理内存,映射到多个进程的虚拟地址空间,进程直接读写。

特点:无数据拷贝,效率最快。但需要配合信号量/互斥锁解决同步问题,编程复杂度高。

管道(Pipe)

内核中创建一个缓冲区,一个进程写入,另一个进程读取,单向传输,半双工。

  • 匿名管道:只能用于父子进程
  • 命名管道(FIFO):可用于无亲缘关系的进程

消息队列(Message Queue)

内核维护一个消息链表,进程可以按消息类型读取,不必按 FIFO 顺序。

特点:支持随机读取,消息有固定格式和大小限制,进程结束后消息仍然存在。

套接字(Socket)

通过网络协议栈通信,可以跨主机(TCP/UDP)。本地 Unix Domain Socket 性能更高。

特点:支持跨网络、双向通信,使用灵活但开销大。

信号量(Semaphore)

一个计数器,用于控制资源访问。P 操作(Wait)计数减 1,为 0 则阻塞;V 操作(Signal)计数加 1,唤醒等待进程。

主要用于同步,不直接传递数据。

信号(Signal)

操作系统向进程发送软件中断,进程收到后执行预定义的处理函数。

特点:只是通知,用于异步通知,传递信息量少。

七、死锁

四个必要条件(同时满足才会死锁)

  1. 互斥条件:资源不能被多个进程同时使用
  2. 持有并等待:进程已持有至少一个资源,还等待获取其他资源
  3. 不可剥夺:资源不能被强制抢占,只能由持有者主动释放
  4. 循环等待:P1 等 P2 的资源,P2 等 P3 的资源…Pn 等 P1 的资源

预防死锁(破坏任一条件)

  1. 破坏互斥:让资源可以共享
  2. 破坏持有并等待:进程启动时申请所有需要的资源
  3. 破坏不可剥夺:申请新资源失败时,主动释放已持有的资源
  4. 破坏循环等待:给资源编号,进程必须按递增顺序申请(最常用

避免死锁:银行家算法

核心思想:分配资源前,先判断分配后系统是否处于安全状态。

步骤:

  1. 进程申请资源时,先试探性分配
  2. 检查是否存在一个安全序列(所有进程都能顺序执行完)
  3. 安全则真正分配,不安全则拒绝并让进程等待

死锁检测与恢复

  1. 定期运行死锁检查算法
  2. 恢复:终止一个或多个死锁进程,或强制剥夺某些进程资源

八、孤儿进程和僵尸进程

孤儿进程:父进程先于子进程结束,子进程变成孤儿进程。几乎没有危害,系统会自动托付给 init 进程。

僵尸进程:子进程已经结束,但父进程没有调用 wait()/waitpid() 回收,进程已死但 PCB 还在。

特性孤儿进程僵尸进程
定义父进程先死子进程先死,父进程不回收
是否运行可能还在运行已经结束
危害无害占用进程表
处理系统自动处理需要父进程回收

僵尸进程预防:

  1. 父进程主动调用 wait()/waitpid() 回收
  2. 信号处理(推荐):捕获 SIGCHLD 信号,在 handler 中回收
  3. 两次 fork()(守护进程经典做法)

僵尸进程清理:杀死父进程(僵尸会被 init 回收),或重启系统。

九、协程 vs 线程

维度协程线程
调度者用户态程序自己内核调度
切换位置用户态,不陷入内核内核态,需要系统调用
切换时机程序主动让出 CPU被动(时间片到/阻塞)
并发性单线程内并发(伪并行)真正的多核并行
栈大小几 KBMB 级别
切换开销纳秒级微秒级

本质区别:

  • 线程是抢占式调度,协程是协作式调度
  • 线程切换比较重,协程切换极轻
  • 线程是真并行,协程是伪并发

十、fork() 的工作原理

fork() 是 Unix/Linux 创建新进程的系统调用。

核心特性:调用一次,返回两次(父进程和子进程各返回一次),子进程是父进程的副本。

返回值含义:

c
pid_t pid = fork();

if (pid < 0) {
    // fork 失败
} else if (pid == 0) {
    // 当前是子进程
} else {
    // 当前是父进程,pid 是子进程的 PID
}
  • 子进程返回 0:子进程没有子进程,返回 0 作为标识
  • 父进程返回子进程 PID:父进程需要知道子进程的 PID 来管理它
  • 失败返回 -1:遵循 Unix 错误处理惯例

十一、进程调度算法

算法抢占优点缺点适用场景
FCFS(先来先服务)简单公平护航效应批处理
SJF(短作业优先)否/是等待时间最短饥饿、难预测已知长度
RR(时间片轮转)响应快、公平切换开销交互式
优先级调度否/是灵活饥饿实时系统
多级反馈队列综合最优复杂通用 OS