操作系统之线程和进程

进程与线程核心解析

操作系统之线程和进程

本文档用于梳理操作系统中线程和进程的核心概念,查缺补漏,巩固基础知识。

进程与线程及应用场景

本质区别:

  • 进程是资源分配的基本单位,相当于一个独立运行的程序,有独立的内存空间。
  • 线程是CPU调度的基本单位,是进程内的执行单元,共享进程的内存空间。

主要区别:

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

进程适用场景:

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

线程适用场景:

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

线程的实现方式(用户级 vs 内核级)

用户级线程(User-Level Thread,ULT)

实现原理:

  • 线程管理由用户空间的线程库完成
  • 内核不知道线程的存在,只看到进程
  • 线程切换在用户态完成,不需要系统调用

优点:

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

缺点:

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

内核级线程(Kernel-Level Thread,KLT)

实现原理:

  • 线程管理由操作系统内核完成
  • 内核为每个线程维护TCB(线程控制块)
  • 线程切换需要系统调度,涉及内核态切换

优点:

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

缺点:

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

进程的状态转换图及各状态转换触发条件

进程三种基本状态

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

进程转换图

进程创建
资源分配完成
获得CPU(调度)
时间片用完
或被高优先级抢占
等待IO/资源
(主动请求)
执行完成
或异常终止
IO完成
或事件发生
资源回收
新建态
就绪态
运行态
阻塞态
终止态
已获得除CPU外
的所有资源
正在CPU上执行
等待事件,
不能直接到运行态

状态转换的触发条件

  1. 创建 → 就绪

    进程创建完成,分配了PCB和必要资源。比如:fork()系统调用后,子进程进入就绪队列

  2. 就绪 → 运行

    进程调度器选中该进程,分配CPU。比如:时间片轮询算法选中该进程

  3. 运行 → 就绪

    遇到时间片用完(时间片轮转)或者更高优先级的进程抢占(抢占式调度)。比如:进程运行10ms后时间片到,被放回就绪队列

  4. 运行 → 阻塞

    等待IO操作(读取文件,网络请求)或申请资源失败(信号量P操作失败)或等待事件(wait等待子进程,sleep()睡眠)。比如:进程执行read()读文件,等待磁盘IO完成。

    注意:这是主动行为,进程自己请求阻塞。

  5. 阻塞 → 就绪

    IO操作完成(磁盘中断通知)或等待的事件发送(子进程结束、锁释放)或睡眠时间到。比如:文件读取完成,磁盘控制器发出中断,进程被唤醒。

    注意:不能直接到运行态,需要先进入就绪队列排队

  6. 运行 → 终止

    正常结束(exit()系统调用)或异常终止(段错误,除零错误)或被杀死(kill信号)。比如:程序执行完return 0,进程结束。

五态模型(扩展)

新增了两个状态

新建态(New)

  • 进程刚创建,还未进入就绪队列
  • 正在申请资源

终止态(Terminated)

  • 进程结束,等待系统收回资源
  • 也叫僵尸态(Zombie)

七态模型(带挂起)

如果考虑内存交换,还有:

就绪挂起(Ready Suspended):进程在外存中等待,等待被调入内存

阻塞挂起(Blocked Suspended):进程在外存中等待事件

触发条件:

  • 内存不足时,系统将低优先级进程换出到磁盘(挂起)
  • 内存充足或优先级提高,换回内存(激活)

注意

  1. 阻塞态不可能直接到运行态,必须先到就绪态
  2. 就绪态不可能直接到阻塞态,只有运行的进程才能请求IO或资源
  3. 运行态只有一个CPU就只有一个进程,多核CPU可以多个进程同时运行
  4. 状态转换是原子操作,由操作系统内核完成,不会被打断。

实际例子

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

上下文切换的具体开销来源

直接开销

保存和恢复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刷新

算法层面优化

  • 批处理
  • 工作窃取
  • 用户态调度器

操作系统配置

  • 调整调度策略
  • 禁用不必要的后台任务

实际案例

  1. Nginx(事件驱动):使用epoll + 少量worker进程,避免大量进程切换,单worker处理数千连接
  2. Redis(单线程):单线程 + IO多路复用,完全避免上下文切换,性能极高
  3. 数据库连接池:复用连接,减少进程/线程创建,降低切换频率

多线程与多进程的选择依据

维度多线程多进程
内存共享内存空间独立内存空间
通信直接访问共享变量需要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)

实现原理:操作系统向进程发送软件中断,进程收到信号后执行预定义的处理函数

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

死锁的四个必要条件

同时满足会发生死锁

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

预防死锁策略

破坏任意必要条件

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

避免死锁策略

银行家算法(Banker’s Algorithm)

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

实现步骤:

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

死锁检测与恢复

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

孤儿进程和僵尸进程

孤儿进程:父进程先于子进程结束,子进程变成孤儿进程。(几乎没有危害)

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

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

僵尸进程预防

  1. 父进程主动回收
  2. 信号处理(推荐)
  3. 两次fork(守护进程)

僵尸进程清理

  1. 杀死父进程(推荐)
  2. 重启系统

协程和线程的本质区别

调度方式

线程:抢占式调度

协程:协作式调度

切换开销

线程:切换比较重

协程:切换比较轻

并发模型

线程:真并行

协程:伪并发

内存占用

线程:每个线程由独立的栈比较大

协程:协程栈很小

维度协程(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
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值