在 Linux 系统中,进程是程序的一次执行过程,也是操作系统进行资源分配的基本单位。掌握进程控制,意味着你能够理解并驾驭程序的创建、执行、终止以及资源回收的整个生命周期。本文将深入浅出地介绍 Linux 下的进程控制核心概念和系统调用,并通过实例帮助你彻底掌握它们。


一、进程的创建 —— fork

创建一个新进程最常用的方法是调用 fork。这个系统调用的特点是:调用一次,返回两次

c

#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        printf("我是子进程,PID = %d,父进程 PID = %d\n", getpid(), getppid());
    } else {
        printf("我是父进程,PID = %d,子进程 PID = %d\n", getpid(), pid);
    }
    return 0;
}
  • 在父进程中,fork 返回新创建的子进程的 PID。

  • 在子进程中,fork 返回 0。

  • 如果出错,返回 -1。

fork 之后,子进程会获得父进程的数据空间、堆、栈的副本,父子进程并发执行,执行顺序由调度器决定。你可能会有疑问:那子进程不是有可能修改父进程的数据了吗?其实,操作系统在这里采用了写时拷贝的技术:当你修改某个数据时,操作系统将为你单独开一块空间来保存修改的结果。


二、进程的执行 —— exec 族函数

fork 创建的子进程通常用来执行另一个程序,这时就需要 exec 系列函数。它们会用新的程序替换当前进程的代码段、数据段、堆和栈,不会创建新进程,只是修改进程映像。

c

#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

常用的是 execlp 和 execvp,它们会在 PATH 环境变量中搜索可执行文件。一个典型的 fork + exec 组合:

c

pid_t pid = fork();
if (pid == 0) {
    // 子进程执行新程序
    execlp("ls", "ls", "-l", NULL);
    perror("execlp error"); // 只有 exec 失败才会执行这里
    exit(1);
} else if (pid > 0) {
    wait(NULL); // 等待子进程结束
}

注意:exec 成功时不返回,失败时返回 -1。


三、进程的终止 —— exit 和 _exit

进程终止有两种方式:

  • 正常终止:从 main 返回、调用 exit、调用 _exit

  • 异常终止:收到信号(如 SIGKILL)。

exit 是标准 C 库函数,它会执行清理操作(如刷新缓冲区、调用 atexit 注册的函数),最终调用内核的 _exit。而 _exit 是系统调用,直接进入内核终止进程。

c

#include <stdlib.h> // exit
#include <unistd.h> // _exit

void cleanup(void) {
    printf("清理函数被调用\n");
}

int main() {
    atexit(cleanup); // 注册终止处理函数
    printf("程序开始\n");
    exit(0); // 会调用 cleanup,并刷新缓冲区
    // _exit(0); // 不会调用 cleanup,也不会刷新缓冲区
}

四、进程的等待 —— wait 和 waitpid

父进程通常需要等待子进程结束,以获取子进程的退出状态,并回收子进程占用的资源,防止子进程成为僵尸进程

wait

c

#include <sys/wait.h>

pid_t wait(int *status);
  • 阻塞直到有一个子进程结束。

  • 参数 status 用于获取子进程的终止状态(可通过 WIFEXITEDWEXITSTATUS 等宏解析)。

  • 返回结束的子进程 PID,出错返回 -1。

waitpid

c

pid_t waitpid(pid_t pid, int *status, int options);
  • pid:指定等待的子进程 PID(-1 表示等待任意子进程,>0 表示特定子进程)。

  • options:常用 WNOHANG 表示非阻塞(如果没有子进程结束,立即返回 0)。

  • 功能更强大,可以指定等待特定子进程或非阻塞等待。

示例:

c

int status;
pid_t ret = waitpid(pid, &status, 0); // 阻塞等待指定子进程
if (WIFEXITED(status)) {
    printf("子进程正常退出,状态码 = %d\n", WEXITSTATUS(status));
}

五、进程的状态与僵尸进程

进程状态(用 ps 查看)

  • R (running):正在运行或可运行。

  • S (sleeping):可中断睡眠,等待某个事件。

  • D (uninterruptible sleep):不可中断睡眠,通常等待 I/O。

  • Z (zombie):僵尸状态,进程已终止但父进程尚未回收其资源。

  • T (stopped):停止,通常由信号控制。

僵尸进程

当子进程先于父进程退出,父进程没有调用 wait 时,子进程的进程描述符仍保留在内核中,成为僵尸进程。僵尸进程不占用内存和 CPU,但占用 PID 资源,大量僵尸会妨碍新进程创建。

避免僵尸的方法

  1. 父进程调用 wait 或 waitpid

  2. 采用信号处理,在 SIGCHLD 信号处理函数中调用 wait

  3. 让父进程忽略 SIGCHLD 信号(某些系统默认行为不同,需谨慎)。

孤儿进程

父进程先于子进程退出,子进程成为孤儿进程,被 init 进程(PID=1)收养,由 init 负责回收。


六、实战演练:一个简单的进程池框架

结合上述知识,我们可以实现一个简单的进程池框架,父进程创建多个子进程,并通过管道分配任务。这个框架可以用于理解多进程协作。

由于篇幅所限,这里仅给出核心思路:

  1. 父进程创建 N 个子进程。

  2. 每个子进程进入循环,等待父进程通过管道发送任务码。

  3. 父进程维护一个任务队列,并通过某种负载均衡(如轮询)选择子进程发送任务。


总结

Linux 进程控制是系统编程的基础,掌握 forkexecexit 和 wait 的配合使用,能够让我们灵活地创建和管理多进程应用。理解进程状态变迁以及僵尸进程的成因,可以帮助我们写出更健壮的程序。希望本文能帮助你构建起清晰的进程控制知识体系。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐