说在前面
今天给大家带来操作系统中进程等待的概念,我们学习的操作系统是Linux操作系统。
我们今天主要的目标就是认识wait和waitpid这两个系统调用。
前言
那么这里博主先安利一下一些干货满满的专栏啦!
这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!手撕数据结构https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482
这里是STL源码剖析专栏,这个专栏将会持续更新STL各种容器的模拟实现。算法专栏https://blog.csdn.net/yu_cblog/category_11464817.html
STL源码剖析https://blog.csdn.net/yu_cblog/category_11983210.html?spm=1001.2014.3001.5482
什么是进程等待
简单来说, 进程等待是进程的一种状态, 是父进程等待子进程退出时的一个过程。
当然这样描述,大家肯定是不明白的,继续向后看,我相信大家就能明白了!
为什么需要进程等待
首先,我们知道,创建一个子进程,肯定是想让子进程去完成一些不同于父进程的东西。那么当子进程执行完之后,如果父进程还没有退出,子进程的状态叫做僵尸。
- 子进程退出,父进程不管子进程,子进程就要处于僵尸状态 --- 导致内存泄漏
- 父进程创建了子进程,是要让子进程办事儿的,那么子 进程把任务完成的怎么样?父进程需要关心吗?如果需 要,如何得知?如果不需要,该怎么处理?
当子进程结束时,子进程的结束方式有三种:
- 子进程代码执行完,结果正确
- 子进程代码执行完,结果不正确
- 子进程代码没有执行完,程序崩溃
那么,父进程如果要通过判断子进程的执行情况,分别进行不同操作的时候,我们的父进程就要了解子进程的执行,是属于上面三种的哪一种。与此同时,父进程还需要回收子进程。
如何进行进程等待
进程等待有两个系统调用接口:
- wait
- waitpid
wait
首先,我们先把Makefile,myproc.c准备好
然后,我们在myproc.c里面先构建一个僵尸状态,然后我们再通过进程等待的系统调用接口去解决。
如图所示:通过代码构建一个僵尸状态。
在这份代码中,5秒之后,子进程就正常退出,此时的子进程就是一个Z状态。
关于wait函数的更详细信息,我们可以通过手册来获取。
wait接口的用法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
//返回值: 成功返回被等待进程pid,失败返回-1。
//参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
验证代码和现象如下:
waitpid
第一个参数,表示要等待的进程的子进程的pid。
刚开始我们学习代码的时候,一般只有一个父进程一个子进程的情况。但是继续往后学习,我们接触到多进程的时候,我们就要使用waitpid接口,进行执行的进程的等待。
第二个参数是一个输出型参数,我们传入一个int类型变量status的地址,waitpid会给把等待到的进程的执行情况存储到status中带回来
第三个参数表示进行选择阻塞等待方式/非阻塞等待方式(后面我们会详细讲解和区分)。
此时我们可以先看一个简单的例子:
这里我们举例子使用了阻塞式等待,即第三个参数是0。
在代码中,我们让子进程传回一个特殊的数字,表示它的进程退出码,这里我们传的是105
最后打印一下status(代码没有体现,但是现象中是加上了打印status的,很简单,加一句话就行)
我们发现status并不是传回来的105。
这是因为:status并不是按照整数的方式来使用的,它是按照比特位的方式对status进行划分,一个32个,我们只学习低16个比特位置的划分。
首先,我们所要明确的一个概念:
进程的退出码只有在进程正常结束的时候才有意义!
假设return 0表示正常退出,return 1表示段错误,exit()也一样,返回退出码的前提是,程序正常执行到了这一句。如果程序异常退出了,退出码毫无意义。
那么如何判断程序是否异常退出呢?看所接受到的信号是否为0
如果所接受到的信号为0 -- 表示程序正常退出。
接下来就是一个C语言的问题了 -- 我们如何从status这个32位整数里面得到退出码和信号呢?
这里博主直接给出结果了,如果不明白的伙伴要补习一下C语言了。
- 退出码:(status>>8) & 0xFF
- 信号:status & 0x7F
此时,博主再给大家再次强调一下这个重要概念
两个重点:
- 程序异常退出,本质是进程异常退出,本质是OS杀掉了这个进程
- OS怎么杀——发送信号
一些程序异常退出的例子
讲到这里,我们再回答三个问题:
1. 父进程通过wait/waitpid可以拿到子进程的退出结果,为什么要用waitpid/wait函数呢??直接全局变量不行吗??
当然不行,进程具有独立性,那么数据就要进行写 时拷贝,父进程肯定是拿不到的,更不用说信号了。
2. 既然进程是具有独立性的,进程退出码,不也是子进程的数据吗??父 进程又凭什么拿到呢?? wait/waitpid究竟干了什么呢?
这里我们要从僵尸进程说起
僵尸进程:至少要保留 pcb信息!task_struct里面保留了进程退出时的退 出结果信息!!
因此,wait/waitpid本质是读取了子进程的task_struct结构!
3. 那么现在的问题是:wait/waitpid有这个权利吗???
别忘了, task_stuct是内核数据结构对象!当然有这个权利啊! 因为它们是系统调用啊! 它们在干活儿不就是OS在干活儿吗?
一个小总结
waitpid/wait 可以在目前的情况下,让进 程退出具有一定的顺序性! 将来可以让父进程进行更多的收尾工作!
一些补充
对于上层使用的人来说,我使用的时候还要对操作系统内核有一定了解,还得知道status的构成,我才能提取信息这样未免太麻烦了!
其实操作系统是给我们提供了宏的,一开始博主不告诉大家,是希望大家理解status的低层构成。
阻塞等待和非阻塞等待
因此,如果我们调用的是阻塞等待,父进程在子进程尚未结束的时候的状态是,挂起!
重新理解挂起
这种阻塞等待,其实是在系统调用接口(wait系列函数)中阻塞 也就是内核中阻塞
比如以前我们的scanf,cin 如果我们一直不往键盘输入。此时OS发现缓冲区里面资源没有就绪 系统就把我们的进程挂起! 此时,表面上看,就是卡住了!
而在哪里卡住? 由冯诺依曼体系我们知道, 键盘这些输入硬件,到显示器这种输出硬件中,肯定会经过操作系统,所以其实本质上也是在内核中卡住了! scanf,cin必定封装了系统调用!
阻塞的本质,是在内核中阻塞,在内核中把进程挂起!
基于非阻塞调用的轮询检测方案
那么非阻塞调用的时候,直接就返回了,父进程不等。
那我们怎么知道子进程的状态呢? 我们会每隔一段时间去检测一下子进程的状态,如果监测到子进程结束,就释放子进程这个叫做 --- 基于非阻塞调用的轮询检测方案。
总结
相信看到这里,大家对Linux操作系统中的进程等待已经有了一定的了解了。操作系统的学习是必要的,虽然它可能很枯燥。如果大家觉得这篇文章有帮助的话,不要忘记一键三连哦!!