目录
进程终止
下面要谈的一个话题就是进程终止,就是说一个进程退出了,可能有三种情况
1.进程代码执行完,结果是正确的
2.进程代码执行完,结果是错误的
3.进程代码没有执行完,进程出异常了,中途退出了
其实我们写的main函数执行起来就是一个进程,而我们一般写的return 0,就叫做进程的退出码。一般0表示正确执行,第一种情况;而非0表示执行失败,第二种情况。因为main函数的return 0就已经是代码的最后部分了。
为什么用非0表示执行失败呢?因为成功就是成功了,而失败可能会有很多种原因。进程的退出码是给机器看的,要是给人看,就要把退出码转化成错误描述,这个错误描述可以是系统或语言自带的,也可以自定义,下面我们先用strerror 函数看一下系统中的错误描述
我们可以看到有很多错误描述,到133个了
这时我们就能解释我们瞎给比如ls 后面一个选项,bash进程,就是命令行解释器报的错是什么了,比如:
这不就是上面的二号错误吗,另外,下面的指令可以查看最近一次进程的退出码
为什么第二次用显示0呢?因为echo $?也是一个进程,它是成功执行的
我们上面说也可以自定义,那就可以创建几个字符串枚举值,并且枚举值是可以表示整数的,这样就可以自己去定义错误码了,比如:
上面我们说了前两种情况,第三种情况是进程没有执行完,中途出现异常了,只要中间出现异常,其实结果对与否就没有意义了。其实中途出现异常本质上就是进程收到了异常信号,就是kill -l那一系列的信号
比如:
8号信号对应的是
SIGFPE
(Floating-Point Exception)信号。这个信号用于指示浮点运算异常,比如除以零或溢出等情况。11号信号对应的是
SIGSEGV
(Segmentation Fault)信号。这个信号用于指示进程发生了内存段错误,即试图访问无效的内存地址。
并且有一个细节就是这些错误是从1开始的,这跟我们下面的如何用16比特位表示退出码和收到什么信号是有关系的,并且这些大写的字母都是宏定义。
所以进程执行的情况可以由两个数字表示,一个是收到什么信号(0就表示没有收到信号),一个是退出码。
errno
除了进程退出,就是main函数退出,我们还有普通函数退出 ,那我们如何知道它的运行情况呢?我们也有个存放错误码的东西,叫errno,就是说,函数如果执行失败的话,那么错误码会放在erron这个整形变量里,这个一般库函数才会有这个错误码,因为库函数内部一般是有这个赋值的,我们一般写的函数没有,比如fopen,可以看一下它的返回值
可以看到errno这个东西,下面写一个代码看一下
其实我当前目录根本就没有这个文件,并且是以只读的方式打开文件,所以它肯定会出错,错误信息就存在errno里面,我们也可以看具体字符信息,运行之后就是这样
exit和_exit
我们如果想让一个进程退出可以用exit或_exit ,前者是库函数,后者是系统调用,我们可以来查一下
这里的参数status就是你想让进程退出时的退出码,我们通过一段代码来展示一下它们的区别
我们写这样一个代码,运行完后发现什么都不打印,而把_exit改成exit后就会打印,这就说明exit会刷新缓冲区,其实exit就是封装了_exit,为什么要这样做呢?
其实我们知道库函数和系统调用是上下层关系,不同的操作系统的系统调用是不同的,比如Windows下_exit就用不了,所以这时我们把系统调用再封装一层成为库函数,不同的操作系统封装不同的系统调用,但是它们的库函数的接口就是一样的了,这就通过库函数屏蔽掉了系统调用的差异,就实现了语言的可移植性和跨平台性,所以不同的操作系统就会安装不同的库文件。并且这里的缓冲区是库级别的缓冲区,所以系统调用是无法刷新的,如果是操作系统级别的,那就会刷新,因为操作系统不会让它白白占着空间的。
进程等待
我们之前说过,父进程要回收子进程的PCB来拿到子进程的退出信息 ,如果父进程不管不顾,子进程就会进入僵尸状态 ,就会造成内存泄漏 ,这时就算kill -9也无能为力,因为谁也不能杀死一个已经死掉的进程。而父进程回收子进程就是通过进程等待
wait和waitpid
我们有两种等待方式,分别是wait和waitpid,我们可以man查一下
这里的status是一个输出型参数,通过给一个整型变量的地址,这个函数内部就可以将退出信息写入到这个地址中,如果不想让它写入可以给NULL;pid就是要等待的子进程的pid,如果是-1的话,那么等待任一子进程,这时与wait等效;options是确定父进程是阻塞等待还是非阻塞等待
我们再看一下返回值是什么意思
就是说:如果成功等待到了子进程结束,就返回子进程的PID;如果等待失败(如果pid参数指定的子进程不存在或不是当前进程的子进程或是如果调用被一个信号中断)就返回-1;如果非阻塞等待(WNOHANG)等待后,子进程状态没变,那么返回0。
知道了各个参数和返回值是什么意思,那我们就可以简单的来使用一下wait和waitpid:
我们上面说过,任何进程最终的执行情况可以有两个数字表明,一个是退出码 ,一个是退出信号,如果收到退出信号,那么最终的退出码没有意义。waitpid就是通过status这个输出型参数拿到这两个数字的,那么这两个数字是怎么存在一个status中的呢?我们来看一下:
我们只用status中32个比特位中的低16位,也就是0-15位,如果进程没有收到退出信号,那么8-15位就表示子进程的退出码,所以我上面获取退出码时是先进行位右移8位,然后按位与上0xff;如果收到退出信号,那么0-6位表示收到什么退出信号,第七位表示core dump标志,这个标志先不用管,所以我上面是直接按位与上0x7f。
为了验证,我们也可以故意给一个错误的代码,比如访问空指针,这时让程序运行,看看父进程能否分析出子进程的退出信号
我们可以看到,父进程确实等待到了子进程的退出信号11
宏:WIFEXITED
其实不一定非得像上面那样进行位操作 才可以得到退出码,我们还可以通过宏来得到退出码或退出信号,像下面这样
wait if exited 这个表示如果进程是正常终止,就是没有收到退出信号,那就返回真,所以我们用wait exit status来获取退出码;如果为假,我们用wait terminate signal来获取退出信号。因为只要有退出信号,退出码就没意义,退出码有意义时没有退出信号,所以它们两个只要根据不同情况获取一个即可。
非阻塞等待
我们上面说过waitpid的第三个参数如果是0,那么就是阻塞等待;如果是WNOHANG,就是非阻塞等待,什么是阻塞等待呢?其实就是父进程在等待子进程退出时如果什么都不做,就叫做阻塞等待;如果父进程使用WNOHANG后waitpid后就会立即返回,不管是否有子进程退出,如果等到了子进程退出,返回值就是子进程的pid,如果没有等到就返回0。因为waitpid只会运行一回,有可能等不到子进程,所以一般把它放进一个循环中,并且父进程执行完waitpid后还可以执行自己的工作,我们一般可以这样去实现
进程结束之后只留下进程PCB,所以父进程肯定是通过子进程的PCB获取退出码和退出信号的,我们在Linux源码中也确实可以看到