进程与线程
进程
进程是程序的一次执行过程,是操作系统进行资源分配的基本单位。
进程控制块(PCB)
每个进程在内核中对应一个 task_struct(Linux),包含:
- 进程标识符:PID
- 状态信息:就绪、运行、阻塞、终止、僵尸
- 调度信息:优先级、调度策略
- 内存信息:代码段/数据段/堆栈指针、页表
- 文件信息:打开的文件描述符表
- 上下文数据:寄存器值(保存/恢复用)
进程状态与转换
创建 → 就绪 ←→ 运行 → 终止
↓ ↑ ↓
阻塞 阻塞挂起/就绪挂起- 就绪态:已具备运行条件,等待 CPU 分配
- 运行态:正在 CPU 上执行
- 阻塞态:等待某事件(IO 完成、锁释放)
- 僵尸态:已终止但 PCB 未被父进程回收
- 孤儿态:父进程先于子进程终止,由 init 进程收养
上下文切换
CPU 从执行一个进程切换到另一个进程时,需要:
- 保存当前进程的寄存器状态(PC、SP、通用寄存器等)到 PCB
- 加载新进程的 PCB 状态到寄存器
- 刷新 TLB 和 CPU 缓存(部分)
- 切换到新进程的页表
切换代价:纯开销,约 1-10μs,切换频率过高会降低系统吞吐量。
进程调度算法
| 算法 | 描述 | 特点 |
|---|---|---|
| FCFS | 先来先服务 | 公平但平均等待时间长,不利于短作业 |
| SJF | 最短作业优先 | 平均等待时间最小,但难以预知执行时间 |
| SRTF | 最短剩余时间优先 | SJF 抢占版,更公平但切换频繁 |
| RR | 时间片轮转 | 响应快,时间片大小关键 |
| 优先级调度 | 高优先级先执行 | 可能导致低优先级饿死 |
| 多级反馈队列 | 多队列 + 不同时间片 | Linux CFS 类似方案 |
进程间通信(IPC)
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 管道(Pipe) | 半双工,父子进程 | 简单数据流 |
| 命名管道(FIFO) | 文件系统路径,任意进程 | 无亲缘进程通信 |
| 消息队列 | 内核维护的消息链表 | 结构化消息 |
| 共享内存 | 最快的 IPC,需同步 | 大量数据交换 |
| 信号量 | 计数器 + 原子操作 | 同步与互斥 |
| 信号(Signal) | 异步通知 | 事件通知、中断处理 |
| Socket | 跨网络通信 | 分布式系统 |
| 内存映射文件 | mmap 共享映射 | 大文件共享 |
线程
线程是 CPU 调度的基本单位,隶属于进程。同一进程的线程共享地址空间(代码段、数据段、堆),但拥有独立的栈和寄存器上下文。
线程 vs 进程
| 对比 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 独立地址空间、文件描述符 | 共享进程资源 |
| 切换开销 | 大(需切换页表、刷新 TLB) | 小(仅保存/恢复寄存器) |
| 通信方式 | IPC(管道/消息/共享内存) | 直接读写共享内存 |
| 健壮性 | 进程间隔离,一个崩溃不影响其他 | 一个线程崩溃可能拖垮整个进程 |
| 创建开销 | 重(复制 PCB、分配资源) | 轻(仅分配栈和 TCB) |
线程模型
- 1:1 模型(Linux NPTL):一个用户线程对应一个内核线程,简单高效,但线程数受限于内核资源
- N:1 模型:多个用户线程映射到一个内核线程,切换快但无法利用多核
- N:M 模型:多对多,灵活但实现复杂,Go 的 GMP 模型近似此类
协程(Coroutine)
协程是用户态的"轻量级线程",由程序自身调度而非内核:
- 协作式调度:主动让出(yield),而非抢占
- 栈极小:初始几 KB,可动态增长
- 切换极快:纯用户态操作,约几十纳秒
- 典型实现:Go goroutine、Python async/await、Kotlin coroutine、Lua coroutine
Linux 线程实现
Linux 下 fork() 创建进程,clone() 创建线程:
c
clone(CLONE_VM | CLONE_FILES | CLONE_SIGHAND, ...) // 创建线程(轻量级进程)
fork() // 创建进程内核视角不存在"线程"概念,所有可执行实体都是 task_struct,只是共享资源的多寡不同。
常见面试问题
- 进程和线程的区别是什么?分别适用于什么场景?
- 上下文切换为什么昂贵?如何优化?
- Linux 的
fork()底层做了什么?写时复制(COW)如何工作? - select/poll/epoll 的区别?
- 协程和线程的根本区别?为什么协程切换更快?
- 进程间通信中,共享内存为什么最快?使用时需要注意什么?