一、进程到底是什么?
说实话,很多初学者学了几个月C语言,对「进程」这个概念还是模模糊糊的。我当年也一样,总觉得进程就是「正在运行的程序」——这话没错,但太笼统了。
咱们换个角度理解。你写了一个C程序,编译成可执行文件,放在磁盘上。这时候它就是个静态的文件,一堆二进制指令躺在那里。当你双击运行它,或者从终端敲下./a.out,操作系统会做几件事:
- 把程序代码从磁盘加载到内存
- 分配一块独立的内存空间(包括栈、堆、数据段)
- 创建一个数据结构来记录它的状态(比如PID、打开的文件、寄存器值)
- 把CPU的控制权交给它,开始执行指令
从这一刻起,它就不再是「程序」了,而是「进程」。
程序是静态的,进程是动态的。程序是菜谱,进程是照着菜谱炒菜的过程。同一个菜谱,可以同时被多个厨师使用——对应同一个程序可以启动多个进程。
二、进程的生命周期:从出生到消亡
每个进程都像一个人,有生老病死。我习惯把进程的生命周期分成四个阶段:
1. 创建(fork)
在Linux系统里,创建一个新进程的唯一方式就是调用fork()。嗯,这里要注意:fork()会复制当前进程,生成一个子进程。子进程几乎拥有父进程的一切——代码、数据、文件描述符,但有自己的PID。
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("我是子进程,PID = %d\n", getpid());
} else if (pid > 0) {
// 父进程
printf("我是父进程,子进程的PID = %d\n", pid);
} else {
// fork失败
perror("fork");
}
return 0;
}
我曾经在项目中遇到过一个问题:fork之后子进程莫名其妙地继承了父进程的锁状态,导致死锁。后来才意识到,fork复制的是整个内存快照,包括锁的状态。所以多线程程序里用fork要格外小心。
2. 运行
进程创建成功后,就进入就绪队列,等待CPU调度。一旦被调度器选中,就开始执行指令。这就是「运行态」。
3. 阻塞与唤醒
进程在执行过程中,可能会因为等待某个事件而暂停。比如:
- 等待用户输入(read系统调用)
- 等待磁盘I/O完成
- 等待网络数据到达
- 等待某个锁被释放
这时候进程进入「阻塞态」,不占用CPU。事件发生后,操作系统会把它重新放回就绪队列。
4. 终止
进程执行完main()函数,或者调用exit(),或者收到终止信号(比如SIGKILL),就会进入终止态。但注意,进程虽然终止了,它的退出状态码还保留着,直到父进程调用wait()或waitpid()来回收。这段时间的进程叫「僵尸进程」。
避坑指南:我曾经写过一个服务程序,父进程fork出子进程后没有调用wait,结果子进程终止后变成了僵尸进程。时间一长,系统进程表被占满,新的进程创建不了。排查了半天才发现是忘了回收子进程。
三、PID与PPID:进程的身份证
每个进程在创建时,内核会分配一个唯一的整数标识符——PID(Process ID)。PID从1开始递增,直到达到上限后回绕。PID 1是init进程(现在通常是systemd),它是所有用户进程的祖先。
PPID(Parent Process ID)就是父进程的PID。通过getpid()和getppid()可以获取:
#include <unistd.h>
#include <stdio.h>
int main() {
printf("我的PID: %d\n", getpid());
printf("我爸爸的PID: %d\n", getppid());
return 0;
}
你想想看,通过PID和PPID,整个系统的进程关系就构成了一棵树。用pstree命令可以直观地看到这棵树:
$ pstree
systemd─┬─NetworkManager───2*[{NetworkManager}]
├─cron
├─sshd───sshd───bash───pstree
└─systemd-journal
四、进程的三种状态
教科书上通常讲五种状态,但咱们先掌握最核心的三种:
| 状态 | 含义 | CPU占用 | 典型场景 |
|---|---|---|---|
| 运行(Running) | 正在CPU上执行指令 | 是 | 执行计算密集型任务 |
| 就绪(Ready) | 万事俱备,只等CPU | 否 | 等待被调度器选中 |
| 阻塞(Blocked) | 等待某个事件发生 | 否 | 等待磁盘I/O、等待网络数据 |
状态转换的规则很简单:
- 就绪 → 运行:被调度器选中
- 运行 → 就绪:时间片用完,或被更高优先级进程抢占
- 运行 → 阻塞:发起I/O操作或等待资源
- 阻塞 → 就绪:等待的事件完成
个人经验:调试性能问题时,我经常用top命令看进程的状态。如果大量进程处于D状态(不可中断睡眠,一种特殊的阻塞态),那多半是I/O瓶颈。如果大量进程处于R状态但CPU空闲,那可能是锁竞争导致的。
五、用ps和top看进程
纸上谈兵没意思,咱们直接上手。
ps命令:进程快照
ps显示的是执行瞬间的进程状态。我常用的几个组合:
# 查看所有进程,显示完整信息
ps aux
# 查看进程树
ps -ef --forest
# 查看某个特定进程
ps -p 1234 -o pid,ppid,state,cmd
ps aux的输出中,STAT那一列就是进程状态:
- R:运行或就绪
- S:可中断睡眠(阻塞)
- D:不可中断睡眠(通常是磁盘I/O)
- Z:僵尸进程
- T:停止(被信号暂停)
top命令:动态监控
top会实时刷新进程信息,像任务管理器一样。我个人习惯先按P按CPU排序,按M按内存排序,快速定位资源消耗大户。
$ top
top - 14:23:45 up 10 days, 2:15, 3 users, load average: 0.08, 0.03, 0.01
Tasks: 123 total, 1 running, 122 sleeping, 0 stopped, 0 zombie
%Cpu(s): 2.3 us, 0.7 sy, 0.0 ni, 96.7 id, 0.3 wa, 0.0 hi, 0.0 si
MiB Mem : 7956.3 total, 2345.6 free, 3120.4 used, 2490.3 buff/cache
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1234 root 20 0 162456 7896 5678 S 0.3 0.1 0:02.34 sshd
5678 myapp 20 0 1.2g 456m 23456 R 12.5 5.7 12:34.56 my_server
重点关注几个字段:
- S:进程状态
- %CPU:CPU使用率
- %MEM:内存使用率
- RES:实际物理内存占用
- TIME+:累计CPU时间
注意:top显示的CPU使用率是相对于单个核心的。如果你有4核CPU,一个进程用满一个核,显示的是100%,而不是25%。
六、知识体系总览
下面这张图把本章的核心内容串起来了:
七、写在最后
进程是操作系统的核心抽象之一。说白了,你写的每一个程序,最终都会变成一个或多个进程在系统里跑。理解进程的创建、状态转换、以及如何用工具观察它们,是系统编程的基本功。
我个人建议你多动手:写几个fork的小程序,用ps和top观察进程状态的变化。遇到僵尸进程也别慌,那是学习过程中最好的老师。