keyipatience:个人主页作者简介C/C后端开发学习者专栏传送门《c》《linux》⭐️patience is key in lifeLinux的写时拷贝COW0.物理内存页物理内存整块仓库 ·物理内存页仓库里的一个个小格子每一个小格子都有唯一的编号物理页号PFNCPU真正访问内存的时候都是按“一页一页”拿不是按字节随便拿实现机制1. 页表与物理页映射父进程和子进程最初指向相同的物理内存页内核将共享的物理页标记为只读并在页表中记录只读的权限。2. 缺页异常触发当任一进程尝试写入共享页时CPU触发缺页异常Page Fault。内核捕获异常并检查触发原因是否是对只有读权限的页采取了写操作。3. 物理页复制内核分配新的物理页复制原页内容到新页。修改故障进程的页表使其指向新复制的物理页。将新页标记为可读写原共享页仍保持只读供其他进程使用。进程控制进程的创建在前面我们已经讨论过了现在我们就不再过多阐述进程终止进程终⽌的本质是释放系统资源就是释放进程申请的相关内核数据结构和对应的数据和代码。进程退出场景• 代码运行完毕结果正确• 代码运行完毕结果不正确• 代码异常终止1.main函数的返回值和进程退出码1.直接先说结论main函数的返回值就是进程的退出码main函数的返回值表明程序的运行情况如果返回结果为0就表示代码运行完毕结果正确如果为非0就表示代码运行完毕但结果不正确对于代码异常终止现在暂时不做讨论不同的返回值代表不同的出错原因可以使⽤ strerror 函数来将错误代码转换为对应的错误描述字符串.在C/C程序中main函数的返回值通常用于表示程序的执行状态这个返回值会被操作系统转换为进程的退出码Exit Code。退出码是一个整数存储在task_struct内部用于指示程序是否成功执行或遇到错误。2.进程退出码的获取echo $ ?打印最近一个进程的退出码信息所以第一次打印的就是./mypocess进程的退出码因为该文件不存在程序运行结果不正确对应的退出码为2第二次打印的就是对应echo $?这个进程的退出码因为该程序运行成功退出码就是0这个进程的退出码就不再是89因为该进程代码异常终止了退出码就无意义了。进程出现了异常一般是进程收到了信号后面再详细说明3.补充一个知识点函数返回不是返回的是函数的内部变量吗而内部变量具有临时性为什么还可以返回到函数外部呢其实一般函数的返回都是通过寄存器实现的return语句就是把返回值写到寄存器内部当要返回的时候就再把寄存器的内容写到另一个接受返回值的变量里就是对应的两条move语句。如果没有return语句即不返回那么就不存在move eax 1 ret这条语句而move 0x123[]这条语句还是存在只不过eax的值就是默认的值了进程的退出方法1.returnmain函数的returnmain函数结束就表示进程结束其他函数只表示自己函数调用完成2.exit发现退出码为40即在任何地方调用exit直接表示进程结束终止整个进程而非仅退出当前函数最后返回一个状态码给操作系统。所以我们看到没有打印fun end甚至是main函数内部要打印的main。3.exit和_exit_exit是系统调用直接终止进程不执行任何清理操作不会刷新标准 I/O 缓冲区。exit是 C 标准库函数会在终止进程前执行一系列清理操作。它会刷新标准 I/O 缓冲区并最终调用_exit完成进程终止egexit终止进程前会刷新缓冲区所以睡眠2s后会显示要打印的main如果是_exit则因为直接终止进程不会刷新缓冲区就不会打印并且我们在前面知道库和系统调用其实是上下层的关系有心的开发者可以对部分系统调用进行适度封装从而形成库有了库就很有利于更上层用户或者开发者进行二次开发因为只用操作系统才有能力去终止一个进程所以exit的底层还是会去调用系统调用即exit底层封装了_exit.4.我们讨论的刷新缓冲区那这个缓冲区到底是在哪里呢首先它一定不是操作系统内部的缓冲区因为exit底层也相当于封装了_exit如果是操作系统内部的话那么它们的在刷新缓冲区这一块功能应该就是一样的了这明显和前面说的矛盾了所以这个缓冲区应该是在库里是库缓冲区c语言提供的缓冲区进程等待进程等待必要性• 之前讲过子进程退出父进程如果不管不顾就可能造成‘僵尸进程’的问题进而造成内存泄漏。• 另外进程⼀旦变成僵⼫状态那就刀枪不入“杀⼈不眨眼”的kill-9也无能为力因为谁也 没有办法杀死⼀个已经死去的进程。• 最后父进程派给子进程的任务完成的如何我们需要知道。如子进程运行完成结果对还是 不对或者是否正常退出。• 父亲进程通过进程等待的方式回收子进程资源获取子进程退出信息进程等待的方法wait() 系统调用wait()是最基础的进程等待方法它会阻塞父进程直到任意一个子进程终止#include sys/types.h #include sys/wait.h pid_t wait(int *status);参数status用于存储子进程的退出状态。不关心就置为NULL返回值成功时返回终止的子进程PID失败返回-1。特点无法指定特定子进程会阻塞调用者直到有子进程终止即如果没有子进程退出父进程就一直处于阻塞状态waitpid() 系统调用waitpid()提供了更精细的控制可以等待特定子进程或非阻塞检查。pid_t waitpid(pid_t pid, int *status, int options);pid指定要等待的子进程PID0等待特定PID的子进程-1等待任意子进程类似wait()0等待与调用进程同组ID的子进程暂时不关心-1等待进程组ID等于pid绝对值的子进程暂时不关心options控制等待行为WNOHANG非阻塞模式WUNTRACED也返回停止的子进程状态返回值成功时返回子进程PID如果使用WNOHANG且没有子进程终止返回0失败返回-1获取子进程的statusstatus参数用于存储子进程的退出状态信息通常通过宏来解析其具体含义我们先看一个代码为什么打印的status不是1呢这就需要我们仔细去理解一下status• wait和waitpid都有⼀个status参数该参数是⼀个输出型参数由操作系统填充。• 如果传递NULL表示不关心子进程的退出状态信息。• 否则操作系统会根据该参数将子进程的退出信息反馈给父进程。• status不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究status低16 比特位虽然是int类型但我们现在只看低16比特位低8位0-7信号编号如果进程被信号终止第8位8-15退出状态如果进程正常退出0-255如果进程是处于代码1运行完毕结果正确或者(2)代码运行完毕结果不正确这两种情况的话core dump和终止信号8个比特位默认都是0代码异常终止的后面再谈所以正确的status应该是退出状态退出码1和8个0的组合即1 00000000对应的结果就是256所以如果我们要查看进程状态就得(status8)0xFF,0xFF255接下来我们再来看一下对应的终止信号有哪些我们发现里面并没有0信号所以如果代码是(3)异常终止(被信号所杀或者代码有错误的低7个bit位就不再是0而是保存异常时对应的信号编号此时退出码退出状态无意义egkill信号杀我们通过kill指令杀掉这个死循环while1的进程就会看到这个结果eg(2):野指针怎么做到的呢进一步理解僵尸状态进程具有独立性那么子进程的退出信息父进程就不能直接拿到那wait和waitpid是怎么得到子进程的退出信息包括退出码和退出信号的呢前面我们知道一旦子进程变成僵尸状态它的退出信息就应该放到其task_struct里面了所以task_struct里面就得有相应的变量来存储这些退出信息呀看一看源码发现果然存在exit_state记录进程退出状态如EXIT_ZOMBIE或EXIT_DEAD。exit_code存储子进程的退出码exit_signal :存储子进程的退出信号所以完整的一个流程大概是1子进程退出时状态设置为EXIT_ZOMBIE僵尸状态2将退出码或信号编号存入task_struct的exit_code等字段3等到父进程调用wait()或waitpid()时内核就会从子进程的task_struct中读取exit_code等等4填充到父进程提供的状态变量中5最后释放子进程的task_struct内存彻底清除僵尸进程。确保父进程能可靠获取子进程的退出状态同时避免资源泄漏。WIFEXITED和WEXITSTATUSWIFEXITED(status): 若子进程正常终止则为真。查看进程 是否是正常退出 WEXITSTATUS(status): 若WIFEXITED非零提取子进程退出码。查看进程 的退出码optionsWNOHANG非阻塞模式。如果没有子进程退出立即返回 0 而不阻塞。我们可以通过一个例子来理解一下当我们要和朋友出去玩去并在他楼下等他时打电话叫他下楼时如果第一次打电话他说还有一会那我们直接就把电话挂了过了一会再打给他如果他说还要等一会那我们就有直接把电话挂了在打了几次后还没有下来这时我们就不挂电话了一直保持通话时刻关注对方到底还有多久才下来。在这个例子中我们打了一次电话但直接挂断就相当于非阻塞状态打了多次电话就相当于轮询多次调用所以综合来看就是非阻塞轮询轮询就是通过循环的方式实现。如果我们打了电话但并没有挂断而是一直保持通话就相当于阻塞状态。如果处于非阻塞状态的话等待方就可以做一些自己的事所以非阻塞状态可以支持更高并发量提高效率所以这时的waitpidoptios为WNOHANG在非阻塞状态下)就会多一个返回值情况1大于0返回子进程的pid等待成功人下来了2等于0调用结束立即返回不阻塞但子进程没有退出相当于挂了电话人还没有下来3小于0等待失败人不去了代码实现运行结果在kill-9杀掉进程后
18.Linux进程退出和进程等待机制详解
发布时间:2026/5/22 3:46:14
keyipatience:个人主页作者简介C/C后端开发学习者专栏传送门《c》《linux》⭐️patience is key in lifeLinux的写时拷贝COW0.物理内存页物理内存整块仓库 ·物理内存页仓库里的一个个小格子每一个小格子都有唯一的编号物理页号PFNCPU真正访问内存的时候都是按“一页一页”拿不是按字节随便拿实现机制1. 页表与物理页映射父进程和子进程最初指向相同的物理内存页内核将共享的物理页标记为只读并在页表中记录只读的权限。2. 缺页异常触发当任一进程尝试写入共享页时CPU触发缺页异常Page Fault。内核捕获异常并检查触发原因是否是对只有读权限的页采取了写操作。3. 物理页复制内核分配新的物理页复制原页内容到新页。修改故障进程的页表使其指向新复制的物理页。将新页标记为可读写原共享页仍保持只读供其他进程使用。进程控制进程的创建在前面我们已经讨论过了现在我们就不再过多阐述进程终止进程终⽌的本质是释放系统资源就是释放进程申请的相关内核数据结构和对应的数据和代码。进程退出场景• 代码运行完毕结果正确• 代码运行完毕结果不正确• 代码异常终止1.main函数的返回值和进程退出码1.直接先说结论main函数的返回值就是进程的退出码main函数的返回值表明程序的运行情况如果返回结果为0就表示代码运行完毕结果正确如果为非0就表示代码运行完毕但结果不正确对于代码异常终止现在暂时不做讨论不同的返回值代表不同的出错原因可以使⽤ strerror 函数来将错误代码转换为对应的错误描述字符串.在C/C程序中main函数的返回值通常用于表示程序的执行状态这个返回值会被操作系统转换为进程的退出码Exit Code。退出码是一个整数存储在task_struct内部用于指示程序是否成功执行或遇到错误。2.进程退出码的获取echo $ ?打印最近一个进程的退出码信息所以第一次打印的就是./mypocess进程的退出码因为该文件不存在程序运行结果不正确对应的退出码为2第二次打印的就是对应echo $?这个进程的退出码因为该程序运行成功退出码就是0这个进程的退出码就不再是89因为该进程代码异常终止了退出码就无意义了。进程出现了异常一般是进程收到了信号后面再详细说明3.补充一个知识点函数返回不是返回的是函数的内部变量吗而内部变量具有临时性为什么还可以返回到函数外部呢其实一般函数的返回都是通过寄存器实现的return语句就是把返回值写到寄存器内部当要返回的时候就再把寄存器的内容写到另一个接受返回值的变量里就是对应的两条move语句。如果没有return语句即不返回那么就不存在move eax 1 ret这条语句而move 0x123[]这条语句还是存在只不过eax的值就是默认的值了进程的退出方法1.returnmain函数的returnmain函数结束就表示进程结束其他函数只表示自己函数调用完成2.exit发现退出码为40即在任何地方调用exit直接表示进程结束终止整个进程而非仅退出当前函数最后返回一个状态码给操作系统。所以我们看到没有打印fun end甚至是main函数内部要打印的main。3.exit和_exit_exit是系统调用直接终止进程不执行任何清理操作不会刷新标准 I/O 缓冲区。exit是 C 标准库函数会在终止进程前执行一系列清理操作。它会刷新标准 I/O 缓冲区并最终调用_exit完成进程终止egexit终止进程前会刷新缓冲区所以睡眠2s后会显示要打印的main如果是_exit则因为直接终止进程不会刷新缓冲区就不会打印并且我们在前面知道库和系统调用其实是上下层的关系有心的开发者可以对部分系统调用进行适度封装从而形成库有了库就很有利于更上层用户或者开发者进行二次开发因为只用操作系统才有能力去终止一个进程所以exit的底层还是会去调用系统调用即exit底层封装了_exit.4.我们讨论的刷新缓冲区那这个缓冲区到底是在哪里呢首先它一定不是操作系统内部的缓冲区因为exit底层也相当于封装了_exit如果是操作系统内部的话那么它们的在刷新缓冲区这一块功能应该就是一样的了这明显和前面说的矛盾了所以这个缓冲区应该是在库里是库缓冲区c语言提供的缓冲区进程等待进程等待必要性• 之前讲过子进程退出父进程如果不管不顾就可能造成‘僵尸进程’的问题进而造成内存泄漏。• 另外进程⼀旦变成僵⼫状态那就刀枪不入“杀⼈不眨眼”的kill-9也无能为力因为谁也 没有办法杀死⼀个已经死去的进程。• 最后父进程派给子进程的任务完成的如何我们需要知道。如子进程运行完成结果对还是 不对或者是否正常退出。• 父亲进程通过进程等待的方式回收子进程资源获取子进程退出信息进程等待的方法wait() 系统调用wait()是最基础的进程等待方法它会阻塞父进程直到任意一个子进程终止#include sys/types.h #include sys/wait.h pid_t wait(int *status);参数status用于存储子进程的退出状态。不关心就置为NULL返回值成功时返回终止的子进程PID失败返回-1。特点无法指定特定子进程会阻塞调用者直到有子进程终止即如果没有子进程退出父进程就一直处于阻塞状态waitpid() 系统调用waitpid()提供了更精细的控制可以等待特定子进程或非阻塞检查。pid_t waitpid(pid_t pid, int *status, int options);pid指定要等待的子进程PID0等待特定PID的子进程-1等待任意子进程类似wait()0等待与调用进程同组ID的子进程暂时不关心-1等待进程组ID等于pid绝对值的子进程暂时不关心options控制等待行为WNOHANG非阻塞模式WUNTRACED也返回停止的子进程状态返回值成功时返回子进程PID如果使用WNOHANG且没有子进程终止返回0失败返回-1获取子进程的statusstatus参数用于存储子进程的退出状态信息通常通过宏来解析其具体含义我们先看一个代码为什么打印的status不是1呢这就需要我们仔细去理解一下status• wait和waitpid都有⼀个status参数该参数是⼀个输出型参数由操作系统填充。• 如果传递NULL表示不关心子进程的退出状态信息。• 否则操作系统会根据该参数将子进程的退出信息反馈给父进程。• status不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究status低16 比特位虽然是int类型但我们现在只看低16比特位低8位0-7信号编号如果进程被信号终止第8位8-15退出状态如果进程正常退出0-255如果进程是处于代码1运行完毕结果正确或者(2)代码运行完毕结果不正确这两种情况的话core dump和终止信号8个比特位默认都是0代码异常终止的后面再谈所以正确的status应该是退出状态退出码1和8个0的组合即1 00000000对应的结果就是256所以如果我们要查看进程状态就得(status8)0xFF,0xFF255接下来我们再来看一下对应的终止信号有哪些我们发现里面并没有0信号所以如果代码是(3)异常终止(被信号所杀或者代码有错误的低7个bit位就不再是0而是保存异常时对应的信号编号此时退出码退出状态无意义egkill信号杀我们通过kill指令杀掉这个死循环while1的进程就会看到这个结果eg(2):野指针怎么做到的呢进一步理解僵尸状态进程具有独立性那么子进程的退出信息父进程就不能直接拿到那wait和waitpid是怎么得到子进程的退出信息包括退出码和退出信号的呢前面我们知道一旦子进程变成僵尸状态它的退出信息就应该放到其task_struct里面了所以task_struct里面就得有相应的变量来存储这些退出信息呀看一看源码发现果然存在exit_state记录进程退出状态如EXIT_ZOMBIE或EXIT_DEAD。exit_code存储子进程的退出码exit_signal :存储子进程的退出信号所以完整的一个流程大概是1子进程退出时状态设置为EXIT_ZOMBIE僵尸状态2将退出码或信号编号存入task_struct的exit_code等字段3等到父进程调用wait()或waitpid()时内核就会从子进程的task_struct中读取exit_code等等4填充到父进程提供的状态变量中5最后释放子进程的task_struct内存彻底清除僵尸进程。确保父进程能可靠获取子进程的退出状态同时避免资源泄漏。WIFEXITED和WEXITSTATUSWIFEXITED(status): 若子进程正常终止则为真。查看进程 是否是正常退出 WEXITSTATUS(status): 若WIFEXITED非零提取子进程退出码。查看进程 的退出码optionsWNOHANG非阻塞模式。如果没有子进程退出立即返回 0 而不阻塞。我们可以通过一个例子来理解一下当我们要和朋友出去玩去并在他楼下等他时打电话叫他下楼时如果第一次打电话他说还有一会那我们直接就把电话挂了过了一会再打给他如果他说还要等一会那我们就有直接把电话挂了在打了几次后还没有下来这时我们就不挂电话了一直保持通话时刻关注对方到底还有多久才下来。在这个例子中我们打了一次电话但直接挂断就相当于非阻塞状态打了多次电话就相当于轮询多次调用所以综合来看就是非阻塞轮询轮询就是通过循环的方式实现。如果我们打了电话但并没有挂断而是一直保持通话就相当于阻塞状态。如果处于非阻塞状态的话等待方就可以做一些自己的事所以非阻塞状态可以支持更高并发量提高效率所以这时的waitpidoptios为WNOHANG在非阻塞状态下)就会多一个返回值情况1大于0返回子进程的pid等待成功人下来了2等于0调用结束立即返回不阻塞但子进程没有退出相当于挂了电话人还没有下来3小于0等待失败人不去了代码实现运行结果在kill-9杀掉进程后