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

Linux C语言等待子进程技术解析

2023-12-195.4k 阅读

一、进程相关基础概念回顾

在深入探讨等待子进程技术之前,我们先来回顾一些进程相关的基础概念。在Linux系统中,进程是程序的一次执行实例,是系统进行资源分配和调度的基本单位。每个进程都有一个唯一的进程标识符(PID),通过getpid()函数可以获取当前进程的PID。

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

int main() {
    printf("当前进程的PID: %d\n", getpid());
    return 0;
}

当一个进程创建另一个新进程时,我们称原进程为父进程,新创建的进程为子进程。子进程会复制父进程的地址空间、代码段、数据段等大部分资源,但拥有自己独立的PID。创建子进程使用fork()函数,fork()函数的返回值非常特殊,在父进程中返回子进程的PID,而在子进程中返回0。

#include <stdio.h>
#include <unistd.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", pid, getpid());
    }
    return 0;
}

二、为什么需要等待子进程

  1. 资源回收 子进程在运行结束后,如果父进程不进行处理,其残留的一些资源(如进程控制块等)不会自动完全释放,这会导致系统资源的浪费,甚至可能引发内存泄漏等问题。通过等待子进程,父进程可以确保子进程占用的资源被正确回收。
  2. 同步执行 在很多实际应用场景中,父进程需要知道子进程的执行结果,然后根据这个结果来决定后续的操作。例如,在一个文件处理程序中,父进程可能创建子进程来进行文件压缩,只有当子进程成功完成压缩后,父进程才能继续进行文件传输等后续操作。如果父进程不等待子进程,就可能在子进程还未完成任务时就执行了错误的后续步骤。

三、等待子进程的方法

  1. wait()函数
    • 函数原型#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
    • 功能wait()函数会使父进程阻塞,直到它的任意一个子进程结束或者收到一个不能被忽略的信号。当有子进程结束时,wait()函数返回该结束子进程的PID,并通过status参数获取子进程的退出状态等信息。
    • 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程开始执行,PID: %d\n", getpid());
        sleep(2);
        printf("子进程结束执行,PID: %d\n", getpid());
        exit(3);
    } else {
        // 父进程
        int status;
        pid_t wpid = wait(&status);
        if (wpid == -1) {
            perror("wait error");
            return 1;
        }
        if (WIFEXITED(status)) {
            printf("子进程正常结束,退出状态码: %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("子进程被信号终止,信号编号: %d\n", WTERMSIG(status));
        }
    }
    return 0;
}

在上述代码中,子进程睡眠2秒后正常退出,返回状态码3。父进程通过wait()函数等待子进程结束,并获取其退出状态。WIFEXITED(status)宏用于判断子进程是否正常结束,WEXITSTATUS(status)宏用于获取子进程正常结束时的退出状态码。

  1. waitpid()函数
    • 函数原型#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
    • 功能waitpid()函数可以等待指定的子进程结束,相比wait()函数更加灵活。pid参数指定要等待的子进程的PID,如果pid为 -1,则和wait()函数一样等待任意一个子进程;status参数同wait()函数,用于获取子进程的退出状态;options参数可以设置一些选项,例如WNOHANG表示如果没有子进程结束则立即返回,不阻塞父进程。
    • 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程开始执行,PID: %d\n", getpid());
        sleep(5);
        printf("子进程结束执行,PID: %d\n", getpid());
        exit(5);
    } else {
        // 父进程
        int status;
        pid_t wpid;
        do {
            wpid = waitpid(pid, &status, WNOHANG);
            if (wpid == 0) {
                printf("子进程还未结束,父进程继续做其他事...\n");
                sleep(1);
            }
        } while (wpid == 0);
        if (wpid == -1) {
            perror("waitpid error");
            return 1;
        }
        if (WIFEXITED(status)) {
            printf("子进程正常结束,退出状态码: %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("子进程被信号终止,信号编号: %d\n", WTERMSIG(status));
        }
    }
    return 0;
}

在这个例子中,父进程使用waitpid()函数并设置WNOHANG选项,以非阻塞的方式等待子进程结束。父进程在子进程未结束时可以继续执行其他任务,每隔1秒检查一次子进程是否结束。

  1. wait3()和wait4()函数
    • 函数原型
      • #include <sys/types.h> #include <sys/wait.h> #include <sys/time.h> #include <sys/resource.h> pid_t wait3(int *status, int options, struct rusage *rusage);
      • #include <sys/types.h> #include <sys/wait.h> #include <sys/time.h> #include <sys/resource.h> pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
    • 功能wait3()wait4()函数除了可以获取子进程的退出状态外,还可以通过rusage参数获取子进程的资源使用情况,如CPU时间、内存使用等。wait3()类似wait(),等待任意一个子进程;wait4()类似waitpid(),可以指定等待的子进程。
    • 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程开始执行,PID: %d\n", getpid());
        for (int i = 0; i < 100000000; i++);
        printf("子进程结束执行,PID: %d\n", getpid());
        exit(7);
    } else {
        // 父进程
        int status;
        struct rusage ru;
        pid_t wpid = wait4(pid, &status, 0, &ru);
        if (wpid == -1) {
            perror("wait4 error");
            return 1;
        }
        if (WIFEXITED(status)) {
            printf("子进程正常结束,退出状态码: %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("子进程被信号终止,信号编号: %d\n", WTERMSIG(status));
        }
        printf("子进程用户态CPU时间: %ld.%06ld 秒\n", ru.ru_utime.tv_sec, ru.ru_utime.tv_usec);
        printf("子进程内核态CPU时间: %ld.%06ld 秒\n", ru.ru_stime.tv_sec, ru.ru_stime.tv_usec);
    }
    return 0;
}

在这段代码中,子进程通过一个简单的循环消耗一些CPU时间。父进程使用wait4()函数等待子进程结束,并获取子进程的资源使用情况,通过rusage结构体中的ru_utimeru_stime分别获取子进程在用户态和内核态的CPU时间。

四、等待子进程中的错误处理

  1. wait()和waitpid()的错误返回
    • 返回 -1:当wait()waitpid()函数返回 -1 时,表明发生了错误。常见的错误原因有:没有子进程(如果在调用这些函数时已经没有子进程,会返回该错误)、调用进程没有权限等待指定的子进程等。在代码中,我们需要通过perror()函数打印错误信息,以便定位问题。
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    // 这里假设之前没有创建子进程
    int status;
    pid_t wpid = wait(&status);
    if (wpid == -1) {
        perror("wait error");
        return 1;
    }
    return 0;
}
  1. 处理子进程异常终止 当子进程因为收到信号而异常终止时,父进程可以通过status参数获取相关信息。例如,使用WIFSIGNALED(status)宏判断子进程是否被信号终止,WTERMSIG(status)宏获取终止子进程的信号编号。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

void child_handler(int signum) {
    printf("子进程收到信号: %d\n", signum);
    exit(1);
}

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        // 子进程
        signal(SIGTERM, child_handler);
        printf("子进程开始执行,PID: %d\n", getpid());
        while (1);
    } else {
        // 父进程
        int status;
        pid_t wpid = wait(&status);
        if (wpid == -1) {
            perror("wait error");
            return 1;
        }
        if (WIFSIGNALED(status)) {
            printf("子进程被信号终止,信号编号: %d\n", WTERMSIG(status));
        }
    }
    return 0;
}

在上述代码中,子进程捕获SIGTERM信号并在收到信号时打印信息并退出。父进程等待子进程结束,并在子进程被信号终止时打印出终止信号的编号。

五、特殊情况处理

  1. 孤儿进程 当父进程先于子进程结束时,子进程就会成为孤儿进程。Linux系统会将孤儿进程的父进程设置为init进程(PID为1),init进程会负责回收孤儿进程的资源。从等待子进程的角度来看,虽然原父进程已经不存在,但init进程会履行类似等待子进程的职责。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.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());
        sleep(5);
        printf("子进程结束,PID: %d\n", getpid());
    } else {
        // 父进程
        printf("我是父进程,PID: %d,子进程PID: %d\n", getpid(), pid);
        sleep(1);
        printf("父进程结束,PID: %d\n", getpid());
    }
    return 0;
}

在这个例子中,父进程睡眠1秒后结束,而子进程睡眠5秒。在父进程结束后,子进程成为孤儿进程,其PPID变为1(init进程的PID)。

  1. 僵尸进程 如果父进程创建了子进程,但没有等待子进程结束,也没有正确处理子进程的退出,子进程在结束后就会变成僵尸进程。僵尸进程虽然已经不再执行,但它的进程控制块仍然存在于系统中,占用一定的系统资源。为了避免僵尸进程的产生,父进程必须及时等待子进程并处理其退出状态。
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程开始执行,PID: %d\n", getpid());
        sleep(2);
        printf("子进程结束执行,PID: %d\n", getpid());
    } else {
        // 父进程
        printf("父进程继续执行,不等待子进程,PID: %d\n", getpid());
        while (1);
    }
    return 0;
}

在上述代码中,父进程创建子进程后不等待子进程结束,而是进入一个无限循环。子进程结束后就会变成僵尸进程,可以通过ps -ef命令查看僵尸进程(状态为Z)。要解决僵尸进程问题,父进程需要使用合适的等待子进程的函数,如wait()waitpid()

六、实际应用场景中的等待子进程技术

  1. 服务器程序 在服务器程序中,常常会为每个客户端连接创建一个子进程来处理客户端请求。父进程需要等待这些子进程结束,以确保资源的正确回收。例如,一个简单的HTTP服务器,父进程监听端口,每当有新的客户端连接时,创建一个子进程来处理该客户端的HTTP请求。
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>

#define PORT 8080
#define BUFFER_SIZE 1024

void handle_client(int client_socket) {
    char buffer[BUFFER_SIZE] = {0};
    int valread = read(client_socket, buffer, BUFFER_SIZE);
    if (valread < 0) {
        perror("read error");
        close(client_socket);
        return;
    }
    printf("收到客户端数据: %s\n", buffer);
    char response[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, World!";
    send(client_socket, response, strlen(response), 0);
    close(client_socket);
}

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听套接字
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    while (1) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
            perror("accept");
            continue;
        }
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork error");
            close(new_socket);
        } else if (pid == 0) {
            // 子进程处理客户端请求
            close(server_fd);
            handle_client(new_socket);
            exit(0);
        } else {
            // 父进程继续监听
            close(new_socket);
            int status;
            waitpid(pid, &status, WNOHANG);
        }
    }
    return 0;
}

在这个HTTP服务器示例中,父进程监听端口并接受客户端连接。每当有新连接时,创建子进程处理客户端请求,父进程通过waitpid()函数以非阻塞方式等待子进程结束,确保资源回收。

  1. 批处理任务 在批处理任务场景中,父进程可能会创建多个子进程并行执行不同的任务,然后等待所有子进程完成后再进行下一步操作。例如,一个文件处理程序,需要对多个文件进行压缩、加密等操作,可以创建多个子进程分别处理不同文件,父进程等待所有子进程完成后输出处理结果。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>

#define FILE_COUNT 3

void process_file(const char *file_name) {
    printf("子进程开始处理文件: %s\n", file_name);
    // 这里模拟文件处理操作,例如睡眠2秒
    sleep(2);
    printf("子进程完成处理文件: %s\n", file_name);
}

int main() {
    pid_t pids[FILE_COUNT];
    const char *file_names[FILE_COUNT] = {"file1.txt", "file2.txt", "file3.txt"};

    for (int i = 0; i < FILE_COUNT; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork error");
            return 1;
        } else if (pid == 0) {
            process_file(file_names[i]);
            exit(0);
        } else {
            pids[i] = pid;
        }
    }

    for (int i = 0; i < FILE_COUNT; i++) {
        int status;
        waitpid(pids[i], &status, 0);
        if (WIFEXITED(status)) {
            printf("子进程 %d 正常结束,处理文件: %s\n", pids[i], file_names[i]);
        }
    }

    printf("所有文件处理完成\n");
    return 0;
}

在这个批处理文件处理示例中,父进程创建多个子进程分别处理不同文件,然后通过waitpid()函数等待每个子进程结束,确保所有文件处理任务完成后再输出提示信息。

通过以上对Linux C语言中等待子进程技术的深入解析,包括相关概念、等待方法、错误处理、特殊情况及实际应用场景,相信读者对这一重要技术有了全面而深入的理解,能够在实际编程中熟练运用,编写出高效、稳定的程序。