大家好,我是程序员小青蛙,今天讲解进程终止和进程等待问题。
一、进程退出的场景
进程退出主要分为 3 种情况:
- 正常终止:代码运行完毕,结果正确(如
return 0)。 - 正常终止:代码运行完毕,结果不正确(如
return 1)。 - 异常终止:代码没跑完,程序被信号终止(如
Ctrl+C、kill命令)。
进程退出码可以通过
echo $?查看,$?永远记录最近一个进程的退出码。
二、进程退出的 4 种方式
1.main函数return返回
main函数的return n,本质上等同于调用exit(n)。- 进程退出码由
return的值决定,只有低 8 位有效(0-255)。 - 示例:
return 0表示程序正常结束;return 1表示程序执行出错。
2. 调用exit()函数(库函数)
#include <stdlib.h> void exit(int status);status:进程的退出状态,父进程可通过wait获取。- 核心特点:退出前会做清理工作
- 执行用户通过
atexit/on_exit注册的清理函数。 - 刷新所有打开的流缓冲区(如
printf的缓冲区)。 - 关闭所有打开的文件流。
- 最终调用
_exit()终止进程。
- 执行用户通过
3. 调用_exit()函数(系统调用)
#include <unistd.h> void _exit(int status); 参数:status定义了进程的终止状态,父进程通过wait来获取该值 说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时, 在终端执行$?发现返回值是255。- 直接终止进程,不会刷新缓冲区、不执行清理函数。
- 示例对比:
// 用 exit() 时,缓冲区会被刷新,输出 "hello" printf("hello"); exit(0); // 用 _exit() 时,缓冲区不会被刷新,无输出 printf("hello"); _exit(0);
4. 被信号终止(异常退出)
- 如
Ctrl+C(SIGINT)、kill -9(SIGKILL)、段错误(SIGSEGV)。 - 这种情况下,进程退出码无意义,终止原因记录在
status中。
三、exit()vs_exit()核心区别
| 特性 | exit() | _exit() |
|---|---|---|
| 类型 | C 标准库函数 | 系统调用 |
| 刷新缓冲区 | ✅ 会刷新用户态缓冲区 | ❌ 不会刷新 |
| 执行清理函数 | ✅ 执行atexit注册的函数 | ❌ 不执行 |
| 关闭文件流 | ✅ 关闭所有打开的流 | ❌ 直接交给内核处理 |
| 适用场景 | 正常退出程序 | 异常情况下强制终止 |
四、进程等待:为什么必须wait?
必要性:
子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
- 回收僵尸进程:子进程退出后,如果父进程不回收,会变成僵尸进程,占用系统资源。
- 获取退出状态:父进程可以通过
wait知道子进程的退出码,判断子进程是否正常结束。 - 进程同步:父进程可以通过
wait阻塞等待子进程完成任务,再继续执行。
五、进程等待的两种方法
1.wait()函数
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);- 功能:阻塞等待任意一个子进程退出,回收其资源。
- 参数:
status是输出型参数,用于获取子进程的退出状态(不关心可传NULL)。 - 返回值:成功返回退出子进程的
pid;失败返回-1。
2.waitpid()函数
pid_t waitpid(pid_t pid, int *status, int options);- 功能:等待指定的子进程退出,支持非阻塞等待。
- 参数详解:
pid:pid > 0:等待pid等于指定值的子进程。pid = -1:等待任意子进程(和wait等价)。pid = 0:等待和调用者同组的任意子进程。pid < -1:等待进程组 ID 等于|pid|的任意子进程。
status:同wait(),获取退出状态。options:0:阻塞等待(默认行为)。WNOHANG:非阻塞等待,若子进程未退出,函数立即返回0。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
六、解析status:判断子进程退出原因
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
分析一下这个16位
st是一个 16 位的状态数字Linux 中,
wait(&st)拿到的st是一个int 整数,但真正有用的只有低 16 位:15 14 13 12 11 10 9 8 │ 7 │ 6 5 4 3 2 1 0 退出码(8位) │core│ 信号号(7位)结构固定死了:
- bit 0~6(低 7 位):终止信号编号
- bit 7:core dump 标志
- bit 8~15(高 8 位):进程退出码(exit code)
第一句:
st & 0x7F→ 取低 7 位(信号编号)1. 0x7F 是什么?
二进制:
0111 1111刚好7 个 1。
2. 按位与 & 作用
st & 0x7F→ 只保留最低 7 位→ 高位全部清零3. 这 7 位存的是什么?
如果进程被信号杀死,这 7 位就是信号编号:
- 9 → SIGKILL
- 15 → SIGTERM
- 2 → SIGINT
所以:
st & 0x7F → 拿到杀死子进程的信号编号第二句:
(st >> 8) & 0xFF→ 取高 8 位(退出码)1.
st >> 8把 16 位数字向右移动 8 位。
移动前:
高8位(退出码) 低8位(信号+core) [15.............8][7.............0]右移 8 位后:
原来的高8位 来到了低8位 [15.............8] → 低8位2.
& 0xFF只保留低 8 位,也就是退出码。
3. 最终结果
(st >> 8) & 0xFF → 拿到子进程的 exit(10) 里的 10
status是一个int类型,其低 16 位包含了子进程的退出状态信息:
- 正常终止:低 8 位为
0,高 8 位为退出码(通过WEXITSTATUS(status)获取)。 - 异常终止:低 7 位为终止信号(通过
status & 0x7F获取),第 8 位为core dump标志。
测试代码
#include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main( void ) { pid_t pid; if ( (pid=fork()) == -1 ) perror("fork"),exit(1); if ( pid == 0 ){ sleep(20); exit(10); } else { int st; int ret = wait(&st); if ( ret > 0 && ( st & 0X7F ) == 0 ){ //正常退出 printf("child exit code:%d\n", (st>>8)&0XFF); } else if( ret > 0 ) {//异常退出 printf("sig code : %d\n", st&0X7F ); } } }关键点:
st & 0x7F:取低 7 位,信号编号在这里(st>>8)&0xFF:取高 8 位,退出码在这里
正常退出
杀死之后异常退出
常用宏定义
WIFEXITED(status) // 若为真,表示子进程正常退出 WEXITSTATUS(status) // 获取子进程的退出码(仅正常退出时有效) WIFSIGNALED(status) // 若为真,表示子进程被信号终止 WTERMSIG(status) // 获取终止子进程的信号编号七、代码示例:进程等待的两种方式
1. 阻塞等待(waitpid+options=0)
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> int main() { pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程 printf("child[%d] is running...\n", getpid()); sleep(3); // 模拟任务执行 exit(10); // 子进程退出,退出码为10 } else { // 父进程 printf("parent[%d] waiting for child...\n", getpid()); int status; pid_t ret = waitpid(pid, &status, 0); // 阻塞等待 if (ret > 0) { if (WIFEXITED(status)) { printf("child exited normally, exit code: %d\n", WEXITSTATUS(status)); } else { printf("child exited abnormally, signal: %d\n", status & 0x7F); } } } return 0; }int main() { pid_t pid; pid = fork(); if(pid < 0){ printf("%s fork error\n",__FUNCTION__); return 1; } else if( pid == 0 ){ //child printf("child is run, pid is : %d\n",getpid()); sleep(5); exit(257); } else{ int status = 0; pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S printf("this is test for wait\n"); if( WIFEXITED(status) && ret == pid ){ printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status)); }else{ printf("wait child failed, return.\n"); return 1; } } return 0; }
1.
fork()创建子进程pid = fork();
- 调用后变成两个进程
- 父进程返回子进程 pid
- 子进程返回0
2. 子进程做了什么?
else if( pid == 0 ){ printf("child is run, pid is : %d\n",getpid()); sleep(5); exit(257); }
- 打印自己的 PID
- sleep(5):休眠 5 秒
- exit(257):退出,退出码是257
3. 父进程做了什么?
pid_t ret = waitpid(-1, &status, 0);
- -1:等待任意子进程
- &status:输出型参数,拿到子进程退出状态
- 0:阻塞等待(子进程不退出,父进程就一直等)
4. 判断子进程是否正常退出
if( WIFEXITED(status) && ret == pid )
WIFEXITED(status)→ 为真 →子进程正常退出(不是被信号杀死)ret == pid→ 确保等待的就是目标子进程5. 获取退出码
WEXITSTATUS(status)
- 拿到子进程
exit(n)中的n- 只取低 8 位!
最重要考点:exit (257) 为什么输出 1?
- 257 的二进制:
257 = 00000001 00000001- 退出码只保留低 8 位
- 所以:
257 & 0xFF = 1所以程序最终输出:
wait child 5s success, child return code is :1.
2. 非阻塞等待(waitpid+WNOHANG)
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> int main() { pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程 printf("child[%d] is running...\n", getpid()); sleep(3); exit(10); } else { // 父进程 printf("parent[%d] waiting non-blocking...\n", getpid()); int status; pid_t ret; do { ret = waitpid(pid, &status, WNOHANG); // 非阻塞等待 if (ret == 0) { printf("child is still running, parent do other work...\n"); sleep(1); } } while (ret == 0); if (ret > 0) { if (WIFEXITED(status)) { printf("child exited normally, exit code: %d\n", WEXITSTATUS(status)); } } } return 0; }#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> int main() { pid_t pid; pid = fork(); if(pid < 0){ printf("%s fork error\n",__FUNCTION__); return 1; }else if( pid == 0 ){ //child printf("child is run, pid is : %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("child is running\n"); } sleep(1); }while(ret == 0); if( WIFEXITED(status) && ret == pid ){ printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status)); }else{ printf("wait child failed, return.\n"); return 1; } } return 0; }
1. 父进程非阻塞等待子进程
waitpid(-1, &status, WNOHANG)- WNOHANG:非阻塞模式
- 如果子进程还在运行→
waitpid立即返回0- 如果子进程已经退出→ 返回子进程pid
2. 父进程做了什么?
- 每秒轮询一次
- 没等到就打印:
child is running- 等到了就打印退出码
运行结果
child is run, pid is : child is running child is running child is running child is running wait child 5s success, child return code is :1.一共打印5 次 child is running因为子进程 sleep (5),父进程每秒问一次。
关键函数讲解
1.
waitpid(-1, &status, WNOHANG)
-1:等待任意子进程&status:拿到退出状态- WNOHANG:非阻塞等待
- 子进程没退出 → 返回0
- 子进程已退出 → 返回子进程 pid
2.
WIFEXITED(status)判断子进程是否正常退出(不是被信号杀死)
3.
WEXITSTATUS(status)拿到子进程的退出码
阻塞 vs 非阻塞(总结)
阻塞等待(wait /waitpid 0)父进程一直等,啥也不干,直到子进程退出
非阻塞等待(waitpid WNOHANG)父进程每隔一段时间问一次,不等的时候可以干别的
八、关键知识点总结
- 进程退出方式:
return、exit()、_exit()、信号终止。 exit()vs_exit():前者刷新缓冲区、执行清理;后者直接终止。- 进程等待的目的:回收僵尸进程、获取退出状态、进程同步。
wait()vswaitpid():wait阻塞等待任意子进程;waitpid支持指定进程和非阻塞等待。status解析:通过WIFEXITED、WEXITSTATUS等宏判断子进程退出原因。