进程控制知识
进程终止
进程退出有三种场景:
1:代码运行完成,正确运行。
2:代码运行完成,但是失败。
3:代码异常终止。
一般来说,我们通过正常的main函数return,或者使用exit()和_exit()都是可以正常的结束进程的。而异常退出的典型例子就是ctrl+c。我们可以通过观察退出码来知道我们的进程到底是成功退出还是失败退出了。通常,我们认为除了0和1以外的都是失败退出。0是最正常的退出,而1是不被允许的访问,常见的退出码如下:
最简单的读取返回值的方法是echo $?。这个命令可以获取上一次执行的命令是否正确,通过下图我们可以发现,当正确返回的时候,他返回了1;当报错了以后,他就返回了错误码。
_exit(status)退出是最底层的退出手段,status是一个int类型的数据,他的低八位可以被父进程读取。所以如果我们写了_exit(-1)时,在终端执⾏$?发现返回值是255.
exit(status)是在_exit(status)的基础上进行升级,他会在exit的基础上增加两个任务:1.执行用户通过 atexit或on_exit定义的清理函数。2. 关闭所有打开的流,所有的缓存数据均被写⼊。然后再调用_exit。
return 是最基础的方法,在子函数中是return给主函数,而在主函数(main)中则是同义为exit(n),n就是错误码。
进程等待
首先,进程等待是什么?为什么要进程等待?
等待的意义
总而言之,进程等待,本质就是为了防止僵尸进程的产生。子死父不管,僵尸进程就来了,内存也就泄露了。父进程需要知道目前子进程的任务完成情况,是否正确完成,是否正确退出。⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息
进程等待的方法
1.pid_t wait(int* status);
这是一个比较常规的等待方法,如果成功返回等待,则获得子进程的pid;失败返回就返回-1.status是一个整数指针,他会得到返回的状态,如果不关心,可以用NULL.
pid_ t waitpid(pid_t pid, int *status, int options);
waitpid是一种升级的方法,他的第一个参数是pid,如果写-1,表示等待所有子进程,就和wait等价了。如果pid>0的话,那就等待与该pid的进程。
第二个参数是status,这个status里面的内容有很多,我们需要使用一些系统的宏函数才好解决。WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
第三个参数是options,他是决定是否使用非阻塞的方法。为0的话就就是阻塞等待,为WNOHANG的话就是非阻塞等待。其功能和单片机的中断差不多,但是实现逻辑很不一样。他需要使用轮询的思想,最重要的代码如下,本质上是趁着子进程没挂,自己还闲着这段时间,去疯狂的兼职干其他活。
int status = 0; pid_t ret = 0; do { // 1. 抬头看一眼子进程挂没挂 ret = waitpid(-1, &status, WNOHANG); if (ret == 0) { // 2. ret == 0 说明子进程还在开心地狂奔,还没退出呢 printf("child is running\n"); } // 3. 核心:既然子进程没退,我闲着也是闲着,去把我手头的“临时任务”做一遍 handler(); // 4. 判断条件:只要 ret == 0(没抓到退出的子进程),就必须继续循环,继续轮询! } while (ret == 0);进程程序替换
fork() 之后,父子各自执行父进程代码的⼀部分如果⼦进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能。程序替换是通过特定的接口,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中
替换原理
⽤fork创建⼦进程后执行的是和父亲进程相同的程序(但有可能执⾏不同的代码分支),子进程往往要调⽤⼀种 exec 函数以执行另⼀个程序。当进程调用⼀种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
替换函数
有六种以exec开头的文件,可以用于进程替换,其各有不同的用处,但本质都是执行execv。其中,l(list) : 表示参数采⽤列表。v(vector) :参数⽤数组。p(path) : 有 p 自动搜索环境变量 PATH。e(env) : 表示自己维护环境变量。
#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[]);#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); }