一、进程的基本概念

1.1 什么是进程?

进程 是程序的一次执行过程,它是系统进行资源分配(如 CPU 时间、内存、文件句柄等)的最小单位。程序是静态的、存放在磁盘上的可执行文件,而进程是动态的,拥有自己的生命周期。

1.2 进程标识符(PID)

每个进程在系统中都有一个唯一的整数编号,称为 PID(Process ID)。父进程的 PID 称为 PPID。系统中第一个进程是 init(或 systemd),PID 为 1,它是所有进程的祖先。

1.3 进程控制块(PCB)

操作系统内核为每个进程维护一个数据结构,称为 进程控制块(Process Control Block, PCB),在 Linux 中对应 task_struct 结构体。PCB 包含进程状态、PID、PPID、程序计数器、寄存器、文件描述符表、内存映射等信息,是内核管理进程的核心依据。


二、进程的状态

进程在其生命周期中会经历多种状态,理解这些状态对于调试和性能优化至关重要。

状态 说明
运行(Running) 进程正在 CPU 上执行,或处于就绪队列中等待被调度(实际上可分为 运行态就绪态,但在 Linux 的 ps 中统称为 R)。
可中断睡眠(Interruptible Sleep) 进程正在等待某个事件(如 I/O 完成),可以被信号唤醒。对应 S 状态。
不可中断睡眠(Uninterruptible Sleep) 进程正在等待硬件条件(如磁盘 I/O),不会被信号中断。对应 D 状态,常见于等待磁盘 I/O 时。
停止(Stopped) 进程被暂停,通常由 SIGSTOPSIGTSTP 信号引起。对应 T 状态。
僵尸(Zombie) 进程已经终止,但其 PCB 仍保留,等待父进程回收。对应 Z 状态。如果父进程不及时回收,会形成大量僵尸进程,消耗内核资源。

状态转换图:

新建 → 就绪 → 运行 → 终止
         ↑      ↓      ↓
         └── 阻塞 ──→ 退出等待

其中阻塞可细分为可中断/不可中断睡眠。


三、进程相关命令

命令 功能
ps 显示当前终端中的进程快照。常用参数:ps aux(显示所有进程)、ps -ef(标准格式)、ps -l(显示更详细信息)。
top / htop 动态查看进程资源占用(CPU、内存),支持交互操作。
pstree 以树形结构显示进程间的父子关系。
kill 发送信号给进程,默认 SIGTERM(终止),常用 kill -9(强制杀死)、kill -STOP(暂停)等。
jobs 查看当前 shell 后台作业。
bg / fg 将作业切换到后台/前台运行。
nice / renice 设置或修改进程的优先级(nice 值,范围 -20~19,值越小优先级越高)。
pidof 根据进程名查询 PID。
pgrep 按条件查找进程 PID。

示例

# 查看所有进程的 PID、PPID、状态、命令行
ps aux

# 动态查看 CPU 占用最高的进程
top -o %CPU

# 强制终止 PID 为 1234 的进程
kill -9 1234

# 以树形图显示进程关系
pstree -p

四、进程的创建

Linux 中创建进程的主要系统调用是 fork()vfork()clone()。我们最常用的是 fork()

4.1 fork() 函数

  • 原型pid_t fork(void);
  • 返回值:在父进程中返回子进程的 PID;在子进程中返回 0;出错返回 -1。
  • 行为fork() 会复制当前进程(父进程)的数据段、堆栈、代码段等,并采用 写时复制(COW, Copy-On-Write) 技术优化性能,即子进程和父进程共享物理内存,只有写操作发生时才会复制页面。
  • 父子进程执行顺序:不确定,由调度器决定。

4.2 vfork()clone()

  • vfork() 是早期优化版本,子进程会阻塞父进程直到子进程调用 execexit,且父子共享地址空间,现已不推荐使用。
  • clone() 是更底层的系统调用,用于创建线程(通过指定 CLONE_VM 等标志),也支持自定义创建行为。

4.3 使用 fork() 创建多个子进程的典型代码

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    int i;

    for (i = 0; i < 5; i++) {
        pid = fork();
        if (pid == -1) {
            perror("fork");
            return 1;
        }
        if (pid == 0) {   // 子进程
            printf("子进程 %d: PID=%d, PPID=%d\n", i+1, getpid(), getppid());
            return 0;     // 子进程立即退出,防止继续循环
        }
        // 父进程继续循环
    }

    // 父进程等待所有子进程结束
    for (i = 0; i < 5; i++) {
        wait(NULL);
    }
    printf("父进程 PID=%d,所有子进程已回收\n", getpid());
    return 0;
}

注意:子进程必须在 fork() 后立刻 returnexit,否则它会继续执行循环,导致更多子进程产生(形成进程树)。上述代码确保只创建 5 个直系子进程。


五、进程的调度

Linux 采用基于 完全公平调度算法(CFS, Completely Fair Scheduler) 的调度器,从内核 2.6.23 开始成为默认调度策略。

5.1 调度策略

  • SCHED_OTHER(普通进程):CFS 默认策略,根据进程的 nice 值虚拟运行时间 动态分配 CPU 时间片。
  • SCHED_FIFOSCHED_RR:实时调度策略,优先级高于普通进程。FIFO 为先进先出,RR 为时间片轮转。
  • 可通过 sched_setscheduler()chrt 命令设置调度策略。

5.2 优先级与 nice 值

  • 普通进程的优先级由 nice 值调节,范围 -20(最高)~ 19(最低),默认 0。
  • 设置 nice 值:nice -n 10 ./program(降低优先级);renice -5 -p 1234(修改已运行进程的 nice 值)。
  • 实时进程优先级范围 1~99,数值越大优先级越高。

5.3 调度触发

  • 时钟中断(例如 1000Hz)触发调度器检查是否需要抢占。
  • 进程主动让出 CPU(如调用 sleep()、阻塞 I/O)。
  • 进程用完时间片。

六、进程相关函数接口

函数 功能
pid_t getpid(void) 获取当前进程 PID。
pid_t getppid(void) 获取父进程 PID。
pid_t fork(void) 创建子进程。
pid_t vfork(void) 创建子进程并阻塞父进程(不推荐)。
unsigned int sleep(unsigned int seconds) 当前进程睡眠指定秒数。
int pause(void) 等待信号到来。
void exit(int status) 终止当前进程,返回状态值,刷新缓冲区。
int execvp(const char *file, char *const argv[]) 加载并执行新程序(替换当前进程映像)。
pid_t wait(int *status) 等待任一子进程终止,返回子进程 PID,并获取退出状态。
pid_t waitpid(pid_t pid, int *status, int options) 等待指定子进程,支持非阻塞等选项。

6.1 exec 系列函数

exec 族(如 execlexecvexecle 等)用于在当前进程上下文中执行一个新的程序,不会创建新进程,而是替换当前进程的代码段、数据段等。调用成功后,后续代码不再执行。典型用法:

if (fork() == 0) {
    // 子进程
    execlp("ls", "ls", "-l", NULL);
    perror("execlp"); // 只有出错才会执行到这里
    exit(1);
}

6.2 waitwaitpid

父进程通过 wait 回收子进程资源,获取退出状态,防止僵尸进程。

  • wait(&status):阻塞等待任意子进程结束。
  • waitpid(pid, &status, WNOHANG):非阻塞等待指定子进程。

七、进程的消亡

7.1 正常终止

进程正常结束的方式:

  • main 函数中执行 return
  • 调用 exit() 函数(标准库函数,会刷新缓冲区)。
  • 调用 _exit()_Exit()(系统调用,不刷新缓冲区)。

注意returnexit 都会执行 atexit 注册的清理函数,而 _exit 则不会。

7.2 异常终止

  • 收到不可捕捉的信号,如 SIGSEGV(段错误)、SIGABRT(abort 调用)。
  • 调用 abort() 函数。

7.3 僵尸进程(Zombie)

进程终止后,它的 PCB 并不会立即释放,而是保留退出状态和资源使用统计等信息,等待父进程调用 waitwaitpid 来读取。这期间进程处于 僵尸状态(Z状态)。如果父进程不回收,僵尸进程会一直存在,虽然不占用内存,但占用 PID 名额,大量僵尸会耗尽进程表项。

7.4 孤儿进程(Orphan)

如果父进程先于子进程结束,那么子进程成为孤儿进程,系统会将其收养,并挂载到 PID=1 的 init 进程下,由 init 负责回收。

7.5 避免僵尸进程的方法

  1. 父进程主动调用 wait()waitpid()
  2. 使用信号机制:父进程注册 SIGCHLD 信号处理函数,在信号处理函数中调用 waitpid 回收子进程。
  3. 显式忽略 SIGCHLD 信号signal(SIGCHLD, SIG_IGN)),这样内核会自动回收子进程资源,但该方法不是所有系统都支持。
  4. 使用双重 fork:即父进程先 fork 一个子进程,该子进程再 fork 实际工作进程后立即退出,这样实际工作进程的父进程变为 init,由 init 自动回收。

示例:使用 SIGCHLD 回收

#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

void sigchld_handler(int sig) {
    int status;
    pid_t pid;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        printf("回收子进程 %d\n", pid);
    }
}

int main() {
    signal(SIGCHLD, sigchld_handler);
    // ... 创建子进程
    while(1) pause(); // 让父进程不退出
    return 0;
}

八、总结

本文我们从进程的基本概念出发,详细介绍了进程的状态、常用管理命令、创建与调度、核心 API 以及进程的消亡过程。掌握这些知识,不仅有助于编写高质量的多进程程序,也能在系统出现异常时快速定位问题。

  • 进程是系统资源分配和调度的基本单位,拥有独立的地址空间和资源。
  • fork() + exec() 是创建新程序的经典组合,wait() 是回收子进程、避免僵尸的关键。
  • 调度器 CFS 保证了公平性,而实时调度则满足高优先级任务的需求。
  • 僵尸进程 是常见问题,务必在父进程中及时回收。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐