程序运行完后可能的情况
正常情况
- 正常执行完了,结果正确
- 结果不正确
异常情况
- 崩溃:进程因为某些原因(越界等),导致进程收到了来自操作系统的信号
- 正常情况
我们之前谈论过main函数的返回值就是退出码,我们判断进程运行完之后结果是否正确就可以通过退出码来知道结果
我们来写段代码来看看退出码
我们知道,进程退出码并不会帮我们打印出来,我们可以通过指令echo $?
来打印进程退出码
可以看到我们运行了许多次打印的都是0,因为我们第一次使用指令打印的是我们运行的可执行程序的退出码,只后每一次打印都是前一次的指令的退出码,因为指令也是一个可执行程序,这是我们之前讲过的
我们可以看到,这些退出码对应的含义,我们是认识见过一些的,比如说前几个大家想必很熟悉,经常见,系统里这些退出码对应的字符串可能不一样,但是基本都有这些字符串
我们知道,return
在main函数中才可以让进程退出,在函数内的return
是不行的,我们可以使用函数exit
来退出
我们可以看到exit
让进程退出了,并且exit
的参数就是退出码
exit
函数在其他函数内部被调用也可以让进程退出
说明在代码的任何地方调用exit
都可以让进程退出
可以看到_exit
函数不仅用法和exit
一样,结果也是一样,那么它们没有区别吗?有的
我们使用printf
打印,然后等待3秒,在用exit
函数退出,这其中因为缓冲区,我们等待的3秒内不会看到字符串被打印出来,在退出的时候才会打印
可以看到我们只是将exit
换成了_exit
其他没有变化,而我们这次并没有打印出字符串
所以我们可以得出结论:_exit
函数不会在退出的时候不会刷新缓冲区,而exit
函数会刷新缓冲区
exit
函数会执行用户定义的清理函数- 冲刷缓冲区,关闭流等
exit
只是将进程退出并返回退出码
exit
是封装了_exit
的函数
进程等待
为什么要进程等待?
- 避免内存泄漏
- 获取子进程执行的结果(如果需要就获取)
进程等待:通过系统调用,获取子进程退出码或退出信号的方式,顺便释放内存
怎么进程等待?- - wait
/waitpid
那么wait
和waitpid
是怎么获取到子进程的退出信息的呢?
这两个函数都是系统调用接口,他们可以通过pid找到子进程,然后去子进程的pcb中获取退出信息
wait
wait
就是等待子进程变成僵尸进程
子进程运行了5秒,这时候都是S状态,之后子进程退出变成Z状态,等了10秒,子进程被回收,只剩下了父进程,然后再等5秒,进程退出,ret就是子进程的pid
waitpid
通过man
手册查看一下waitpid
,我们来解读一下:
-
第一个参数pid就是要你调用的时候传进程的pid过来,传过来的pid大于0就表示等待指定的进程,如果等于-1就表示等待任意一个子进程,与wait等效
-
第二个参数表示状态,是输出型参数,如果传的是
NULL
,表示不关心子进程的退出状态信息,waitpid
会使用位图的方法来通过这个参数输出两个值,一个是子进程的退出状态,一个是终止信号status
是int*
类型的,但是实际上,我们只会使用它的低16比特位,这和我们之前学的权限可以用8进制数字去设置是差不多的在使用的16个比特位之间,高8个比特位会用来表示子进程的退出码,低7位会用来表示终止终止信号,中间有一个比特位是core dump标志,0或1表示是否打开核心转储
- 终止信号为0,表示正常退出,我们再来看退出码
- 不为0,表示进程不是正常退出,我们不用再管退出码了,退出码就没有意义了
我们来截取一下status里的两个参数:
Linux下有一些宏可以获取退出码和终止信号:
WIFEXITED(status)
:获取子进程的终止信号,为真表示正常退出WEXITSTATUS(status)
:提取子进程的退出码
退出码
status >> 8
表示将后16个比特位中的高地址的8位移到最后,变成最后8位,然后再按位与上0xFF,表示获取到最后8位- - 退出码
status
按位与上0x7F表示获取最后的7位 - - 终止信号终止信号
我们在这里加一句错误的代码
可以看到,子进程还没有执行完立马就退出了,父进程通过waitpid
函数收到了子进程的终止信号,这时候子进程的退出码就没有意义了
我们来看一下,我们将代码改成了死循环,然后通过指令去结束子进程,看看可不可以被父进程获取到终止信号
可以看到,父进程仍然可以获取到子进程的终止信号 -
第三个参数
WNOHANG
:让父进程非阻塞等待子进程,设置非阻塞选项,子进程没有退出,父进程就可以脱离阻塞状态,不会一直等待着,可以运行其他代码我们运行刚刚的几段代码可以发现,父进程好像一直都没有退出,在等待子进程退出,那么父进程这段时间在干什么呢?
父进程在使用waipid函数之后,只能一直等待着子进程退出,然后才会执行之后的代码,这种情况叫做阻塞等待
在使用
waitpid
函数的时候,我们可以让父进程不要一直等,可以多次检测子进程是否退出了,这叫做非阻塞轮询,这时候有三种情况:- 子进程还没有结束,父进程可以运行其他代码
- waitpid出错
- 子进程退出,父进程等待结束
那么我们怎么样才能让父进程不要一直等待,可以去做运行其他代码呢?
可以看到,父进程没有一直在等待子进程,父进程经过每次轮询之后,子进程没有退出,就运行自己的代码 -
返回值
如果等待成功,返回子进程的pid,如果我们第三个参数设置的是WNOHANG
,子进程还没有退出就会返回0,出错就返回-1
进程程序替换
用fork创建子进程之后执行的是和父进程相同的程序(有可能执行不同的代码分支)
但是如果我们不想让子进程执行父进程的代码,我们可以使用exec函数:
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
- 如果调用出错则返回-1
- exec函数只有出错的返回值,没有成功的返回值
命名理解
着写函数原型看起来很容易混淆,但是我们只要掌握了规则就很好记
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
execl
int execl(const char *path, const char *arg, ...);
我们可以理解记忆,execl等于exec加上l,l就是list,就是把字符串一个个传过去
第一个参数传的是可执行程序(路径),后面的参数表示如何使用可执行程序(使用方法),**最后一个参数要传NULL
**告知execl
参数传完了
不管是子进程还是父进程使用了execl
函数替换了程序,都不会影响另一个,也就是说,进行了程序替换,就会自动发生写时拷贝,将代码和数据都写时拷贝(代码也会发生写时拷贝)
我们写段代码来看看怎么用的
可以看到,子进程打印了自己的pid,然后也执行了ls -a -l的命令,也就是说,execl
之前的程序不会被替换,会照常运行,execl
之后的程序会被替换(虽然我们这里没有写),父进程是照常运行,并没有被子进程影响
我们看看如果替换失败了,会怎样,返回值是不是-1呢?
我们可以看到,替换失败了之后,返回确实是-1,然后会继续执行后面的代码
execv
int execv(const char *path, char *const argv[]);
理解记忆:exec加上v,v就是vector,c++里的容器,就是一个要传数组
也就是说将我们要在后面要传的放进一个指针数组里
execlp
int execlp(const char *file, const char *arg, ...);
理解记忆:l就是list,p就是PATH环境变量,file就是程序名,不用带路径,系统会自动在PATH中找
execvp
int execvp(const char *file, char *const argv[]);
理解记忆:file就是程序名,v就是vector,传个数组,p就是PATH
execle
int execle(const char *path, const char *arg, ...,char *const envp[]);
理解记忆:path就是路径,e就是自定义环境变量,l是list
自定义环境变量,会覆盖PATH
,变成自己传入的环境变量
我们使用函数putenv
将我们的环境变量加入到我们这个进程的环境变量中,再传进入函数execle
中,这样就不会覆盖PATH
了
execve
int execve(const char *path, char *const argv[], char *const envp[]);
这个系统调用不和上面六个放在一起,这个是真正的系统调用,上面六个是对它的封装