深入理解进程等待:wait的简化与waitpid的灵活性
进程等待是操作系统中进程管理的一个重要概念,指的是一个进程暂停执行,直到某个特定条件满足后再继续执行的过程。
目录
一、基本概念
二、进程等待的必要性
三、进程等待方法
1、wait方法
2、waitpid方法
3、注意事项
4、核心区别
5、waitpid 的 PID 参数选项
6、waitpid 的 options 选项
7、联系
四、获取子进程状态
Core Dump(了解即可)
五、阻塞与非阻塞等待
1、进程的阻塞等待方式
2、进程非阻塞等待实现
一、基本概念
进程等待通常发生在以下几种情况:
-
等待I/O操作完成:如磁盘读写、网络数据传输等
-
等待子进程结束:父进程等待子进程执行完毕
-
等待资源可用:如等待锁、信号量等同步机制
-
等待时间条件:如sleep()函数调用
二、进程等待的必要性
- 当子进程退出时,若父进程未读取其退出信息,子进程将转为僵尸状态,导致内存泄漏。
- 僵尸进程无法通过
kill -9
命令清除,因为已终止的进程无法被再次终止。 - 父进程作为子进程的直接管理者,需要了解其派发任务的完成情况。
- 通过进程等待机制,父进程不仅能回收子进程资源,还能获取其退出状态信息。
三、进程等待方法
1、wait方法
#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int* status);
-
作用:等待任意一个子进程终止或进入停止状态。
-
特点:如果没有已终止的子进程,父进程会阻塞(除非设置
WNOHANG
选项)。 -
返回值:成功时返回被等待进程的PID,失败时返回-1。
-
参数说明:status:输出型参数,用于获取子进程退出状态。若不关心可设置为NULL。
例如,父进程创建子进程后,可以通过调用wait函数来等待子进程结束,并获取其退出状态信息:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork();if(id == 0){//childint count = 10;while(count--){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}exit(0);}//fatherint status = 0;pid_t ret = wait(&status);if(ret > 0){//wait successprintf("wait child success...\n");if(WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}}sleep(3);return 0;
}
WIFEXITED(status)
:检查进程是否正常退出(本质是判断是否收到信号),若正常退出,退出信号为0;不正常退出的话,退出信号则为非0WEXITSTATUS(status)
:获取进程的退出码
我们可以使用以下监控脚本对进程进行实时监控:
while :; do ps axj | head -1 && ps axj | grep demo | grep -v grep;echo "######################";sleep 1;done
这时我们可以看到,当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了:
2、waitpid方法
pid_t waitpid(pid_t pid, int *status, int options);
-
作用:等待指定的子进程(或一组子进程)终止或状态变化。
-
特点:可以精确控制等待哪个子进程,支持非阻塞模式。
返回值
- 当waitpid正常执行时,它会返回被收集子进程的进程ID;
- 若设置了WNOHANG选项且未发现已退出的子进程,则返回0;
- 若调用过程中发生错误,则返回-1并设置相应errno值以指示错误类型。
参数说明
通过 pid
参数指定目标进程,options
提供额外控制(如 WNOHANG
)。
-
pid
=-1
:等待任意子进程(等效于wait)>0
:等待其进程ID与pid相等的子进程。
-
status(输出型参数)
WIFEXITED(status)
:若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status)
:若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
-
options
0
:默认阻塞等待WNOHANG
:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
例如,创建子进程后,父进程可使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork();if (id == 0){//child int count = 10;while (count--){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}exit(0);}//father int status = 0;pid_t ret = waitpid(id, &status, 0);if (ret >= 0){//wait success printf("wait child success...\n");if (WIFEXITED(status)){//exit normal printf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killed printf("killed by siganl %d\n", status & 0x7F);}}sleep(3);return 0;
}
WIFEXITED(status)
:检查进程是否正常退出(本质是判断是否收到信号),若正常退出,退出信号为0;不正常退出的话,退出信号则为非0WEXITSTATUS(status)
:获取进程的退出码
运行结果:
在父进程运行过程中,我们可以尝试使用kill -9命令将子进程杀死,这时父进程也能等待子进程成功:
注意: 被信号杀死而退出的进程,其退出码将没有意义。
3、注意事项
当调用 wait/waitpid
时:
- 若子进程已退出,函数会立即返回,并释放相关资源,同时获取子进程的退出信息
- 若子进程仍在正常运行,调用可能会阻塞
- 若指定的子进程不存在,函数将立即返回错误
4、核心区别
特性 | wait | waitpid |
---|---|---|
目标进程 | 任意一个子进程 | 指定 PID 的子进程(或进程组) |
阻塞行为 | 默认阻塞 | 可通过 WNOHANG 非阻塞轮询 |
进程选择灵活性 | 无选择,任意子进程 | 支持多种 PID 选项(见下文) |
使用场景 | 简单等待所有子进程 | 精确控制等待的进程或非阻塞检查 |
5、waitpid
的 PID 参数选项
waitpid
通过 pid
参数指定目标进程:
-
pid > 0
:等待指定的子进程(PID =pid
)。 -
pid = -1
:等待任意子进程(等效于wait
)。 -
pid = 0
:等待与父进程同进程组的任意子进程。 -
pid < -1
:等待进程组 ID 等于|pid|
的任意子进程。
6、waitpid
的 options
选项
-
WNOHANG
:非阻塞模式,如果没有子进程退出,立即返回 0。 -
WUNTRACED
:返回已停止(但未终止)的子进程状态(如被SIGSTOP
暂停)。 -
WCONTINUED
(某些系统):返回已恢复运行的子进程状态(如收到SIGCONT
)。
7、联系
-
wait
是waitpid
的特例:
wait(&status)
等价于waitpid(-1, &status, 0)
。 -
共享底层机制:两者均通过检查子进程的退出状态(
status
)来获取信息,使用宏如WIFEXITED
、WEXITSTATUS
解析状态。
四、获取子进程状态
进程等待使用的两个函数——wait和waitpid,都有一个输出型参数status,该参数由操作系统负责填充。当传入NULL值时,表示父进程不关心子进程的退出状态;否则,操作系统将通过该参数向父进程反馈子进程的退出信息。
虽然status是一个整型变量,但不能简单地将其视为普通整数。它的不同比特位承载着不同的信息(本说明仅分析status的低16位):
status的低16位中,高8位存储进程的退出状态(退出码)。若进程因信号终止,低7位记录终止信号,第8位则作为core dump标志位。
Core Dump(了解即可)
- 在Linux中,core dump标志位(通常指进程的
ulimit -c
设置或文件系统路径中的/proc/sys/kernel/core_pattern
配置)用于控制系统在程序崩溃时是否生成core dump文件,以及如何生成。 - Core Dump(核心转储)文件 是当 Linux 程序异常崩溃(如段错误、非法指令、被
kill -6
终止等)时,操作系统自动生成的一个内存快照文件。它记录了程序崩溃时的内存状态、寄存器值、堆栈信息等关键数据,主要用于调试和分析程序崩溃原因。
通过位操作可以轻松地从status变量中提取进程的退出码和退出信号:
exitCode = (status >> 8) & 0xFF; // 获取退出码
exitSignal = status & 0x7F; // 获取退出信号
系统提供了两个便捷的宏来处理这些信息:
WIFEXITED(status)
:检查进程是否正常退出(本质是判断是否收到信号),若正常退出,退出信号为0;不正常退出的话,退出信号则为非0WEXITSTATUS(status)
:获取进程的退出码
使用示例:
exitNormal = WIFEXITED(status); // 判断是否正常退出
exitCode = WEXITSTATUS(status); // 获取实际退出码
需要注意的是,当进程异常退出(被信号终止)时,其退出码将失去实际意义。
五、阻塞与非阻塞等待
1、进程的阻塞等待方式
上述示例展示了父进程创建并等待单个子进程的情况。实际上,我们还可以同时创建多个子进程,让父进程依次等待它们退出,这就是多进程创建与等待的编程模式。
比如,在以下代码中,我们一次性创建了10个子进程,并将它们的进程ID存入ids数组。每个子进程的退出码被设置为它在数组中的下标位置。随后,父进程使用waitpid函数逐个等待这10个子进程结束。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t ids[10];for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){//childprintf("child process created successfully...PID:%d\n", getpid());sleep(3);exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标}//fatherids[i] = id;}for (int i = 0; i < 10; i++){int status = 0;pid_t ret = waitpid(ids[i], &status, 0);if (ret >= 0){//wait child successprintf("wiat child success..PID:%d\n", ids[i]);if (WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killedprintf("killed by signal %d\n", status & 0x7F);}}}return 0;
}
运行结果:
运行代码,这时我们便可以看到父进程同时创建多个子进程,当子进程退出后,父进程再依次读取这些子进程的退出信息。
2、进程非阻塞等待实现
在之前的例子中,当子进程尚未退出时,父进程会持续等待,在此期间父进程无法执行其他任务,这种等待方式称为阻塞等待。
实际上,我们可以采用非阻塞等待的方式:在子进程未退出时,父进程可以继续处理其他事务,待子进程退出后再获取其状态信息。
实现方法很简单:只需在调用waitpid函数时,将第三个参数options设为WNOHANG。这样设置后,若子进程仍在运行,waitpid会立即返回0;若子进程已正常结束,则返回该子进程的PID。
例如,父进程可以定期调用waitpid进行检查。若子进程尚未退出,父进程可先处理其他任务,稍后再尝试获取子进程的退出状态信息。
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>typedef void (*handler_t)(); // 函数指针类型定义
std::vector<handler_t> handlers; // 任务处理函数容器void task_one() {printf("执行临时任务1\n");
}void task_two() {printf("执行临时任务2\n");
}void load_tasks() {handlers.push_back(task_one);handlers.push_back(task_two);
}void execute_tasks() {if (handlers.empty()) load_tasks();for (auto task : handlers) {task();}
}int main() {pid_t pid = fork();if (pid < 0) {printf("%s: 进程创建失败\n", __FUNCTION__);return 1;} else if (pid == 0) { // 子进程printf("子进程运行中,PID: %d\n", getpid());sleep(5);exit(1);} else { // 父进程int status = 0;pid_t ret = 0;do {ret = waitpid(-1, &status, WNOHANG); // 非阻塞等待if (ret == 0) {printf("子进程仍在运行\n");}execute_tasks();} while (ret == 0);if (WIFEXITED(status) && ret == pid) {printf("成功等待子进程5秒,返回码: %d\n", WEXITSTATUS(status));} else {printf("等待子进程失败\n");return 1;}}return 0;
}
该代码要使用到g++编译器,首先我们要安装g++:
sudo yum install gcc-c++
auto
是 C++11 引入的特性,如果编译器较旧,需添加 -std=c++11
:
g++ demo1.cpp -o demo1 -std=c++11
运行结果:
父进程定时轮询检查子进程的退出状态。若子进程仍在运行,父进程会先处理其他任务,稍后再继续轮询检查,直至子进程退出后获取其终止信息。