MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言等待子进程的策略

2024-11-075.3k 阅读

一、进程等待的基本概念

在 Linux 环境下,当一个进程(父进程)创建了一个子进程后,父进程常常需要知道子进程的执行情况,例如子进程是否正常结束,结束时的返回状态等。进程等待机制就是为了解决这个问题,它允许父进程暂停执行,直到子进程发生特定的事件,比如终止、停止或继续运行等。

(一)为什么需要等待子进程

  1. 资源回收:子进程结束后,如果父进程不等待并回收子进程的资源,子进程就会变成僵尸进程(Zombie Process)。僵尸进程虽然已经停止运行,但它的进程描述符仍然保存在系统中,占用一定的系统资源,长期积累可能会导致系统资源耗尽。
  2. 获取返回状态:父进程可能需要了解子进程的执行结果,比如子进程执行某个任务是否成功,返回的错误码等。通过等待子进程,父进程可以获取这些信息,以便进行后续的处理。

(二)进程等待的相关系统调用

在 Linux C 语言编程中,主要使用 wait()waitpid() 这两个系统调用来实现进程等待。

  1. wait() 函数
    • 函数原型#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
    • 功能wait() 函数会暂停调用它的进程(通常是父进程),直到其任何一个子进程终止。当有子进程终止时,wait() 返回该终止子进程的进程 ID,并通过 status 参数传出子进程的终止状态。如果调用 wait() 时没有子进程,wait() 会阻塞直到有子进程终止。
    • 返回值:成功时返回终止子进程的进程 ID,出错时返回 -1。
  2. waitpid() 函数
    • 函数原型#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
    • 功能waitpid() 提供了更灵活的等待方式。它可以等待指定的子进程(通过 pid 参数指定),而不像 wait() 只能等待任意一个子进程。status 参数同样用于获取子进程的终止状态,options 参数可以设置一些额外的选项,如是否非阻塞等待等。
    • 返回值:成功时返回等待到的子进程的进程 ID。如果设置了 WNOHANG 选项且没有子进程状态改变,返回 0。出错时返回 -1。

二、使用 wait() 等待子进程

(一)简单示例

下面是一个使用 wait() 等待子进程的简单代码示例:

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

int main() {
    pid_t pid;
    int status;

    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("子进程: 我的进程 ID 是 %d\n", getpid());
        sleep(2);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        printf("父进程: 等待子进程 %d 结束\n", pid);
        wait(&status);
        if (WIFEXITED(status)) {
            printf("父进程: 子进程正常结束,返回值是 %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("父进程: 子进程被信号 %d 终止\n", WTERMSIG(status));
        }
    }
    return 0;
}

在这个示例中,父进程创建了一个子进程。子进程睡眠 2 秒后正常退出。父进程调用 wait(&status) 等待子进程结束,并通过 WIFEXITED(status)WEXITSTATUS(status) 宏来判断子进程是否正常结束以及获取其返回值。

(二)wait() 的特点及注意事项

  1. 等待任意子进程wait() 会等待任何一个子进程终止。如果父进程有多个子进程,wait() 会随机等待其中一个子进程结束。
  2. 阻塞特性:默认情况下,wait() 是阻塞的。也就是说,如果当前没有子进程终止,调用 wait() 的父进程会一直暂停执行,直到有子进程终止。
  3. 错误处理:如果 wait() 调用失败(返回 -1),可能是因为没有子进程,或者在等待过程中发生了信号中断等错误。在实际编程中,需要根据具体的错误码进行相应的处理。

三、使用 waitpid() 等待子进程

(一)等待指定子进程

waitpid() 的一个重要特性是可以等待指定的子进程。下面是一个示例:

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

int main() {
    pid_t pid1, pid2;
    int status1, status2;

    pid1 = fork();
    if (pid1 == -1) {
        perror("fork1");
        exit(EXIT_FAILURE);
    } else if (pid1 == 0) {
        // 第一个子进程
        printf("第一个子进程: 我的进程 ID 是 %d\n", getpid());
        sleep(1);
        exit(EXIT_SUCCESS);
    } else {
        pid2 = fork();
        if (pid2 == -1) {
            perror("fork2");
            exit(EXIT_FAILURE);
        } else if (pid2 == 0) {
            // 第二个子进程
            printf("第二个子进程: 我的进程 ID 是 %d\n", getpid());
            sleep(3);
            exit(EXIT_SUCCESS);
        } else {
            // 父进程
            printf("父进程: 等待第一个子进程 %d 结束\n", pid1);
            waitpid(pid1, &status1, 0);
            if (WIFEXITED(status1)) {
                printf("父进程: 第一个子进程正常结束,返回值是 %d\n", WEXITSTATUS(status1));
            }

            printf("父进程: 等待第二个子进程 %d 结束\n", pid2);
            waitpid(pid2, &status2, 0);
            if (WIFEXITED(status2)) {
                printf("父进程: 第二个子进程正常结束,返回值是 %d\n", WEXITSTATUS(status2));
            }
        }
    }
    return 0;
}

在这个例子中,父进程创建了两个子进程。父进程先使用 waitpid(pid1, &status1, 0) 等待第一个子进程结束,然后再等待第二个子进程结束。这样可以按照特定的顺序处理子进程的结束状态。

(二)非阻塞等待

waitpid() 还可以通过设置 options 参数为 WNOHANG 实现非阻塞等待。示例如下:

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

int main() {
    pid_t pid;
    int status;

    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("子进程: 我的进程 ID 是 %d\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        printf("父进程: 尝试非阻塞等待子进程 %d\n", pid);
        while (1) {
            pid_t result = waitpid(pid, &status, WNOHANG);
            if (result == -1) {
                perror("waitpid");
                break;
            } else if (result == 0) {
                printf("父进程: 子进程还未结束,继续执行其他任务...\n");
                sleep(1);
            } else {
                if (WIFEXITED(status)) {
                    printf("父进程: 子进程正常结束,返回值是 %d\n", WEXITSTATUS(status));
                } else if (WIFSIGNALED(status)) {
                    printf("父进程: 子进程被信号 %d 终止\n", WTERMSIG(status));
                }
                break;
            }
        }
    }
    return 0;
}

在上述代码中,父进程通过 waitpid(pid, &status, WNOHANG) 进行非阻塞等待。如果子进程还未结束,waitpid() 返回 0,父进程可以继续执行其他任务。当子进程结束时,waitpid() 返回子进程的进程 ID,父进程可以获取子进程的终止状态。

(三)waitpid() 的其他选项

除了 WNOHANG 外,waitpid() 还有其他一些选项:

  1. WUNTRACED:如果子进程因收到 SIGSTOPSIGTSTP 等信号而停止,waitpid() 也会返回。此时可以通过 WIFSTOPPED(status) 宏来判断子进程是否是因为停止而返回。
  2. WCONTINUED:如果子进程之前因停止而现在又继续运行,waitpid() 会返回。可通过 WIFCONTINUED(status) 宏来判断。

四、处理多个子进程的等待策略

(一)循环等待多个子进程

当父进程创建了多个子进程时,需要一种有效的方式来等待所有子进程结束。一种常见的方法是使用循环调用 wait()waitpid()。下面是使用 wait() 等待多个子进程的示例:

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

#define CHILD_PROCESSES 5

int main() {
    pid_t pids[CHILD_PROCESSES];
    int status;

    for (int i = 0; i < CHILD_PROCESSES; i++) {
        pids[i] = fork();
        if (pids[i] == -1) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pids[i] == 0) {
            // 子进程
            printf("子进程 %d: 我的进程 ID 是 %d\n", i, getpid());
            sleep(i + 1);
            exit(i);
        }
    }

    for (int i = 0; i < CHILD_PROCESSES; i++) {
        pid_t terminated_pid = wait(&status);
        if (WIFEXITED(status)) {
            printf("父进程: 子进程 %d 正常结束,返回值是 %d\n", terminated_pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("父进程: 子进程 %d 被信号 %d 终止\n", terminated_pid, WTERMSIG(status));
        }
    }
    return 0;
}

在这个示例中,父进程创建了 5 个子进程,每个子进程睡眠不同的时间后退出。父进程通过循环调用 wait(&status) 等待所有子进程结束,并获取每个子进程的终止状态。

(二)使用数组记录子进程状态

另一种处理多个子进程等待的策略是使用数组记录每个子进程的状态。结合 waitpid() 可以实现更灵活的处理。示例如下:

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

#define CHILD_PROCESSES 3

int main() {
    pid_t pids[CHILD_PROCESSES];
    int statuses[CHILD_PROCESSES];
    int finished_count = 0;

    for (int i = 0; i < CHILD_PROCESSES; i++) {
        pids[i] = fork();
        if (pids[i] == -1) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pids[i] == 0) {
            // 子进程
            printf("子进程 %d: 我的进程 ID 是 %d\n", i, getpid());
            sleep(i + 1);
            exit(i);
        }
    }

    while (finished_count < CHILD_PROCESSES) {
        for (int i = 0; i < CHILD_PROCESSES; i++) {
            if (pids[i] != 0) {
                pid_t result = waitpid(pids[i], &statuses[i], WNOHANG);
                if (result == pids[i]) {
                    if (WIFEXITED(statuses[i])) {
                        printf("父进程: 子进程 %d 正常结束,返回值是 %d\n", pids[i], WEXITSTATUS(statuses[i]));
                    } else if (WIFSIGNALED(statuses[i])) {
                        printf("父进程: 子进程 %d 被信号 %d 终止\n", pids[i], WTERMSIG(statuses[i]));
                    }
                    pids[i] = 0;
                    finished_count++;
                }
            }
        }
        sleep(1);
    }
    return 0;
}

在这个代码中,父进程使用数组 pids 记录子进程的进程 ID,statuses 数组记录子进程的终止状态。通过 waitpid(pids[i], &statuses[i], WNOHANG) 进行非阻塞等待,当某个子进程结束时,更新相应的数组元素并增加已完成子进程的计数。

五、处理僵尸进程

(一)僵尸进程的产生

当子进程终止但父进程没有调用 wait()waitpid() 来回收其资源时,子进程就会变成僵尸进程。僵尸进程在进程表中仍然保留一个表项,虽然它不再执行任何代码,但占用了一定的系统资源。

(二)避免僵尸进程的方法

  1. 及时等待子进程:父进程在创建子进程后,及时调用 wait()waitpid() 等待子进程结束,获取其终止状态并回收资源。这是最常见的避免僵尸进程的方法。
  2. 使用 SIGCHLD 信号:父进程可以注册 SIGCHLD 信号处理函数。当子进程状态改变(如终止、停止等)时,系统会向父进程发送 SIGCHLD 信号。在信号处理函数中,父进程可以调用 wait()waitpid() 来处理子进程的结束。示例如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

void sigchld_handler(int signum) {
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("父进程: 子进程 %d 正常结束,返回值是 %d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("父进程: 子进程 %d 被信号 %d 终止\n", pid, WTERMSIG(status));
        }
    }
}

int main() {
    pid_t pid;
    struct sigaction sa;

    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("子进程: 我的进程 ID 是 %d\n", getpid());
        sleep(2);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        printf("父进程: 继续执行其他任务...\n");
        while (1) {
            sleep(1);
        }
    }
    return 0;
}

在这个示例中,父进程注册了 SIGCHLD 信号处理函数 sigchld_handler。当子进程结束时,系统会调用该信号处理函数,在函数中使用 waitpid(-1, &status, WNOHANG) 以非阻塞方式回收所有已结束的子进程资源,从而避免了僵尸进程的产生。

  1. 让 init 进程收养子进程:如果父进程在创建子进程后立即终止,子进程会被 init 进程(进程 ID 为 1)收养。init 进程会在子进程结束时自动回收其资源,避免僵尸进程。例如:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    pid_t pid;

    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("子进程: 我的进程 ID 是 %d\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        printf("父进程: 立即终止\n");
        exit(EXIT_SUCCESS);
    }
    return 0;
}

在这个代码中,父进程创建子进程后立即退出。子进程会被 init 进程收养,当子进程结束时,init 进程会回收其资源。

六、特殊情况处理

(一)子进程在父进程之前结束

在一些情况下,子进程可能会比父进程先结束。例如,子进程执行的任务比较简单且耗时短,而父进程可能在进行一些复杂的操作。这时父进程需要及时等待子进程,避免产生僵尸进程。前面介绍的 wait()waitpid() 等方法都可以处理这种情况。

(二)父进程需要对子进程的不同状态做出不同反应

有时候父进程不仅需要知道子进程是否正常结束,还需要根据子进程的不同终止状态(如被信号终止、正常结束但返回特定错误码等)做出不同的反应。可以通过 WIFEXITED(status)WEXITSTATUS(status)WIFSIGNALED(status)WTERMSIG(status) 等宏来获取子进程的详细终止状态,并根据这些状态进行相应的处理。例如:

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

int main() {
    pid_t pid;
    int status;

    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("子进程: 我的进程 ID 是 %d\n", getpid());
        // 这里模拟子进程因除零错误被信号终止
        int a = 1 / 0;
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        wait(&status);
        if (WIFEXITED(status)) {
            printf("父进程: 子进程正常结束,返回值是 %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("父进程: 子进程被信号 %d 终止,可能是出现了异常\n", WTERMSIG(status));
        }
    }
    return 0;
}

在这个示例中,子进程故意进行除零操作导致被信号终止。父进程通过 WIFSIGNALED(status)WTERMSIG(status) 宏判断子进程是被信号终止,并可以根据信号值进一步分析异常原因。

(三)子进程因信号停止或继续运行

当子进程因收到 SIGSTOPSIGTSTP 等信号而停止,或者因收到 SIGCONT 信号而继续运行时,父进程可以通过 waitpid()WUNTRACEDWCONTINUED 选项来获取这些状态变化。例如:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

int main() {
    pid_t pid;
    int status;

    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        printf("子进程: 我的进程 ID 是 %d\n", getpid());
        sleep(10);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        printf("父进程: 等待子进程状态变化\n");
        pid_t result = waitpid(pid, &status, WUNTRACED | WCONTINUED);
        if (WIFSTOPPED(status)) {
            printf("父进程: 子进程被信号 %d 停止\n", WSTOPSIG(status));
        } else if (WIFCONTINUED(status)) {
            printf("父进程: 子进程继续运行\n");
        } else if (WIFEXITED(status)) {
            printf("父进程: 子进程正常结束,返回值是 %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("父进程: 子进程被信号 %d 终止\n", WTERMSIG(status));
        }
    }
    return 0;
}

在这个代码中,父进程使用 waitpid(pid, &status, WUNTRACED | WCONTINUED) 等待子进程的状态变化。如果子进程被停止,父进程可以通过 WIFSTOPPED(status)WSTOPSIG(status) 宏获取停止信号;如果子进程继续运行,父进程可以通过 WIFCONTINUED(status) 宏得知。

通过合理运用上述各种等待子进程的策略和方法,在 Linux C 语言编程中能够有效地管理子进程,避免资源浪费,并根据子进程的执行情况进行灵活处理。无论是简单的单子进程程序,还是复杂的多子进程并发程序,都可以通过这些技术确保程序的稳定性和高效性。