操作系统 --进程与线程篇
进程和线程区别
进程(Process)
- 操作系统资源分配的基本单位
- 每个进程有自己独立的地址空间(代码段、数据段、堆、栈)
- 程序一旦运行,就会至少有一个进程
- 切换开销大,因为需要切换整个内存地址空间、文件描述符等上下文
线程(Thread)
- 程序执行的最小单位,是 CPU 调度的基本单位
- 一个进程可以包含多个线程
- 线程共享进程的地址空间,但有自己独立的栈和寄存器
- 切换开销小,多个线程共享进程资源,只需切换少量上下文(寄存器、栈指针)
👉 直观类比:
- 进程 = 公司(独立运作,有资源)
- 线程 = 公司里的员工(共享公司资源,但干不同的活)
通信方面:
- 进程间通信(IPC):麻烦,需要操作系统提供机制(管道、消息队列、共享内存、socket)
- 线程间通信:方便,因为共享进程地址空间,直接读写共享变量即可,但需要同步机制(锁、条件变量)避免数据冲突
崩溃影响:
- 进程:一个进程崩溃不会影响其他进程(只要没有依赖关系)
- 线程:一个线程崩溃可能导致整个进程崩溃(因为共享地址空间)
| 对比点 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 地址空间 | 独立 | 共享(代码、堆),但有独立栈 |
| 切换开销 | 大 | 小 |
| 通信 | 需要 IPC | 直接共享内存,需同步 |
| 崩溃影响 | 不影响其他进程 | 可能拖垮整个进程 |
| 粒度 | 粗 | 细 |
| 并发能力 | 多进程并发 | 多线程并发,更轻量 |
什么是线程同步,线程同步有哪些方法?
1. 什么是线程同步?
- 线程同步 = 保证多个线程在访问共享资源时,按照一定的顺序执行,避免数据冲突。
- 背景:多线程可以同时访问同一块内存,如果不加限制就可能出现“竞态条件”(race condition)。
同步的目标:
保证数据一致性(不会读到脏数据)。
避免死锁和资源浪费。
提高并发效率。
👉 举例:
银行账户余额 = 1000 元。
- 线程 A:取 800 元。
- 线程 B:取 500 元。
如果没有同步机制,可能两个线程都判断余额够 → 都取成功 → 最终余额变负数 ❌。
同步机制可以保证操作的原子性:要么 A 成功 B 失败,要么 B 成功 A 失败。
2. 线程同步的方法
(1) 互斥锁(Mutex)
最常见的方法。
线程进入临界区前先加锁,用完资源后释放锁。
保证同一时间只有一个线程能访问共享资源。
缺点:可能导致阻塞,若使用不当会出现死锁。
C++
std::mutex,Javasynchronized/ReentrantLock。
(2) 读写锁(Read-Write Lock)
区分 读锁 和 写锁:
多个线程可以同时读(共享锁)。
写操作必须独占(互斥锁)。
适合读多写少的场景(如缓存、配置查询)。
Java
ReentrantReadWriteLock,C++std::shared_mutex。
(3) 信号量(Semaphore)
维护一个计数器,表示可用资源数量。
P操作(等待):请求一个资源,计数器减一;如果不足就阻塞。V操作(释放):释放资源,计数器加一。常用于限流,比如线程池控制最大并发数。
Java
Semaphore,Linuxsem_t。
(4) 条件变量(Condition Variable)
与互斥锁配合使用。
线程可以在条件变量上等待,直到某个条件为真再继续执行。
常用于生产者-消费者模型:
消费者在队列空时等待条件变量;
生产者放入数据后通知条件变量。
Java
wait()/notify(),C++std::condition_variable。
(5) 自旋锁(Spinlock)
当资源被占用时,线程不断循环检查(忙等),而不是挂起。
适合锁占用时间很短的场景(避免线程上下文切换开销)。
缺点:浪费 CPU 时间。
(6) 屏障(Barrier)
一种同步点。
多个线程必须都到达某个屏障位置,才能继续往下执行。
常用于并行计算的阶段性同步。
Java
CyclicBarrier,C++20std::barrier。
PCB —进程状态的标识
1 | ┌─────────────────┐ |
PCB作用:
- 进程切换
- 内核保存当前进程的 PCB(寄存器内容、状态)。
- 切换到另一个进程时,从该进程的 PCB 中恢复 CPU 状态。
- 进程管理
- 操作系统通过 PCB 维护所有进程的运行状态(创建、调度、挂起、终止)。
进程通信
- PCB 中保存通信所需的信息(管道、消息队列等)。
直观类比
- 进程 = 一个在跑的应用。
- PCB = 操作系统里的“档案袋”,里面记录了这个进程的身份证号(PID)、运行现场(寄存器)、调度优先级、用的资源等。
- 当操作系统切换进程时,就像“把档案袋收起来,换另一个档案袋”。
进程的调度算法 🚩
![[Pasted image 20251002113159.png]]
(1) 先来先服务(FCFS, First-Come-First-Served)
- 类型:非抢占式
- 规则:按照进程到达就绪队列的顺序分配 CPU。
- 优点:简单、公平
- 缺点:平均等待时间可能大,容易发生 短作业被长作业阻塞(Convoy Effect)
(2) 短作业优先(SJF, Shortest Job First)
- 类型:非抢占式 / 可抢占式(称 SRTF, Shortest Remaining Time First)
- 规则:优先执行预计运行时间最短的进程
- 优点:平均等待时间最小(理论上最优)
- 缺点:难以预测进程执行时间;可能导致长作业饥饿
(3) 时间片轮转(RR, Round Robin)
- 类型:抢占式
- 规则:每个进程按顺序分配固定时间片(quantum),时间片用完被剥夺 CPU → 排队等待下一轮
- 优点:响应时间好,适合 时间共享系统
缺点:时间片选择不合理会导致频繁切换(开销大)或响应慢
(4) 优先级调度(Priority Scheduling)
类型:可抢占或非抢占
- 规则:给每个进程分配一个优先级,CPU 优先给优先级高的进程
- 优点:重要任务优先处理
- 缺点:低优先级可能 饥饿(Starvation)
改进:老化(Aging):随着等待时间增加,提高低优先级进程优先级
(5) 多级队列调度(Multilevel Queue)
类型:抢占或非抢占
- 规则:将进程按类型(如交互型、批处理型)划分多个队列,每个队列有自己的调度策略
- 优点:可针对不同进程类型优化调度
缺点:队列划分固定,灵活性低
(6) 多级反馈队列调度(Multilevel Feedback Queue)
类型:抢占式
规则:多个队列,进程可根据执行情况在队列间移动
- 刚开始进程优先级高(时间片短)
- 占用 CPU 时间长 → 移到低优先级队列
优点:兼顾短作业响应快和长作业公平性
- 缺点:实现复杂
僵尸进程和孤儿进程
1. 僵尸进程(Zombie Process)
(1)定义
- 僵尸进程是已经 执行完毕(终止) 的进程,但其 PCB(进程控制块)仍然保留在内核中。
- 保留的原因是 父进程还没有读取子进程的退出状态(通过
wait()系列系统调用)。
(2)出现场景
父进程没有及时回收子进程的状态信息:
pid_t pid = fork(); if (pid == 0) { exit(0); // 子进程退出 } else { sleep(1000); // 父进程没有 wait }此时子进程已经结束,但 PCB 还在内核中 → 僵尸进程。
(3)特点和作用
特点:
- 不占用 CPU(已经退出)
- 占用少量内存(PCB)
- 仍然有 PID(可以
ps查看)
作用:
- 保存退出状态,让父进程能获取子进程退出码(正常/异常)。
(4)解决方法
父进程主动回收:
- 使用
wait()或waitpid()读取子进程退出状态。
- 使用
自动回收(避免僵尸):
- 父进程捕获
SIGCHLD信号,处理时调用wait()。 在 Linux 可以设置:
signal(SIGCHLD, SIG_IGN);系统会自动回收子进程 PCB。
- 父进程捕获
(5)排查
ps -el | grep Z→ 查看状态为Z的进程(僵尸)top→ STAT 列显示Z
2. 孤儿进程(Orphan Process)
(1)定义
- 孤儿进程是 父进程先于子进程结束 的进程。
- 系统会把孤儿进程的 父进程改为 init 进程(PID=1),由 init 负责回收。
(2)出现场景
pid_t pid = fork(); if (pid > 0) { exit(0); // 父进程先结束 } else { sleep(100); // 子进程还在运行 }
- 子进程就成为孤儿进程,被
init收养。
(3)特点和作用
特点:
- 父进程消失,子进程仍运行
- 由
init进程接管
作用:
- 保证操作系统可以最终回收子进程资源,不会出现永远占用内存的僵尸。
(4)解决方法
- 孤儿进程不需要手动处理,由 init 自动回收。
(5)排查
ps -ef→ PPID 列为 1 的进程通常是孤儿进程。
| 类型 | 定义 | 状态 | 占用 CPU | 如何回收 |
|---|---|---|---|---|
| 僵尸进程 | 已退出,但 PCB 未被回收 | Z |
否 | 父进程调用 wait(),或由 SIGCHLD 处理自动回收 |
| 孤儿进程 | 父进程先退出的活着子进程 | 正常运行 | 是 | 被 init 收养,运行完毕由 init 回收 |
- 僵尸过多 → PCB 占用过多 → 进程表满 → 新进程无法创建
- 孤儿进程本身无害,由 init 管理
