一、进程与程序:静态与动态的本质区别
初学者易混淆进程与程序,二者从存在形式、生命周期、资源占用等维度存在本质差异,核心是 “静态文件” 与 “动态执行实例” 的区别:
| 维度 | 程序(Program) | 进程(Process) |
|---|---|---|
| 存在形式 | 静态,存储在硬盘中的代码、数据集合 | 动态,程序加载到内存后执行的实例 |
| 生命周期 | 永存,除非手动删除文件 | 暂时,有创建、调度、运行、消亡的完整周期 |
| 状态变化 | 无状态,始终是静态文件 | 有就绪、运行、阻塞、终止等状态切换 |
| 并发特性 | 无并发概念,仅作为文件存在 | 支持并发执行,多进程可抢占 CPU 资源 |
| 资源占用 | 不占用系统资源(CPU、内存、文件描述符等) | 占用 CPU、内存、IO 等系统资源 |
| 运行关联 | 一个程序可多次运行,生成多个独立进程 | 一个进程可加载并执行一个或多个程序 |
直观示例:从代码到进程的转化
test.c(源代码文件) → 编译 → test.out(可执行程序/静态文件) → 运行 → process(进程,分配PID)- test.c 是程序的 “源码形态”,存储于硬盘;
- test.out 是编译后的可执行文件,仍为静态程序;
- 执行
./test.out后,系统为其分配内存、PID,才成为动态运行的进程。
虚拟内存与 MMU:进程隔离的核心保障
Linux 通过虚拟内存和内存管理单元(MMU)实现多进程安全运行:
- 隔离性:每个进程拥有独立虚拟地址空间,MMU 负责虚拟地址到物理地址的映射,进程间无法直接访问内存,避免篡改;
- 安全性:内核运行在核心态,进程运行在用户态,进程需通过系统调用(如
fork()、exit())并经权限校验后才能调用内核功能,防止恶意破坏。
二、进程的分类:按运行特性划分
根据运行方式和交互特性,Linux 进程分为三类,操作系统通过差异化调度策略适配其特性,实现系统并发:
1. 交互式进程
- 核心特征:运行依赖用户输入,交互性强,执行后返回输出;
- 典型示例:终端中的
vim、ssh、top,图形界面的浏览器、编辑器; - 调度特点:优先保证响应速度,分配更短时间片、更高优先级,避免用户操作卡顿。
2. 批处理进程
- 核心特征:无需用户实时交互,按预设逻辑批量执行;
- 典型示例:Shell 脚本、数据库批量备份程序、日志分析脚本;
- 调度特点:系统负载较低时执行(如夜间),优先级低于交互式进程,避免占用前台资源。
3. 守护进程(Daemon Process)
- 核心特征:系统启动后自动运行,长期驻留内存,休眠状态下等待触发;
- 典型示例:
nginx/apache(Web 服务)、rsyslogd(日志收集)、系统更新进程; - 调度特点:后台常驻,优先级稳定,PPID 通常为 1(由 init 进程接管)。
操作系统的进程状态切换图
linux的进程状态切换图
进程分类的核心价值:实现系统并发
进程分类的本质是优化系统并发能力 —— 操作系统在一段时间内同时运行多个任务的能力:
- 单 CPU 核心:通过调度器快速切换进程(时间片轮转),宏观上 “同时运行”,微观上同一时刻仅一个进程执行;
- 多 CPU 核心:升级为并行,多个进程可在不同核心真正同时执行,提升吞吐量。
Linux 针对不同进程的调度优化目标:
- 保证交互式进程响应速度;
- 提升批处理进程执行效率;
- 维持守护进程稳定常驻。
三、父子进程关系:fork () 创建与写时复制机制
Linux 中除 init 进程(PID=1,系统启动时创建)外,所有进程都有且仅有一个父进程,形成树形结构,核心创建方式是fork()系统调用。
3.1 fork () 函数的核心特性
fork()遵循 “一次调用,两次返回” 规则:
- 调用时,内核为新进程分配 PCB(进程控制块),复制父进程大部分资源;
- 父进程中,
fork()返回子进程 PID(正整数),用于管理子进程; - 子进程中,
fork()返回 0,可通过getppid()获取父进程 PID; - 调用失败(如进程数达上限)返回 -1,并设置
errno。
3.2 写时复制(Copy-On-Write):高效的内存复用策略
早期fork()会复制父进程全部内存空间(代码段、数据段、堆、栈),若子进程立即执行exec加载新程序,内存复制完全浪费。Linux 2.6 内核后引入写时复制(COW),核心原理:
fork()执行后,父子进程共享所有内存页,且标记为 “只读”;- 当任意一方修改内存页时,内核为该页创建副本,分配给修改方单独使用;
- 优势:降低
fork()开销,提升进程创建效率,节省内存。
3.3 fork () 代码示例:区分父子进程
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int global_var = 10; // 全局变量,存储在数据段 int main() { pid_t pid; int local_var = 20; // 局部变量,存储在栈 pid = fork(); if (pid == -1) { perror("fork failed"); return 1; } else if (pid > 0) { // 父进程执行逻辑 global_var++; local_var++; printf("父进程 - PID: %d, 子进程PID: %d\n", getpid(), pid); printf("父进程 - global_var: %d, local_var: %d\n", global_var, local_var); sleep(2); // 等待子进程执行完毕 } else { // 子进程执行逻辑 printf("子进程 - PID: %d, 父进程PID: %d\n", getpid(), getppid()); printf("子进程 - global_var: %d, local_var: %d\n", global_var, local_var); // 修改变量,触发写时复制 global_var += 2; local_var += 2; printf("子进程修改后 - global_var: %d, local_var: %d\n", global_var, local_var); } return 0; }运行结果分析:
- 子进程初始变量与父进程一致(共享内存页);
- 父进程修改变量不影响子进程,子进程修改时触发 COW,生成独立内存页副本。
四、进程的调度:CPU 资源的分配策略
多进程争夺有限的 CPU 核心资源时,Linux 内核调度器通过合理策略分配 CPU 时间,平衡公平性与响应性。
4.1 调度的核心逻辑:宏观并行与微观串行
- 宏观并行:通过进程快速切换,用户感知所有进程 “同时运行”;
- 微观串行:单个 CPU 核心同一时刻仅执行一个进程的指令。
4.2 进程上下文切换
当进程时间片耗尽,内核切换到其他进程运行的过程,核心步骤:
- 保存当前进程状态(PCB 标识、寄存器值、程序计数器、内存映射等);
- 将状态写入内存,释放 CPU;
- 读取待运行进程的状态,恢复到寄存器和 CPU;
- 按程序计数器继续执行该进程。
注意:上下文切换存在系统开销,过于频繁会降低整体性能。
4.3 Linux 主流调度算法
| 调度算法 | 核心逻辑 | 适用场景 | 特点 |
|---|---|---|---|
| 时间片轮转 | 就绪进程轮流占用 CPU,时间片耗尽触发切换 | 交互式进程 | 保证公平获取 CPU 资源 |
| 短任务优先 | 优先调度运行时间更短的进程 | 批处理进程 | 降低整体任务平均等待时间 |
| 优先级调度 | 高优先级进程优先获取 CPU | 响应敏感的实时任务 | 按优先级分配资源 |
| 完全公平调度器(CFS) | 按 “权重” 分配 CPU 时间,权重越高时间片越长 | 内核默认(通用场景) | 兼顾公平性与响应性 |
| 实时调度(SCHED_FIFO/SCHED_RR) | 先进先出 / 实时进程时间片轮转 | 工业控制、自动驾驶等实时任务 | 优先级高于普通进程,可抢占 CPU |
4.4 进程调度相关命令
| 命令 | 功能说明 | 常用示例 | |
|---|---|---|---|
ps aux | 显示所有进程详细信息(PID、状态、CPU 占用率等) | `ps aux | grep nginx`(过滤 nginx 进程) |
top | 实时监控进程资源占用,支持交互式调整优先级 | 按P按 CPU 排序,renice调整优先级 | |
kill | 向进程发送信号,终止 / 调整进程状态 | kill -9 1234(强制终止 PID=1234 的进程) | |
killall | 按进程名批量关闭进程 | killall -9 a.out(关闭所有 a.out 进程) |
总结
进程是 Linux 实现并发的基本单位:
- 与程序的核心区别是 “动态执行” 与 “静态文件”;
- 按运行特性分为交互式、批处理、守护进程,适配不同调度策略;
fork()是创建进程的核心调用,写时复制优化了内存复用;- 调度算法通过平衡公平性与响应性,实现 CPU 资源的高效分配,配套命令可快速管理进程。