一、进程基础:定义、生命周期与状态模型
什么是进程?
进程是操作系统进行资源分配和调度的基本单位,是程序在某个数据集合上的一次运行活动。它由程序段、数据段、进程控制块(PCB)三部分组成。
简单理解:程序是静态的指令集合,进程是动态的执行过程。例如,你双击打开 Chrome 浏览器,系统会创建一个或多个进程来运行它。
进程的五大特性
- 动态性:进程是程序的一次执行过程,有创建、运行、撤销的生命期
- 并发性:多个进程可同存于内存并交替执行(宏观并行,微观串行)
- 独立性:进程是独立的调度单位和资源拥有者
- 异步性:进程按各自独立、不可预知的速度向前推进
- 结构特征:由程序、数据和PCB组成
进程 vs 程序对比
以 gcc hello.c 编译命令为例:
此时系统中存在一个名为 gcc 的进程,其 PCB 中记录了 PID=12345、内存映射、打开文件列表等信息。程序文件 gcc 是磁盘上的静态二进制文件,而进程是其在内存中的动态实例。
进程的生命周期与状态转换
创建(Create)→ 就绪(Ready)
操作系统调用 fork() 或 clone() 创建新进程时:
此时进程已具备运行条件,但尚未获得CPU,处于就绪队列中等待调度。
就绪(Ready)→ 运行(Running)
调度器(Scheduler)从就绪队列中选取进程,分配CPU时间片后进入运行状态。
面试陷阱:在单核CPU上,任意时刻仅有一个进程处于运行态;多核下可有多个进程并行运行。
运行(Running)→ 阻塞(Blocked)
进程主动调用 sleep()、wait() 或等待I/O(如读文件)时进入阻塞态。
当I/O完成(如磁盘数据就绪),操作系统通过中断机制唤醒该进程,重新进入就绪队列。
运行(Running)→ 就绪(Ready)
当时间片耗尽(Time Slice Expired),调度器剥夺CPU,进程回到就绪态继续排队。
时间片长度影响:过短 → 上下文切换频繁;过长 → 响应延迟。Linux CFS调度器采用“公平调度”,动态调整时间片。
⚠️ 常见误区
❌ “进程切换比线程切换快” —— 实际相反!进程切换需保存/恢复整个地址空间(包括页表),而线程共享地址空间,仅需切换寄存器和栈指针。
✅ 正确理解:线程是CPU调度的基本单位;进程是资源分配的基本单位。
二、进程与线程:区别、对比与典型应用场景
核心区别对比表
| 维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源拥有 | 独占内存空间、文件描述符、信号处理 | 共享所属进程的资源 |
| 调度单位 | 否(资源载体) | 是(CPU调度基本单位) |
| 切换开销 | 高(需切换页表、TLB等) | 低(仅保存寄存器) |
| 通信方式 | 管道、消息队列、共享内存等 | 直接读写共享变量(需同步机制) |
| 独立性 | 相互独立,一个崩溃不影响其他进程 | 共享地址空间,一个线程崩溃可能导致进程退出 |
浏览器多进程架构
现代浏览器(如Chrome)采用多进程模型:
- Browser进程:主进程,管理UI、网络、存储
- Renderer进程:每个标签页独立进程,渲染HTML/CSS/JS
- GPU进程:独立GPU调度
- Plugin进程:插件隔离
优势:隔离性好(一个标签页崩溃不影响其他页面)、安全(沙箱机制)、可利用多核。
创建1000个进程 vs 线程
实测结果(Ubuntu 22.04, 16GB RAM):
- 1000进程:内存占用约 2.8GB(含页表开销),创建耗时 ~1.2秒
- 1000线程:内存占用约 120MB(共享地址空间),创建耗时 ~0.08秒
网友关注:何时该用进程?何时该用线程?
三、进程通信(IPC):6大方式详解与性能对比
无名管道(Pipe)
适用于具有亲缘关系的进程(父子进程),半双工通信。
限制:仅限父子/兄弟进程;单向通信;内核缓冲区大小有限(Linux默认64KB)。
命名管道(FIFO)
通过文件系统路径名标识,可用于无亲缘关系进程通信。
优势:跨进程通信;劣势:仍为单向、无长度限制,但需手动管理同步。
消息队列(Message Queue)
内核维护的链表,支持多进程读写,可带类型标识。
特点:异步通信;支持多消费者;消息可带类型过滤;但需内核系统调用,开销较大。
共享内存(Shared Memory)
最快IPC方式,多个进程映射同一物理内存区域。
关键点:需配合信号量/互斥锁保证同步;否则出现竞态条件(Race Condition)。
信号量(Semaphore)
用于进程/线程同步,控制共享资源访问权限。
注意:信号量不传输数据,仅用于同步;常与共享内存配合使用。
套接字(Socket)
最通用IPC方式,支持网络通信(同一台主机可用Unix Domain Socket)。
优势:跨主机;支持TCP/UDP;劣势:开销最大(需协议栈处理)。
IPC方式性能与适用场景对比
| 方式 | 速度 | 适用范围 | 是否需同步 |
|---|---|---|---|
| 共享内存 | ★★★★★ | 本地高效通信 | 必须 |
| 消息队列 | ★★★☆ | 任意进程 | 可选 |
| 无名管道 | ★★★ | 父子进程 | 自动 |
| 套接字 | ★☆ | 任意(含跨机) | 视协议而定 |
四、进程调度算法:从FCFS到CFS的演进
经典调度算法详解
先来先服务(FCFS)
非抢占式,按请求顺序排队。简单但“护短”,短进程可能被长进程阻塞(平均等待时间长)。
短作业优先(SJF)
抢占式(SRTF)更优:剩余执行时间最短者优先。减少平均等待时间,但需预估运行时间,且可能导致长进程“饿死”。
面试案例:进程A(10ms)、B(2ms)、C(1ms)同时到达。SJF顺序:C→B→A,平均等待时间 = (0+1+3)/3 = 1.33ms;FCFS顺序:A→B→C,平均等待时间 = (0+10+12)/3 = 7.33ms。
时间片轮转(RR)
每个进程分配固定时间片(如100ms),时间片用完则轮转。保证公平性,适合分时系统。
关键参数:时间片大小影响性能——太小→切换开销大;太大→交互性差。
完全公平调度(CFS)
Linux 2.6.23+采用。基于“红黑树”管理就绪队列,按虚拟运行时间(vruntime)选择进程。核心思想:让所有进程获得公平的CPU时间。
优势:支持可配置权重(nice值);自动处理SMP负载均衡。
⚠️ 面试陷阱提醒
❌ “CFS是抢占式调度” —— 实际CFS本身非严格抢占,但配合内核抢占(CONFIG_PREEMPT)可实现。
✅ 正确理解:调度策略(Policy)决定选择哪个进程;调度类(Scheduler Class)决定策略实现。Linux有SCHED_FIFO、SCHED_RR、SCHED_DEADLINE、SCHED_NORMAL(CFS)等策略。
五、死锁:原理、检测与解决方案
死锁的四个必要条件
- 互斥:资源不能被共享(如打印机)
- 占有且等待:进程持有资源又申请新资源
- 不可剥夺:已分配资源不能被强制回收
- 循环等待:存在进程-资源的循环等待链
只要破坏任一条件,即可避免死锁。
哲学家进餐问题
5个哲学家围桌而坐,每人左右各有一根筷子。需同时拿到两根才能吃饭。若所有哲学家同时拿起左手筷子,则无法拿到右手筷子,陷入死锁。
4种主流策略
- 预防:破坏四个条件之一(如资源预分配)
- 避免:银行家算法(动态检查资源分配安全性)
- 检测与恢复:定期检测死锁,强制终止进程/回滚
- 忽略:Unix/Linux默认策略(因发生概率低)
Linux中的死锁处理
常用检测命令
调试技巧
- 使用
strace -p PID查看系统调用阻塞点 - 用
lsof -p PID检查打开的文件/网络连接 - Java应用:通过
jstack分析线程堆栈 - 内核锁(spinlock):启用
CONFIG_DEBUG_SPINLOCK编译内核
六、Linux进程管理:命令实战与原理剖析
进程查看与监控
关键字段:PID(进程ID)、PPID(父进程ID)、STAT(状态:R运行、S睡眠、D不可中断、Z僵尸)。
僵尸进程处理
子进程退出后,若父进程未调用 wait() 回收PCB,子进程变为僵尸(Z状态)。
根本解决:父进程应调用 waitpid() 或设置 signal(SIGCHLD, SIG_IGN)。
进程创建流程(fork)
- 分配新的task_struct和PID
- 复制父进程页表(COW写时复制)
- 设置子进程状态为TASK_RUNNING
- 加入调度队列等待执行
COW原理:fork后父子进程共享物理页,仅当写入时才复制(减少内存开销)。
程序员常忽略的细节
fork后全局变量状态
因COW机制,子进程修改counter时会复制新页,父进程值不变。
fork与多线程
fork() 在多线程程序中仅复制调用线程的上下文,其他线程消失!
解决方案:使用 pthread_atfork() 注册清理回调;或改用 vfork()(不安全,慎用)。