Linux C语言等待子进程技术解析
一、进程相关基础概念回顾
在深入探讨等待子进程技术之前,我们先来回顾一些进程相关的基础概念。在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;
}
二、为什么需要等待子进程
- 资源回收 子进程在运行结束后,如果父进程不进行处理,其残留的一些资源(如进程控制块等)不会自动完全释放,这会导致系统资源的浪费,甚至可能引发内存泄漏等问题。通过等待子进程,父进程可以确保子进程占用的资源被正确回收。
- 同步执行 在很多实际应用场景中,父进程需要知道子进程的执行结果,然后根据这个结果来决定后续的操作。例如,在一个文件处理程序中,父进程可能创建子进程来进行文件压缩,只有当子进程成功完成压缩后,父进程才能继续进行文件传输等后续操作。如果父进程不等待子进程,就可能在子进程还未完成任务时就执行了错误的后续步骤。
三、等待子进程的方法
- 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)
宏用于获取子进程正常结束时的退出状态码。
- 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秒检查一次子进程是否结束。
- 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_utime
和ru_stime
分别获取子进程在用户态和内核态的CPU时间。
四、等待子进程中的错误处理
- wait()和waitpid()的错误返回
- 返回 -1:当
wait()
或waitpid()
函数返回 -1 时,表明发生了错误。常见的错误原因有:没有子进程(如果在调用这些函数时已经没有子进程,会返回该错误)、调用进程没有权限等待指定的子进程等。在代码中,我们需要通过perror()
函数打印错误信息,以便定位问题。
- 返回 -1:当
#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;
}
- 处理子进程异常终止
当子进程因为收到信号而异常终止时,父进程可以通过
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
信号并在收到信号时打印信息并退出。父进程等待子进程结束,并在子进程被信号终止时打印出终止信号的编号。
五、特殊情况处理
- 孤儿进程 当父进程先于子进程结束时,子进程就会成为孤儿进程。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)。
- 僵尸进程 如果父进程创建了子进程,但没有等待子进程结束,也没有正确处理子进程的退出,子进程在结束后就会变成僵尸进程。僵尸进程虽然已经不再执行,但它的进程控制块仍然存在于系统中,占用一定的系统资源。为了避免僵尸进程的产生,父进程必须及时等待子进程并处理其退出状态。
#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()
。
六、实际应用场景中的等待子进程技术
- 服务器程序 在服务器程序中,常常会为每个客户端连接创建一个子进程来处理客户端请求。父进程需要等待这些子进程结束,以确保资源的正确回收。例如,一个简单的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()
函数以非阻塞方式等待子进程结束,确保资源回收。
- 批处理任务 在批处理任务场景中,父进程可能会创建多个子进程并行执行不同的任务,然后等待所有子进程完成后再进行下一步操作。例如,一个文件处理程序,需要对多个文件进行压缩、加密等操作,可以创建多个子进程分别处理不同文件,父进程等待所有子进程完成后输出处理结果。
#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语言中等待子进程技术的深入解析,包括相关概念、等待方法、错误处理、特殊情况及实际应用场景,相信读者对这一重要技术有了全面而深入的理解,能够在实际编程中熟练运用,编写出高效、稳定的程序。