佛山做外贸网站咨询,平面设计培训班有用吗,阳江房管局查询房产信息网,建站程序选择文章目录进程等待与资源回收#xff1a;父进程的责任一、进程终止方式回顾与深化1.1 回顾#xff1a;为什么需要进程等待1.2 进程退出的三种方式1.2.1 return退出1.2.2 exit()函数1.2.3 _exit()函数1.3 三种方式的关键区别#xff1a;缓冲区刷新1.4 退出码的含义二、进程等待…文章目录进程等待与资源回收父进程的责任一、进程终止方式回顾与深化1.1 回顾为什么需要进程等待1.2 进程退出的三种方式1.2.1 return退出1.2.2 exit()函数1.2.3 _exit()函数1.3 三种方式的关键区别缓冲区刷新1.4 退出码的含义二、进程等待机制2.1 进程等待的必要性2.2 wait函数详解2.3 waitpid函数详解2.4 status参数的位图解析2.5 阻塞等待 vs 非阻塞等待2.5.1 阻塞等待2.5.2 非阻塞等待三、实战案例3.1 非阻塞等待一边等待一边工作3.2 完整的进程回收示例四、总结与展望进程等待与资源回收父进程的责任欢迎讨论这是Linux系统编程系列的第五篇文章。在第二篇中我们学习了僵尸进程的产生和危害——当子进程退出而父进程不回收时子进程就会变成僵尸。那么父进程如何正确地回收子进程呢这就是本篇要深入讲解的进程等待机制。如果有任何疑问欢迎在评论区交流点赞、收藏与分享这篇文章包含了大量实战代码和原理分析如果对你有帮助请点赞、收藏并分享给更多的朋友承上启下建议先阅读本系列前四篇文章理解进程的创建、状态、调度和虚拟内存这样学习进程等待会更轻松。一、进程终止方式回顾与深化在深入学习进程等待之前我们先来系统地理解进程的退出方式。这将帮助我们更好地理解wait/waitpid获取的退出信息。1.1 回顾为什么需要进程等待在第二篇文章中我们详细学习了僵尸进程的概念。让我们快速回顾一下核心要点僵尸进程的产生条件子进程已经执行结束父进程仍在运行父进程没有调用wait()或waitpid()读取子进程的退出状态僵尸进程的危害占用内核内存PCB无法释放占用进程号PID资源耗尽大量僵尸进程会导致无法创建新进程因此父进程必须通过进程等待来回收子进程避免产生僵尸进程。1.2 进程退出的三种方式进程正常退出有三种方式它们看起来相似但有重要的区别。1.2.1 return退出这是最常见的退出方式intmain(){printf(hello world\n);return0;// 返回退出码0}当main函数执行return n时等同于调用exit(n)。这是因为调用main函数的运行时库返回值作为exit的参数。1.2.2 exit()函数#includestdlib.hvoidexit(intstatus);exit是C标准库函数它在终止进程前会做一些清理工作调用用户通过atexit()或on_exit()注册的清理函数刷新所有标准I/O流的缓冲区关闭所有打开的 标准 I/O 流FILE删除tmpfile()创建的临时文件最后调用_exit()1.2.3 _exit()函数#includeunistd.hvoid_exit(intstatus);_exit是系统调用它会立即终止进程_exit 不做用户态清理不刷新 stdio 缓冲、不调用 atexit 回调等进程结束后内核会回收资源并关闭 FD。1.3 三种方式的关键区别缓冲区刷新让我们通过实验来观察三种退出方式的区别。实验1使用exit()#includestdio.h#includestdlib.hintmain(){printf(hello);// 注意没有\nexit(0);}编译运行gcc test.c -otest./test输出hello可以看到hello被正常输出了。实验2使用_exit()#includestdio.h#includeunistd.hintmain(){printf(hello);// 注意没有\n_exit(0);}运行结果./test# 什么都没有输出为什么会这样这涉及到C标准库的缓冲机制printf(hello)→ 数据进入缓冲区 → 等待刷新 exit(0)→ 刷新缓冲区 → 输出到终端 → 进程退出 _exit(0)→ 直接退出 → 缓冲区数据丢失缓冲区的刷新时机缓冲区满了遇到换行符\n程序正常结束调用exit或return手动调用fflush()让我们验证一下#includestdio.h#includeunistd.hintmain(){printf(hello\n);// 加上\n_exit(0);}这次会输出hello因为\n触发了缓冲区刷新。注意当 stdout 连接到终端时stdout 通常是行缓冲\n 会触发刷新所以能看到输出。当 stdout 重定向到文件/管道时stdout 往往变成全缓冲\n 不一定立刻刷新这时 _exit() 可能仍然导致输出丢失。或者手动刷新#includestdio.h#includeunistd.hintmain(){printf(hello);fflush(stdout);// 手动刷新_exit(0);}1.4 退出码的含义无论使用哪种退出方式都需要传递一个退出码(exit code)exit(0);// 退出码为0exit(1);// 退出码为1_exit(10);// 退出码为10return5;// 退出码为50表示成功程序正常执行完毕没有错误非0表示失败不同的非0值可以表示不同的错误类型注意虽然退出码是int类型但只有低8位有效。所以退出码的范围是0-255。exit(256);// 实际退出码是0256 % 256 0exit(257);// 实际退出码是1257 % 256 1在shell中可以通过$?查看上一个程序的退出码./testecho$?# 查看test的退出码常见的退出码0成功1通用错误2误用shell命令126命令不可执行127命令未找到130通过CtrlC终止信号2143通过kill终止信号15可以使用strerror()函数获取错误码的描述#includestdio.h#includestring.h#includeerrno.hintmain(){for(inti0;i10;i){printf(错误码%d: %s\n,i,strerror(i));}return0;}输出错误码0: Success 错误码1: Operation not permitted 错误码2: No suchfileor directory 错误码3: No such process...二、进程等待机制理解了进程的退出方式后我们来学习父进程如何获取子进程的退出信息。2.1 进程等待的必要性父进程需要进行进程等待主要有三个原因1. 回收子进程资源子进程退出后虽然用户空间内存被释放了但PCB(task_struct)仍然占用内核内存。父进程通过wait/waitpid来释放这部分资源。2. 获取子进程的退出信息父进程往往需要知道子进程的执行结果子进程是否正常退出退出码是多少如果异常退出是被哪个信号终止的3. 避免僵尸进程堆积如果父进程不回收子进程系统中会堆积大量僵尸进程最终导致内核内存耗尽PID资源耗尽无法创建新进程2.2 wait函数详解wait是最简单的进程等待函数#includesys/types.h#includesys/wait.hpid_twait(int*status);返回值成功返回被回收的子进程的PID失败返回-1参数status输出型参数用于获取子进程的退出状态如果不关心退出状态可以传NULLwait的行为如果所有子进程都还在运行wait会阻塞等待如果有子进程已经退出变成僵尸wait立即返回并回收如果没有子进程wait返回-1让我们看一个简单的例子#includestdio.h#includestdlib.h#includeunistd.h#includesys/wait.hintmain(){pid_t idfork();if(id0){perror(fork);return1;}elseif(id0){// 子进程printf(子进程[%d]开始运行\n,getpid());sleep(3);printf(子进程[%d]即将退出退出码10\n,getpid());exit(10);}else{// 父进程printf(父进程[%d]等待子进程[%d]\n,getpid(),id);intstatus0;pid_t retwait(status);// 阻塞等待if(ret0){printf(等待成功回收了进程%d\n,ret);printf(子进程的退出码: %d\n,(status8)0xFF);}}return0;}运行结果父进程[12800]等待子进程[12801]子进程[12801]开始运行 子进程[12801]即将退出退出码10等待成功回收了进程12801 子进程的退出码:102.3 waitpid函数详解waitpid是wait的增强版提供了更多的控制选项#includesys/types.h#includesys/wait.hpid_twaitpid(pid_t pid,int*status,intoptions);参数pidpid 0等待进程ID为pid的子进程pid -1等待任意子进程与wait相同pid 0等待同一进程组的任意子进程pid -1等待进程组ID为|pid|的任意子进程参数status与wait相同输出型参数传NULL表示不关心退出状态参数options0阻塞等待默认行为WNOHANG非阻塞等待如果没有子进程退出则立即返回0返回值成功回收子进程返回子进程的PID使用WNOHANG且没有子进程退出返回0出错返回-1waitpid相比wait的优势可以等待指定的子进程支持非阻塞等待更灵活的控制2.4 status参数的位图解析status参数不是简单的整数而是一个位图包含了丰富的信息。我们需要理解它的结构status(32位整数)┌─────────┬─────────┬──────────┬──────────┐ │ 高16位 │ 退出码 │ core dump│ 信号编号 │ │(不关心)│8位 │1位 │7位 │ └─────────┴─────────┴──────────┴──────────┘15-8位 第7位6-0位低16位的含义0-6位导致进程终止的信号编号如果是信号终止第7位core dump标志1表示产生了core文件8-15位进程的退出码如果是正常退出不要依赖固定位布局因为不同平台实现可能不一样务必使用 wait.h 提供的宏解析。Linux提供了一组宏来解析status1. WIFEXITED(status)检查进程是否正常退出调用exit/_exit/returnif(WIFEXITED(status)){printf(进程正常退出\n);}2. WEXITSTATUS(status)获取退出码仅当WIFEXITED为真时有效if(WIFEXITED(status)){intcodeWEXITSTATUS(status);printf(退出码: %d\n,code);}3. WIFSIGNALED(status)检查进程是否被信号终止if(WIFSIGNALED(status)){printf(进程被信号终止\n);}4. WTERMSIG(status)获取终止信号编号仅当WIFSIGNALED为真时有效if(WIFSIGNALED(status)){intsigWTERMSIG(status);printf(终止信号: %d\n,sig);}让我们写一个完整的例子来演示#includestdio.h#includestdlib.h#includeunistd.h#includesys/wait.hintmain(){pid_t idfork();if(id0){perror(fork);return1;}elseif(id0){// 子进程运行20秒后退出printf(子进程[%d]开始运行\n,getpid());sleep(20);exit(10);}else{// 父进程等待子进程printf(父进程[%d]等待子进程[%d]\n,getpid(),id);printf(你可以在20秒内kill掉子进程来观察信号终止\n);intstatus0;pid_t retwaitpid(id,status,0);if(ret0){if(WIFEXITED(status)){// 正常退出printf(子进程正常退出退出码: %d\n,WEXITSTATUS(status));}elseif(WIFSIGNALED(status)){// 信号终止printf(子进程被信号%d终止\n,WTERMSIG(status));}}}return0;}测试场景1让子进程正常退出./test# 等待20秒输出父进程[12900]等待子进程[12901]子进程[12901]开始运行 你可以在20秒内kill掉子进程来观察信号终止 子进程正常退出退出码:10测试场景2在另一个终端kill子进程# 终端1./test# 终端2psaux|greptest# 找到子进程PIDkill-912901# 发送SIGKILL信号终端1输出父进程[12900]等待子进程[12901]子进程[12901]开始运行 你可以在20秒内kill掉子进程来观察信号终止 子进程被信号9终止2.5 阻塞等待 vs 非阻塞等待wait和waitpid默认都是阻塞等待但waitpid支持非阻塞模式。2.5.1 阻塞等待阻塞等待的特点pid_t retwaitpid(-1,status,0);// options0阻塞模式如果子进程还在运行父进程会一直等待无法执行其他代码直到子进程退出waitpid才返回示例#includestdio.h#includestdlib.h#includeunistd.h#includesys/wait.hintmain(){pid_t idfork();if(id0){perror(fork);return1;}elseif(id0){// 子进程运行5秒printf(子进程[%d]开始运行\n,getpid());sleep(5);printf(子进程[%d]退出\n,getpid());exit(0);}else{// 父进程阻塞等待printf(父进程开始等待...\n);intstatus0;pid_t retwaitpid(id,status,0);// 阻塞在这里printf(等待结束回收了进程%d\n,ret);}return0;}运行结果父进程开始等待... 子进程[13000]开始运行 子进程[13000]退出 等待结束回收了进程13000可以看到父进程在waitpid处阻塞了5秒期间无法做任何事情。2.5.2 非阻塞等待非阻塞等待允许父进程在等待的同时做其他事情pid_t retwaitpid(-1,status,WNOHANG);// WNOHANG非阻塞如果子进程还在运行waitpid立即返回0如果子进程已经退出waitpid返回子进程PID并回收父进程可以在循环中检查子进程状态同时执行其他任务这在实际应用中非常有用。让我们来写一个更实用的例子三、实战案例3.1 非阻塞等待一边等待一边工作假设父进程需要等待子进程完成任务但在等待期间还有自己的工作要做。这时就需要非阻塞等待#includestdio.h#includestdlib.h#includeunistd.h#includesys/wait.h#includevector#includetime.htypedefvoid(*handler_t)();// 函数指针类型std::vectorhandler_ttasks;// 任务队列// 模拟父进程的任务voidtask_one(){printf( 执行任务1检查系统日志\n);}voidtask_two(){printf( 执行任务2更新配置文件\n);}voidtask_three(){printf( 执行任务3发送心跳包\n);}// 加载任务voidload_tasks(){if(tasks.empty()){tasks.push_back(task_one);tasks.push_back(task_two);tasks.push_back(task_three);}}// 执行所有任务voiddo_tasks(){load_tasks();for(autotask:tasks){task();}}intmain(){pid_t idfork();if(id0){perror(fork);return1;}elseif(id0){// 子进程模拟长时间任务printf(子进程[%d]开始工作...\n,getpid());sleep(10);printf(子进程[%d]完成工作\n,getpid());exit(0);}else{// 父进程非阻塞等待 处理自己的任务printf(父进程[%d]创建了子进程[%d]\n,getpid(),id);printf(父进程在等待的同时会处理自己的任务\n\n);intstatus0;pid_t ret0;// 轮询检查子进程状态do{retwaitpid(id,status,WNOHANG);// 非阻塞if(ret0){// 子进程还在运行父进程可以做自己的事printf([时间: %d秒] 子进程还在运行中...\n,(int)time(NULL)%100);do_tasks();// 执行父进程的任务printf(\n);sleep(2);// 每2秒检查一次}}while(ret0);// 子进程退出了if(ret0){printf(\n);printf(子进程已退出\n);if(WIFEXITED(status)){printf(正常退出退出码: %d\n,WEXITSTATUS(status));}}}return0;}运行结果父进程[13100]创建了子进程[13101]父进程在等待的同时会处理自己的任务 子进程[13101]开始工作...[时间:45秒]子进程还在运行中...执行任务1检查系统日志执行任务2更新配置文件执行任务3发送心跳包[时间:47秒]子进程还在运行中...执行任务1检查系统日志执行任务2更新配置文件执行任务3发送心跳包[时间:49秒]子进程还在运行中...执行任务1检查系统日志执行任务2更新配置文件执行任务3发送心跳包 子进程[13101]完成工作子进程已退出 正常退出退出码:0可以看到父进程在等待子进程的同时每2秒就执行一次自己的任务实现了并发处理。3.2 完整的进程回收示例让我们写一个综合示例演示进程等待的各种场景#includestdio.h#includestdlib.h#includeunistd.h#includesys/wait.h#includestring.h// 打印status的详细信息voidprint_status(intstatus){printf(status %d (0x%X)\n,status,status);printf(低7位(信号): %d\n,status0x7F);printf(第7位(core): %d\n,(status7)1);printf(高8位(退出码): %d\n,(status8)0xFF);if(WIFEXITED(status)){printf(→ 进程正常退出退出码%d\n,WEXITSTATUS(status));}elseif(WIFSIGNALED(status)){printf(→ 进程被信号%d终止\n,WTERMSIG(status));}}intmain(){printf( 测试1正常退出 \n);pid_t id1fork();if(id10){printf(子进程1[%d]退出退出码5\n,getpid());exit(5);}else{intstatus0;waitpid(id1,status,0);print_status(status);}printf(\n 测试2异常退出(除0) \n);//这是演示用除 0 属于未定义行为不建议在工程代码中依赖它触发信号。pid_t id2fork();if(id20){printf(子进程2[%d]执行除0操作\n,getpid());inta1/0;// 触发SIGFPE信号(void)a;}else{intstatus0;waitpid(id2,status,0);print_status(status);}printf(\n 测试3使用WNOHANG \n);pid_t id3fork();if(id30){printf(子进程3[%d]将运行5秒\n,getpid());sleep(5);exit(0);}else{intcount0;intstatus0;pid_t ret;while(1){retwaitpid(id3,status,WNOHANG);if(ret0){printf(第%d次检查子进程还在运行\n,count);sleep(1);}elseif(ret0){printf(第%d次检查子进程退出了\n,count);print_status(status);break;}else{perror(waitpid);break;}}}return0;}运行结果测试1正常退出子进程1[13200]退出退出码5status1280(0x500)低7位(信号):0第7位(core):0高8位(退出码):5→ 进程正常退出退出码5测试2异常退出(除0)子进程2[13201]执行除0操作 status8(0x8)低7位(信号):8第7位(core):0高8位(退出码):0→ 进程被信号8终止测试3使用WNOHANG子进程3[13202]将运行5秒 第1次检查子进程还在运行 第2次检查子进程还在运行 第3次检查子进程还在运行 第4次检查子进程还在运行 第5次检查子进程还在运行 第6次检查子进程退出了 status0(0x0)低7位(信号):0第7位(core):0高8位(退出码):0→ 进程正常退出退出码0这个示例完整演示了正常退出的status解析信号终止的status解析非阻塞等待的使用方式四、总结与展望通过本篇文章我们系统地学习了进程等待与资源回收的核心知识进程退出方式理解了return、exit()、_exit()三种退出方式掌握了它们的关键区别缓冲区刷新学会了退出码的使用和含义进程等待机制掌握了wait()和waitpid()的使用理解了阻塞等待与非阻塞等待的区别学会了解析status参数获取子进程退出信息实现了一边等待一边工作的并发处理模式核心要点回顾父进程必须回收子进程否则产生僵尸进程status是位图包含退出码和信号信息WNOHANG实现非阻塞等待提高程序并发能力使用宏解析statusWIFEXITED、WEXITSTATUS等在下一篇文章中我们将学习进程程序替换exec函数族。我们会理解为什么fork出的子进程只能执行父进程的代码如何让子进程执行一个全新的程序以及如何结合forkexecwait实现一个完整的命令行解释器(mini-shell)。思考题为什么_exit()不刷新缓冲区而exit()要刷新这样设计的目的是什么如果父进程创建了5个子进程如何确保所有子进程都被回收在什么场景下应该使用阻塞等待什么场景下应该使用非阻塞等待如果子进程的退出码是256父进程通过WEXITSTATUS获取到的值是多少为什么以上就是关于进程等待与资源回收的内容下一篇我们将揭开程序替换的神秘面纱实现属于自己的shell