尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

进程退出与等待_复习笔记

进程退出与等待_复习笔记 进程退出与进程等待第一部分进程退出1.1 进程终止的本质创建进程和终止进程是一对逆向操作操作做了什么创建进程操作系统内多了一个进程 → 多 PCB、多地址空间、多页表、多代码和数据终止进程操作系统内少了一个进程 →释放PCB、地址空间、页表、代码和数据关键点创建进程要占用系统资源终止进程要释放系统资源。但有一个例外 —— 僵尸状态进程终止时它的 PCB不会立即释放而是暂时保留进入僵尸Zombie状态。这样做的目的是方便父进程获取子进程的退出信息退出码、退出信号等。代码和数据释放 ✅地址空间释放 ✅页表释放 ✅PCB保留❌等父进程回收1.2 进程退出的三种场景一个进程退出无非就是三种情况场景描述举例① 代码跑完结果正确程序正常执行完毕完成了用户交给的任务打开文件 → 写入100条数据 → 关闭文件 → 退出② 代码跑完结果不正确代码执行完毕但没达到预期目标想写100条数据但打开文件失败了 → 走异常处理逻辑 → 代码跑完了但没写成③ 代码异常终止代码都没跑完程序直接崩溃段错误、除零错误等场景②和场景③的区别场景②是代码执行完了包括异常处理代码但结果不对场景③是代码执行到一半就崩了根本没跑完。1.3 main 函数返回值的含义intmain(){// ...return0;// 0 表示成功// return 1; // 非0 表示失败}核心知识点main 函数也是函数也要被别人调用被操作系统/启动代码调用main 函数的返回值表明程序的执行情况返回值通常对应退出三种场景中的前两种正常退出结果正确 →return 0结果不正确 →return 非0如 1、2、3…第三种异常终止由操作系统通过信号处理不走 return为什么 main 函数的返回值很重要因为子进程也是进程子进程的 main 函数返回值就是子进程给父进程的交代。父进程需要通过进程等待来获取这个返回值。1.4 返回值通过寄存器传递底层原理问题引入函数内部的变量具有临时性函数结束后销毁那 return 的值是怎么逃出函数传递给调用者的答案通过寄存器。调用者 A 函数 被调用者 B 函数 ┌─────────────────┐ │ B 函数内部变量 │ │ (具有临时性) │ │ │ │ return 1; │ │ ↓ │ │ 将 1 写入 EAX │ ← return 语句的本质 │ 寄存器 │ └────────┬────────┘ │ ret 返回 ↓ 从 EAX 寄存器读取值 ←──────────────┘ 写入 A 函数的变量中详细步骤B 函数执行到return 1时将返回值1写入EAX 寄存器B 函数执行ret指令返回A 函数调用者从 EAX 寄存器中读取值写入自己的变量所以return 语句的本质就是「把返回值写到寄存器」函数返回的本质就是「调用者从寄存器读值」。变量销毁了没关系值已经在寄存器里了。这边还有几个小点补充大型结构体的返回小型返回值int、指针等直接通过 EAX 寄存器大型结构体通过内存拷贝从一个内存块拷贝到另一个内存块但也要通过寄存器辅助1.5 C 语言默认返回值类型为 int为什么 main 函数不写 return 也能编译通过// C 语言中这样写能编译通过func(){// 没写返回值类型// ...}intmain(){// 没写 return 语句// 也能编译通过默认返回 0}两个 C 语言特性不写返回值类型 → 默认是 intC 语言中如果函数不写返回值类型编译器默认认为返回值是int不写 return → EAX 寄存器有默认值如果没有 return 语句EAX 寄存器会有一个默认值通常为 0这是语言层面的特性不是操作系统的问题。不同语言的规则不同但操作系统只关心最终的返回值在 EAX 寄存器中。1.6 0 成功非 0 失败考试类比为什么用 0 表示成功非 0 表示失败用一个生活中的类比来理解场景考试类比对应返回值成功你考了100分没人会问你为什么考100分0成功不需要原因失败你考了9分你爸一定要问你为什么才考9分非0失败需要知道原因核心设计思想0 只有一个值→ 成功只有一种情况不需要区分原因非 0 有 1、2、3、4… 无数个值→ 失败有无数种原因用不同的值区分返回值 0 → 成功不解释 返回值 1 → 失败原因一如没有权限 返回值 2 → 失败原因二如文件不存在 返回值 3 → 失败原因三如资源不存在 返回值 4 → 失败原因四如选项写错了一句话总结成功没人问为什么0失败一定要问为什么非0不同值代表不同原因。1.7 进程退出的方法进程退出有三种常见方式方法说明是否刷新缓冲区returnmain 函数中 return等同于调用 exit是exit(int code)C 库函数退出时会刷新缓冲区、关闭流是_exit(int code)系统调用直接退出不刷新缓冲区否#includestdio.h#includestdlib.h// exit#includeunistd.h// _exitintmain(){printf(hello world);// 没有 \n数据在缓冲区中// 方式一return 0; → 会刷新缓冲区hello world 会被打印// 方式二exit(0); → 会刷新缓冲区hello world 会被打印// 方式三_exit(0); → 不刷新缓冲区hello world 不会被打印_exit(0);}exit 与 _exit 的关系exit 是 C 库函数它在底层会调用 _exit但在调用之前会做收尾工作刷新缓冲区、关闭文件流、执行 atexit 注册的清理函数等。_exit 是系统调用直接让操作系统终止进程。第二部分进程等待2.1 为什么需要进程等待核心逻辑链父进程创建子进程 ↓ 期望子进程帮忙完成任务 ↓ 子进程执行完毕退出三种场景之一 ↓ 父进程需要知道子进程的执行结果如何 ↓ → 进程退出是为了给父进程一个交代 → 进程等待是父进程接收这个交代的手段如果不等待会怎样子进程退出后如果父进程不读取其退出信息子进程的 PCB 一直保留 → 变成僵尸进程僵尸进程占用内存PCB 结构体→ 资源泄漏僵尸进程无法被 kill 掉因为已经死了所以进程等待有两个目的① 获取子进程退出信息 ② 回收子进程资源避免僵尸进程2.2 wait 与 waitpid 函数#includesys/wait.hpid_twait(int*status);pid_twaitpid(pid_tpid,int*status,intoptions);函数作用特点wait等待任意一个子进程退出阻塞式无法指定特定子进程waitpid等待指定子进程退出可指定 PID、可设置阻塞/非阻塞waitpid 参数说明参数说明pid要等待的子进程 PID传 -1 表示等待任意子进程等同 waitstatus输出型参数用于获取子进程退出信息传 NULL 表示不关心options选项标志默认 0 阻塞等待WNOHANG 非阻塞等待2.3 status 参数的位图结构status 是一个int类型的输出型参数但不能直接当整数用。它内部是一个位图结构不同位域存储不同的信息status32位整数的位图结构 31 16 15 8 7 0 ┌──────────────────────┬──────────────┬──────────┐ │ (未使用) │ 退出码 │ 退出信号 │ └──────────────────────┴──────────────┴──────────┘位域内容说明低 8 位bit 0-7退出信号0 表示正常退出非 0 表示被信号杀死次低 8 位bit 8-15退出码main 函数 return 的值或 exit 的参数注意正常退出时退出信号为 0退出码有意义异常终止时退出信号非 0退出码无意义。手动位操作提取理解原理intstatus;waitpid(pid,status,0);// 提取退出码次低8位intexit_code(status8)0xFF;// 提取退出信号低7位intexit_signalstatus0x7F;课堂上老师先带大家手动做位操作理解原理然后再介绍系统提供的宏来替代手动操作。2.4 系统提供的宏重点系统提供了一组宏来替代手动位操作主要有两个WIFEXITED(status)作用检测子进程是否正常退出原理检测退出信号是否为 0退出信号为 0正常退出→ 返回真退出信号非 0异常终止→ 返回假if(WIFEXITED(status)){// 子进程正常退出}else{// 子进程异常终止}WEXITSTATUS(status)作用提取子进程的退出码前提只有在WIFEXITED(status)为真时才有意义原理提取 status 的次低 8 位if(WIFEXITED(status)){intcodeWEXITSTATUS(status);printf(子进程正常退出退出码: %d\n,code);}else{printf(子进程异常终止\n);// 此时退出码无意义可以提取退出信号来了解原因}完整使用示例#includestdio.h#includeunistd.h#includesys/wait.h#includestdlib.hintmain(){pid_tidfork();if(id0){// 子进程intcount5;while(count--){printf(子进程运行中... PID: %d\n,getpid());sleep(1);}exit(10);// 退出码设为 10}// 父进程intstatus0;pid_tridwaitpid(id,status,0);// 阻塞等待if(rid0){if(WIFEXITED(status)){printf(子进程正常退出退出码: %d\n,WEXITSTATUS(status));}else{printf(子进程异常终止\n);}}return0;}运行结果正常退出子进程运行中... PID: 20965 子进程运行中... PID: 20965 子进程运行中... PID: 20965 子进程运行中... PID: 20965 子进程运行中... PID: 20965 子进程正常退出退出码: 10如果子进程被信号杀死如kill -9则WIFEXITED返回假退出码无意义退出信号为 9。2.5 WNOHANG —— 非阻塞等待什么是 WNOHANGWNOHANG是 waitpid 的第三个参数 options 的一个选项值pid_tridwaitpid(id,status,WNOHANG);options 值行为0默认阻塞等待子进程不退出waitpid 就不返回WNOHANG非阻塞等待子进程还没退出waitpid 立即返回名字拆解W - NO - HANG 等 不要 卡住 → 等待时不要卡住 非阻塞HANG的含义计算机卡死了、没反应了就叫 “hang 住了”。服务器不响应请求也叫 “hang 住了”。WNOHANG 就是等待的时候不要 hang 住。2.6 阻塞 vs 非阻塞张三李四打电话类比这是理解阻塞/非阻塞最经典的类比类比角色对应概念张三父进程 / 用户李四子进程 / 操作系统打电话函数调用 / 系统调用挂电话函数返回非阻塞不挂电话函数不返回阻塞非阻塞场景WNOHANG张三走到李四宿舍楼下给李四打电话 张三李四下来没 李四还没我在复习呢。 张三挂电话 ← 立即返回 张三等了一会做做自己的事翻翻书、玩玩手机... 张三又打电话李四下来没 李四还没。 张三挂电话 ← 立即返回 如此往复... 张三再打电话李四下来没 李四好了我下来了 张三挂电话 ← 等待成功 → 两人手拉手去吃饭了这就是非阻塞 轮询每次打电话 一次 waitpid(WNOHANG) 调用李四说还没好 waitpid 返回 0张三挂电话 函数立即返回不卡住张三可以做自己的事 父进程可以执行其他代码反复打电话 轮询阻塞场景默认 options0又是期末考试张三又要找李四复习 张三给李四打电话李四下来我请你吃饭复习。 李四行但你得等我一会我在复习其他科目。 张三好我等你。 张三你别挂电话上次给你打好多次电话花了十块钱电话费 还费电。你把电话放桌上你好了告诉我我再挂。 → 张三一直拿着电话等李四 → 李四复习完了好了我准备好了。 → 张三挂电话 → 两人手拉手去吃饭了这就是阻塞调用打一次电话就不挂 一次 waitpid 调用不返回一直等李四准备好 等子进程退出李四说好了才挂 子进程退出了waitpid 才返回生活中的阻塞调用例子scanf(%d,a);// 你不输入它就一直卡在那里 → 阻塞调用scanf 就是一个典型的阻塞调用你不从键盘输入它就永远不返回。此时你就是张三操作系统就是李四。2.7 阻塞与非阻塞的对比特性阻塞调用非阻塞调用调用后行为等到操作完成才返回立即返回不管操作是否完成调用方状态卡住什么都做不了可以继续做其他事情是否需要重复调用不需要一次就等到结果需要轮询常见例子scanf、默认的 wait/waitpidwaitpid(WNOHANG)效率调用方无法做其他事调用方可以利用等待时间做其他事效率更高适用场景简单场景不关心等待期间的效率需要并发处理等待期间有事可做非阻塞效率高的原因不是说让子进程更快完成而是让父进程在等待期间可以做其他事情单位时间能做更多的事所以整体效率更高。张三在等李四的时候可以翻书、玩手机而不是傻站着。2.8 waitpid 返回值非阻塞模式下的三种情况非阻塞模式下waitpid 有三种返回值返回值含义对应类比 0等待成功子进程已退出李四说我下来了 0调用结束但子进程还没退出李四说我还没下来 0等待失败调用出错电话打错了/号码不存在阻塞模式只有 0和 0两种情况因为不会在子进程未退出时返回。非阻塞模式多了 0这种情况。2.9 非阻塞轮询代码示例#includestdio.h#includeunistd.h#includesys/wait.h#includestdlib.hintmain(){pid_tidfork();if(id0){// 子进程一直运行不主动退出intcount5;while(count--){printf(子进程运行中... PID: %d\n,getpid());sleep(1);}exit(10);}// 父进程非阻塞轮询intstatus0;while(1){pid_tridwaitpid(id,status,WNOHANG);// 非阻塞if(rid0){// 等待成功子进程已退出printf(等待成功\n);if(WIFEXITED(status)){printf(子进程正常退出退出码: %d\n,WEXITSTATUS(status));}else{printf(子进程异常终止退出信号: %d\n,status0x7F);}break;}elseif(rid0){// 本轮调用结束子进程没有退出printf(本轮调用结束子进程没有退出\n);sleep(1);// 休息一会再问避免 CPU 空转}else{// 等待失败printf(等待失败\n);break;}}return0;}运行效果子进程运行中... PID: 20965 本轮调用结束子进程没有退出 子进程运行中... PID: 20965 本轮调用结束子进程没有退出 子进程运行中... PID: 20965 本轮调用结束子进程没有退出 子进程运行中... PID: 20965 本轮调用结束子进程没有退出 子进程运行中... PID: 20965 本轮调用结束子进程没有退出 等待成功 子进程正常退出退出码: 10注意轮询时一定要加sleep否则父进程会疯狂循环调用 waitpid导致 CPU 占用率飙升CPU 空转。这在实际开发中非常不友好。2.10 非阻塞轮询的并发优势非阻塞调用的真正价值在于父进程在等待子进程期间可以做其他事情。时间线对比 【阻塞模式】 父进程: [waitpid...等待...等待...等待...等待...等待...] → 获取结果 ↑ 这段时间父进程什么都做不了 【非阻塞模式】 父进程: [waitpid→0] [做自己的事] [waitpid→0] [做自己的事] [waitpid→0] → 获取结果 子进程: [运行中...] [运行中...] [运行中...] [运行中...] [退出] ↑ 这段时间父进程在做其他事两个进程并发执行非阻塞 轮询 父进程和子进程可以并发运行父进程不会因为等待子进程而浪费自己的时间。第三部分知识总结与考点3.1 进程退出知识图谱进程退出 ├── 本质释放系统资源PCB保留→僵尸状态 ├── 三种场景 │ ├── 代码跑完结果正确 → return 0 │ ├── 代码跑完结果不正确 → return 非0 │ └── 代码异常终止 → 被信号杀死 ├── 返回值含义 │ ├── 0 成功不需要原因因为只有一个值 │ └── 非0 失败1,2,3... 不同值表示不同原因 ├── 返回值传递原理 │ └── 通过 EAX 寄存器传递return 写入调用者读取 ├── C语言特性 │ ├── 不写返回值类型 → 默认 int │ └── 不写 return → EAX 有默认值通常为 0 └── 退出方法 ├── returnmain函数中 → 刷新缓冲区 ├── exit() → C库函数刷新缓冲区 └── _exit() → 系统调用不刷新缓冲区3.2 进程等待知识图谱进程等待 ├── 目的 │ ├── 获取子进程退出信息 │ └── 回收子进程资源避免僵尸进程 ├── 函数 │ ├── wait(status) → 等待任意子进程阻塞 │ └── waitpid(pid, status, options) → 等待指定子进程可选阻塞/非阻塞 ├── status 参数 │ ├── 位图结构低8位退出信号次低8位退出码 │ ├── WIFEXITED(status) → 是否正常退出信号0 │ └── WEXITSTATUS(status) → 提取退出码 ├── WNOHANG 选项 │ ├── 非阻塞等待子进程没退出也立即返回 │ └── W-NO-HANG 等待-不要-卡住 ├── 阻塞 vs 非阻塞 │ ├── 阻塞等完才返回如 scanf │ └── 非阻塞立即返回需轮询 ├── waitpid 返回值非阻塞模式 │ ├── 0等待成功子进程已退出 │ ├── 0调用结束子进程未退出 │ └── 0等待失败 └── 非阻塞轮询 ├── 循环调用 waitpid(WNOHANG) ├── 需加 sleep 避免 CPU 空转 └── 优势父进程可并发做其他事3.3 还有一些我总结的知识点进程退出的三种场景是什么代码跑完结果正确return 0代码跑完结果不正确return 非0代码异常终止被信号杀死为什么 0 表示成功非 0 表示失败成功只有一种情况0不需要区分原因失败有多种原因1,2,3…用不同值区分函数返回值是怎么传递的通过寄存器EAX传递return 将值写入 EAX调用者从 EAX 读取exit 和 _exit 的区别exit 是 C 库函数会刷新缓冲区、关闭流_exit 是系统调用直接终止不刷新缓冲区WIFEXITED 和 WEXITSTATUS 的作用WIFEXITED检测是否正常退出退出信号是否为0WEXITSTATUS提取退出码正常退出时才有意义什么是 WNOHANG非阻塞等待选项子进程未退出时立即返回返回0避免父进程一直卡在 waitpid阻塞和非阻塞的区别阻塞调用后一直等待直到完成才返回非阻塞调用后立即返回需要通过轮询重复检测非阻塞轮询为什么要加 sleep避免 CPU 空转疯狂循环调用 waitpidsleep 让父进程在两次检测之间休息为什么需要进程等待获取子进程退出信息给父进程一个交代回收子进程资源避免僵尸进程僵尸进程是怎么产生的子进程退出后父进程没有调用 wait/waitpid 回收子进程的 PCB 一直保留在系统中僵尸进程无法被 kill 掉已经死了
返回列表