一、进程到底是什么?

说实话,很多初学者学了几个月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) 运行 阻塞/唤醒 终止 (exit) PID / PPID getpid() 获取自己 getppid() 获取父进程 进程三种状态 运行 (R) → 就绪 (R) 阻塞 (S/D) → 就绪 (R) 监控命令 ps aux 静态快照 top 动态监控 核心要点 程序是静态的 → fork创建进程 → 三种状态转换 → 用ps/top观察

七、写在最后

进程是操作系统的核心抽象之一。说白了,你写的每一个程序,最终都会变成一个或多个进程在系统里跑。理解进程的创建、状态转换、以及如何用工具观察它们,是系统编程的基本功。

我个人建议你多动手:写几个fork的小程序,用ps和top观察进程状态的变化。遇到僵尸进程也别慌,那是学习过程中最好的老师。