进程退出
进程的退出一共有三种场景。
- 程序跑完,结果正确,正常退出。
- 程序跑完,结果不正确,正常退出。
- 代码没有跑完,程序异常退出。
所以在Linux中我们可以通过 echo $? 查看最近一个进程的退出码。
程序正常退出的方法
- 从main返回
- 调用exit
- _exit
我们平时在main中return的那个数字,就是我们的退出码,并且exit和_exit都有一个整数参数,那个整数参数也是退出码。所以说我们让子进程去执行某项任务,它执行的怎么样我们就可以通过退出码来进行判断。一般来说0表示的是成功,非0就代表出错的原因,数字可以说明出错的原因,但是不利于我们人进行阅读,所以每个数字在C语言中还对应的有一个字符串,表示该数字的错误原因,这个原因我们也可以自己自定义。C语言中有一个全局的变量errno,它里面存的是最近一个函数调用发生错误的错误码。
错误码vs退出码
错误码通常是表示一个函数或者系统调用的情况。
推船一般是一个进程退出的时候,它的退出结果。
当失败时,它们就用来衡量函数、进程的出错详细原因。如果一个进程异常的话,那么它就会收到信号,所以是否收到信号可以用来衡量一个进程是否异常。所以一个信号数字和一个退出码就可以来判断一个进程是否正常退出,如果收到了信号,那么就说明异常了,一个异常的进程我们是不关心它的退出结果的,只有进程没有收到信号,并且返回值是0,那么这个进程就是正常的并且是成功的。
我们上面提到了exit和_exit的方法,那么这两个方法有什么区别呢?
最明显的区别就是exit是C语言的一个函数,而_exit是系统调用,而exit在底层也是封装了_exit的,
其次就是exit在退出的时候会刷新缓冲区,但是_exit在退出的时候不会刷新缓冲区,所以我们可以得出一个结论,我们平时在C语言中说的缓冲区一定不在系统中,它是一个语言自己封装的缓冲区。
进程等待
什么是进程等待?
进程等待就是通过wait/waitpid的方式,让父进程对子进程进行资源回收的等待过程。
为什么要进行等待?
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
等待的方法
- wait
- waitpid
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
我们可以看到这两个函数都可以获取进程的退出信息,是一个整数,但是这个整数如何表示两个数字,信号和退出码的呢?这个整数不能整体看到,一般来说0~6和比特位是信号,第7个比特表示core dump,8 ~15表示退出码,后面16个比特位我们不管。
进程程序替换
我们父进程所创建的子进程执行的代码一般都是父进程代码的一部分,如果想让子进程执行全新的代码,就可以使用进程替换,所以进程替换就是让进行程序替换的进程执行全新的代码访问全新的数据,不和父进程有任何的瓜葛。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
磁盘中的可执行程序中的ELF中是保存了可执行程序的入口地址,所以不用担心替换后的进程没法找到入口地址的问题。程序替换有6种函数。
#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[]);
前5个都是普通函数,第六个是系统调用,前面5个在底层本质也是封装了最后一个。
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
这些函数都是exec开头的,作用都一样,只不过参数不一样可以应对各种场景。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}