前言
前面的好几期都是在介绍进程是什么,以及进程的状态、优先级等,并未过多的介绍如何控制进程,例如:如何终止进程、如何等待进程等!本期就来介绍如何等待进程!
本期内容介绍
进程创建
进程终止
进程等待
一、进程创建
1.1初识fork函数
在Linux中fork函数是非常重要的一个函数,他从已存在的进程中创建一个新的进程,新进程为子进程,原先的进程为父进程!
#include <unistd.h>
pid_t fork();
返回值:如果创建子进程成功,给子进程返回0, 给父进程返回子进程的pid;创建失败,返回父进程-1
进程调用fork,当控制转移到内核的fork代码后,内核将做:
1、分配新的内存块和内核数据结构给子进程
2、将父进程部分数据结构内容拷贝给子进程
3、添加子进程到系统的进程列表中
4、fork返回,开始调度器调度
其实上面的这几条都很好理解,我们知道子进程创建成功的本质是系统中多了一个进程,而进程 = 内核的相关数据结构(task_struct、mm_struct、页表) + 代码和数据!代码共享、数据以写时拷贝的方式各自私有,内核数据结构上期介绍了每个进程也是各自有一份,子进程内核数据结构的大部分数据都是继承于父进程的(pid等除外),这样也就保证了进程的独立性!
下面就是fork创建子进程的一个简图:
当一个进程调用完fork之后,就有了两个二进制代码相同的进程,但每个进程都将执行自己的那部分代码:
再来看一个:
这两个例子都可以看到一开始的第一句子进程没有执行,原因是fork之后父子进程才开始同时独立的执行fork之后的代码!注意:子进程是可以看到父进程的fork之前的代码的,但是并不会回退执行!!
如下图:
注意:fork之后谁先被调度,取决于调度器
1.2fork的返回值
创建成功:给子进程返回0, 给父进程返回子进程的pid
创建失败:给父进程返回-1
OK,这个上面就已经介绍过了这里不咋多哔哔了,这里有一个问题是:为什么要给父进程返回子进程的pid, 给子进程返回0?
为了方便父进程标识子进程,从而进行管理(杀掉、等待)!!
例如一位父亲是通过自己孩子的名字来表示自己的孩子的,有了对孩子的标识就可以更好的对孩子进行管理!
1.3写时拷贝
通常情况下,父子代码都是共享的,如果父子都不对数据进行做写入,数据也是共享的;当任意一方试图写入时,页表会判断标记位,返现出现的错误返回给操作系统,系统可以识别是需要发生写时拷贝,然后重新开辟内存空间将原先的共享的数据拷贝过去,然后再让写入方写入即写时拷贝!
OK,如下图:
1.4fork的常规用法
1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。也就是和父进程做类似的工作!例如:父进程等待客户的请求,生成的子进程来请求处理!
2、一个进程要执行一个不同的程序!也就是和父进程做不同的工作!例如:子进程从fork返回后,调用exec函数(进程等待介绍)!
1.5fork调用失败的原因
1、系统中有太多的进程
2、实际用户的进程数超过了限制
二、进程终止
2.1什么是进程终止?
进程终止是指进程完成了其工作或因为某种原因(错误、用户请求、异常等)而停止执行!进程终止后,不在接收新的CPU的调度,即不再运行了!
2.2进程终止是在做什么?
在谈后面的进程终止的情况前,我们得先搞明白进程终止是在做什么?我们知道:进程 = 内核的相关数据结构(task_struct、mm_struct、页表)+ 进程的代码和数据;所以终止之后的进程就要把他的代码和数据先给释放掉,然后释放内核数据结构(PCB、页表和地址空间),但是这里注意的是PCB(task_struct)是会等待一段时间等父进程读取其退出信息的,此时等待父进程来读取的子进程时处于僵尸(Z)状态!
总结:进程终止的本质是在做两件事:1、释放曾今的代码和数据所占据的空间 2、释放内核数据结构,其中PCB会等待一段时间,等父进程来读取退出信息,此时子进程处于僵尸状态!
2.3进程退出码
OK,在介绍进程终止的情况前先来介绍一个必备的知识即退出码:退出码是一个进程在执行结束后返回给操作系统的一个数字表示该进程执行任务的结果!
例如,我们平时写代码在main函数中最后总是写一个return 0;这个0就是退出码!我们所有的退出码最终都是要被父进程bash拿到的,在父进程即bash中有一个专门存储退出码的变量?可以通过取?里面的值来查看!
?是bash内部的一个获取最近一个子进程的退出码的变量!
echo $? 查看最近一次bash子进程的退出码
例如:
在例如将main函数的返回值改为100获取到的就是100:
这里也验证了?是bash的一个内部变量,因为进程之间具有独立性,?是bash的本地局部变量,子进程是获取不到的,而我们以前介绍过echo是内建命令,即echo是没有创还能子进程而是bash本身去执行的!
这种情况,bash先是获取了子进程的退出码100,之后再echo $?,此时bash内部没有创建子进程是bash本身去执行echo的,bash本身就是 一个进程执行完成后也会有退出码的,所以后面获取到的就是0;
OK,虽然获取到了进程的退出码,但是我们对退出码并不敏感,数字更适合给机器看,我们更适合看数字对应的信息,其实我们可以通过C语言的strerror函数来打印出进程退出码对应的信息:
这里系统规定的目前只有134个,退出码对应的退出信息其中0表示进程执行成功,非0表示有问题!当然我么也可以自定义(退出码和错误信息):
这些我们在C语言实现数据结构是大量玩过的,这里不在多哔哔了!
父进程bash为什么要获取子进程的退出码?
获取子进程退出的情况(成功、失败,失败的原因),如果出现作物或异常翻译给用户,让用户处理!其实就是为用户负责!
举个例子:
假设你马上要考高等数学了,但是你平时没有好好去上课,经常去逃课打游戏,但是你发现上一届和你一起打游戏的学长高数学的挺好的,你于是就让他帮你代考(代考当然不符合我们当代青少年的价值观,不能采取),你就等着,他考完了你是不是得问一下什么情况呀!你好判断是不是可以过了? 同理,子进程是不是父进程派出去完成某个任务的,所以子进程完成的情况是不是父进程也要获取一下子进程的完成情况。
2.4进程终止的三种情况
1、代码运行结束,结果正确
2、代码运行结束,结果不正确
3、代码异常终止(出现异常的本质是进程收到了OS发送的信号)
上面的三种情况可以分为两类即代码跑完和代码没跑完!如果是代码跑完可以按照,进程的退出码来判断该进程执行任务的结果是否正确!如果是代码没有跑完那一定是出异常了,此时的退出码就没有意义了,得看退出信号!
OK,举个例子:比如所你今天考试了回家你爹问你儿子试考的咋样?你说不行C语言才考了100分,你爹问总分多少?总分100!好了,你爹什么也不说了因为你过了!这种情况就是代码跑完,结果对即情况1; 第二学期考完回家,你爹再问考的咋样,你直接说数据结构考了20分,你爹就问你为什么考了20!这种情况就是代码跑完,结果不对,父进程读取后要显示给用户退出码对应的错误信息方便用户操作!第三学期考完回家,你爹还是问儿子考的咋样?你说爹我没考完,你爹此时就不在问你考了多少了,而是问你为什么没考完?你说考试作弊被抓了...这就是第三种情况,代码没跑完异常终止,此时退出码没意义了,要看退出信号确定异常终止的原因!
代码跑完的上面在演示退出码的哪里已经演示了,这里不在多哔哔了,直接演示一下异常终止的情况:
这其实就是子进程发生异常,系统给子进程发了11号信号,出现的段错误:
OK,我们可以让一个正常的程序一直跑,然后给这个正常的程序发送11号信号,看是不是会出现上面的段错误:
所以介绍了这里,我么就可以理解,在VS上运行程序的时候崩溃了的本质是操作系统发现你的进程做了不该做的事情,系统给你的进程发送信号杀死你的进程!
总结:一个退出的进程结果是否正确,看退出信息,退出信息分为:退出信号和退出码。
1、先看退出信号,信号为0无异常;非0有异常,按照信号确定异常信息
2、不是异常,就是进程正常结束,此时看退出码就可以确定出进程的结果
衡量一个进程的退出,父进程bash只需要两个数字即进程的退出码和退出信号!如何获取后面等待介绍~!
我们知道子进程退出之后要等待父进程读取它的PCB(task_struct)也就是当子进程退出的时候要把它对应的退出码和退出信号写入到它的PCB里面,父进程读取的就是他俩喽~!
2.5如何终止进程?
1、main函数return 返回;(注意:在main函数中返回才算终止进程,在普通函数return 表示函数结束)
2、在代码中调用 exit函数(在代码的任何地方调用都是结束进程)
3、调用 _exit(); 系统调用
4、异常终止,ctrl+c
第一个和第四很好理解,不在演示了!这里主要演示一下第二个和第三个:
#include <stdlib.h>
void exit(int status);
#include <uistd.h>
_exit(int status);
status 定义了进程终止的状态,父进程通过wait来获取该值(进程的等待介绍)
说明:虽然status是int但是仅有低8位可以被父进程所用。所以_exit(-1);在中断的时候echo $?是255!
OK,这样看两者没有区别但其实exit最后是调用了_exit的,但是在调用之前,还做了其他的工作!
1、执行用户通过atexit或on_exit定义的清理函数
2、关闭所有打开的流,所有的缓存数据均被写入
3、调用_exit
如下图:
所以,我们可以看到exit是在_exit的上层,而exit会刷新缓冲区,在去调_exit但是_exit是直接调内核的,不会刷新缓冲区!即目前我们所说的缓冲区不是内核的缓冲区!
OK,可以用一段代码验证一下:
注意:return是一种更加常见的退出进程的方式。执行return n;等于执行exit(n);因为调用main的运行时函数会将main的返回值当作exit的参数!
三、进程等待
3.1进程等待是什么?
进程等待就是在父进程的代码中等待子进程的结束,通过调用wait/waitpid,来进行对子进程状态监测和对子进程的PCB进行回收的功能!
3.2进程等待必要性
1、之前介绍过,子进程退出,父进程如果不管不顾,就可可能造成僵尸进程的问题,进而造成内存泄漏。
2、进程一旦变成僵尸状态,那就刀枪不入了,就连"杀人不眨眼的"kill -9都无能为力了,因为谁也没有办法杀死一个已经死掉的进程!
3、父进程派给子进程的任务完成的如何,我们需要知道。例如:子进程完成任务的情况,以及是否正常退出!
所以,父进程通过进程等待的方式,回收子进程资源(解决僵尸状态问题),获取子进程的退出信息(子进程的退出情况)!
3.3进程等待的方式
wait方法
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
作用:等待任意一个子进程结束
返回值:等待成功返回,被等待进程的pid, 等待失败,返回父进程-1
参数:输出型参数,获取子进程退出的状态信息,不关心则可以设置成NULL;
注意:这里暂时不介绍参数,下面和waitpid一起介绍,所以如果我们不管退出状态可以将wait的参数设置为NULL
OK,举个例子:
这样就可以等待进程了!我们现在有个问题就是:如果子进程没有退出,父进程是一直等待还是处于其他状态呢?
其实这个问题通过上面的运行结果,可以清楚的看到如果子进程没有退出,此时父进程会一直处于阻塞等待即S状态!
如何理解父进程阻塞等待子进程?
关于阻塞,我们前面在介绍进程概念那一期介绍过,阻塞的本质就是在等待某种资源,我们在那里举的例子是scanf输入时进程就在等待键盘资源,即把当前进程的PCB链到键盘的等待队列中,等待时进程处于休眠状态,等资源满足了就从键盘的等待队列中唤醒,然后再到CPU的等待队列继续等待被调度!这里其实也是类似的,上面是进程等待硬件资源,这里父进程在等(子进程)待软件资源的满足,所以当父进程阻塞等待时是把父进程的PCB链入到子进程的等待队列即父进程进入S状态,等待子进程的条件满足了,再把父进程从子进程的等待队列中唤醒继续执行!
waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定pid的子进程!
返回值:
当等待成功时,返回子进程的pid;
等待失败,当返回-1;此时errno会被设置相应的值来表示错误!
如果设置了选项WNOHANG,而调用中发现等待的子进程没有退出时,返回0;
参数:
pid: 要等待子进程的子进程的pid
pid = -1,等待任意一个子进程,和wait的功能一样!
pid > 0,等待进程的id与pid相等的子进程!
status: 子进程的退出信息
WIFEXITED(status);若为正常终止子进程返回的状态,则为真!(正常退出)
WWXITSTATUS(status);若WIFEXITED非0提取进程的退出码!(获取退出码)
options: 是否是阻塞等待
options : 若pid指定的子进程未结束,则waitpid返回0,不予以等待;
OK, 我们来先演示一下参数,等待任意一个和等待指定pid的:
看一个等待失败的:
这里父进程在子进程执行他的代码的时候一直等待着,5秒以后进入睡眠,10s之后等待pid为id+1的子进程,结果没有id+1的子进程所以就等待失败了!等待失败了,我们得看看为什么失败的,就得看看进程的退出信息,这个信息包含两部分:一个是进程的退出码,一部分是进程的退出信号!他两如何获取呢?其实就存在在第二个参数status中!
status的最低7位是进程的退出信号, 次低8位(无符号)是退出码!(位图的思想)
位运算的方式获取退出信息
获取退出码:((status >> 8) & 0xff) 将次低八位按位与上8个1
获取退出信号:(status & 0x7f) 将最低7位按位与上7个1
OK,下面我们就先用位运算的方式获取一下:
没有问题我们子进程的退出码就是1,且没有异常退出即退出信号为0!我们再来看一个异常的:
子进程第一次循环就挂了(退出信号不为0):
我们前面介绍过当子进程发生异常时,系统会给该进程发送11号终止该进程,显示为段错误!此时退出码无意义!
OK,这样我们就获取到了子进程的退出信息但是,这样获取是不是有点难度啊,还得对子进程的退出信息有所了解!我就想获取一下退出信息你好让我了解他的组成是不是很不方便啊,那我们还有没有其他方法呢?其实是有的就是上面提到的两个宏!
使用宏获取退出信息
WIFEXITED(status);若为正常终止子进程返回的状态,则为真!(正常退出)WWXITSTATUS(status);若WIFEXITED非0提取进程的退出码!(获取退出码)
OK,和前面的位运算是一样的,其实这两宏就是通过上面的位运算来获取到的退出信息和退出码!
非阻塞等待介绍
前面已经介绍过了阻塞等待就是父进程在等待某种资源满足(某种条件发生), 阻塞等待期间父进程是不能干其他事情的!那什么又是非阻塞式等待呢?顾名思义就是,不是一直阻塞等待资源的满足!在原先的阻塞期间父进程不能干其他事情,而非阻塞等待在等待资源满足期间(某种条件发生),父进程可以去做其他的事情!
如何实现非阻塞等待?
我们可以通过waitpid的第三个参数来控制非阻塞等待!waitpid的第三个参数是int类型的options;如果options是0则就是阻塞等待,如果不想阻塞等待就使用一个宏WNOHANG(就是不要让当前进程hang住了)!
如果设置了非阻塞等待,此时的waitpid的返回值有三种情况:
pid_t>0表示子进程退出了,并且父进程成功回收了;
pid_t < 0表示等待失败了;
pid_t == 0表示检测成功,但是子进程没有退出,需要下一次继续监测!
OK,直接来个栗子:
OK,本期内容就分享到这里,好兄弟我们下期再见!
结束语:暗夜逐光,孤勇前行!