[Linux] 进程等待 | 进程替换
🪐🪐🪐欢迎来到程序员餐厅💫💫💫
主厨的主页:Chef‘s blog
所属专栏:青果大战linux
我有一个朋友,拿了个国二,还找了个小学妹,被上压力了啦,
我们在上一篇进程退出码提到了退出码,但其实他的相关知识还有一半没讲,因为这个要结合进程阻塞才可以。
进程等待
我们在讲进程状态时就提到了,当子进程结束,如果父进程不对子进程进行回收,那 子进程就会一直处于僵尸状态,现在我们就要开始讲父进程是如何对子进程进行回收的了。
进程回收的必要性
- 子进程结束后,如果不回收,就会进入僵尸状态,那么他的一部分内存就无法回收,造成内存泄漏,即便是kill命令也不行,因为你无法杀死一个死掉的进程
- 子进程结束后,父进程需要知道子进程是否完成任务,如果失败了,失败原因是什么,这些可以通过回收进程来获取相关信息。
wait函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
函数返回值
- 成功:返回被终止子进程的进程ID
- 失败:返回 -1
我愿称之为最朴实无华的回收函数
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
int main(){
pid_t p=fork();
if(p>0){pid_t k=wait(NULL);if(k<0)printf("回收失败了\n");elseprintf("回收成功了\n");while(1){sleep(1);printf("我是父进程%d\n",getpid());}
}
else if(p==0)
{int cnt=5;while(cnt--){printf("我是子进程:%d 我还在运行\n",getpid());sleep(1);
}
}
}
可以看出,wait确实回收了子进程,子进程在结束后不再像之前一样以僵尸状态继续保留 ,而是立刻消失了
但是wait的功能实在是太少了,所以我们不打算对它细讲
waitpid
wait算是waitpid的一个子功能,即回收一个进程,并获取其退出码
waitpid有三个参数,我们一个个说
【pid】
- pid>0,则表示指定waitpid回收该pid的进程
- pid<0:回收任意一个子进程,就像wait
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
int main(){
pid_t p=fork();
if(p>0){pid_t k=waitpid(p,NULL,0);printf("我是父进程\n");if(k<0)printf("回收失败了\n");elseprintf("回收成功了\n");while(1){sleep(1);printf("我是父进程%d\n",getpid());}
}
else if(p==0)
{int cnt=5;while(cnt--){printf("我是子进程:%d 我还在运行\n",getpid());sleep(1);
}
}
}
【返回值】
- 回收成功,他会返回回收的进程的PID
- 回收失败则返回-1
我们现在把要回收的子进程从p改为了p+1,就会导致回收失败。
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
int main(){
pid_t p=fork();
if(p>0){pid_t k=waitpid(p+1,NULL,0);printf("我是父进程\n");if(k==-1)printf("回收失败了\n");elseprintf("回收成功了\n");while(1){sleep(1);printf("我是父进程%d\n",getpid());}
}
else if(p==0)
{int cnt=5;while(cnt--){printf("我是子进程:%d 我还在运行\n",getpid());sleep(1);
}
}
}
【status】
输出型参数,他将存储子进程的退出信息
如果不想接收该信息,就传入空指针即可
我们要用位图的思想去看status,
为什么要这样设置?
进程结束有两种情况
-
正常终止(但最后结果可能不对),退出信息为0,表示无信号异常
-
被信号杀死(没跑到return就挂了),退出码不确定,此时研究它没有意义
为了研究进程到底是处于什么原因结束,我们需要存储这两种信息
同时我们也可以得出status>>8即是退出码,status&07F就是终止信号
代码展示
#include<unistd.h>
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>
int main(){pid_t p=fork();if(p==0){int cnt=5;while(cnt--){sleep(1);printf("子进程PID:%d\n",getpid());}exit(1);}int st=0;pid_t k=waitpid(p,&st,0);printf("回收子进程:%d status:%d :退出码:%d 退出信息 %d\n",k,st,st>>8,st&0x7F);return 1;
}
我们的退出码是return的1,且运行时没有异常所以退出信息为0,与打印结果一致
对于退出信息,我们可以使用kill -l指令查看
例如kill -8表示因为浮点错误(除零)而导致进程被终止
这里我们用kill -8 +PID终止了进程,可以看到退出信息变成了8(因为是8号信息终止),退出码则变成了0,这也说明了当进程因为异常提前终止,退出码就没有意义了
linux给用户提供了宏去检测status
WIFEXITED:检测进程是否正常退出,返回一个布尔值,如果进从正常退出,返回真
WEXITSTATUS:提取子进程的退出码
但是我想吐槽一下,对于英格力士不好的人来说,那些宏的英文真不好记,还不如写个st>>8查看退出码,st&0x7F查看退出信息来的好
非阻塞轮询
waitpid的第三个参数表示等待回收的方法,
- 0表示阻塞等待
- WNOHANG(wait no hang)表示非阻塞等待
阻塞等待:当父进程执行到该语句时,就会检测要等待回收的子进程是否结束了,如果结束了就回收,如果没有就会卡在该语句,一直等待直到等待失败(返回-1),或者子进程结束对其进行回收。
非阻塞等待:当父进程执行到该语句时,也会检测要等待回收的子进程是否结束了,如果结束了就回收并且返回该进程的PID,如果没有就会返回0,不会卡在这里,因此多与while循环搭配
#include<unistd.h>
#include<stdio.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>
int main(){pid_t p=fork();if(p==0){int cnt=5;while(cnt--){sleep(1);printf("子进程PID:%d\n",getpid());}exit(1);}while(1){ int st=0;pid_t k=waitpid(p,&st,WNOHANG);if(k>0){printf("回收子进程:%d status:%d :退出码:%d 退出信息 %d\n",k,st,st>>8,st&0x7F);break;}else if(k==0){printf("子进程没结束,在等等\n");}sleep(1);}return 1;
}
可以看到父进程的waitpid在子进程没有执行完时,并没有被卡住,而是继续执行后面的语句了
进程替换
请注意,这个函数非常重要!!我们下次要手写一个shell外壳,就需要用到它,所以请读者认真阅读
我们也算是比较了解fork函数了,fork的作用是创建一个子进程,我们可以利用这点让父子进程执行不同的代码,来满足不同的需求,但是子进程的代码时拷贝的父进程的,如果我们想让子进程执行某种代码,就必须把代码写在父进程中,再用if else区分fork返回值进行区分才可以。那假如我想在代码中使用ls ,pwd这种指令真么办;我想在这个文件的代码中运行别的文件的代码怎么办,把那些代码都拷贝进来?那实在太麻烦了,于是进程替换闪亮登场解决了这个问题
exec系列接口一共有6个,我们可以输入 man -3 exec查看
execl
【返回值】
- 进程替换失败返回-1
- 成功则没有返回值
第一个参数,表示要执行的文件的路径,可以是绝对路径也可以是相对路径。
第二个参数,命令行参数中的argv,以列表(list)的形式传参,记得要以nullptr结尾
#include<iostream>
#include<unistd.h>
using namespace std;
int main(){execl("/bin/ls","ls","-a","-l",nullptr);return 0;
}
这样一看貌似就是执行了该指令而已,但其实不只是这样,请看下面的代码
#include<iostream>
#include<unistd.h>
using namespace std;
int main(){execl("/bin/ls","ls","-a","-l",nullptr);printf("明天没早八!!\n");return 0;
}
是的,你没有看错,本该在exec函数后执行的printf语句没有执行!
这里就涉及exec的原理了。
事实上,执行到exec函数时,会把第一个参数对应的代码和数据加载进内存,并且直接覆盖掉原来的代码和数据,因此在exec之后的代码是不可能被执行的 。
这也是为什么exec成功后没有返回值,因为没有意义!毕竟后面的代码都被覆盖了。
#include<unistd.h>
#include<bits/stdc++.h>
using namespace std;
int main(){printf("我是exec后的进程,我的PID是%d",getpid());
}
#include<iostream>
#include<unistd.h>
#include<unistd.h>
using namespace std;
int main(){printf("我是exec之前的进程,我的PID是%d\n",getpid());execl("./test5","test5",nullptr);return 0;
}
但是请注意,虽然代码和数据都被修改了,但是进程还是那个进程,不信我们可以用PID验证
execlp
int execlp(const char* file, const char* arg, ... /* (char *) NULL */);
与execl相比,只是修改了第一个参数,从要求传递路径,变成了要求传递文件名,
这就是告诉我们,不用再传路径了,把要执行的文件名传进来,至于他的路径,会在PATH的环境变量中查找,如果找得到就执行,找不到就无法执行。
#include<iostream>
#include<unistd.h>
#include<unistd.h>
using namespace std;
int main(){execlp("pwd","pwd",nullptr);return 0;
}
execle
可以指定替换后的进程的环境变量
#include<unistd.h>
#include<bits/stdc++.h>
using namespace std;
int main(int argc,char*argv[],char*env[]){
int i=0;
while(env[i])cout<<env[i++]<<endl;
}
#include<iostream>
#include<unistd.h>
#include<unistd.h>
using namespace std;
int main(){char*const env[]={(char*) "A=111",(char*) "B=222",(char*) "LINUX",NULL};execle("./test5","./test5",NULL,env);return 0;
}
举一反三时间
观察三个函数, 我们不难发现这些函数名的含义
-
首先都是exec系列,所以前缀都是exec
-
execl 后缀l(list列表)表示传入的命令行参数argv是以一个个字符串作为参数进行传入的
-
execlp 后缀p的含义同上,p表示第一个参数不用传路径,直接传文件名
-
execle 后缀e表示可传入环境变量
在此基础上,对剩下三个进行分析
【execv】
和后缀l相对,后缀v(vector)这个表示传入命令行argv是以指针数组的形式传入的
#include<iostream>
#include<unistd.h>
#include<unistd.h>
using namespace std;
int main(){char*const argv[]={"ls","-a","-l",nullptr};execv("/bin/ls",argv);return 0;
}
【exevp】
后缀v表示传argv是以指针数组传参,p表示第一个参数不传路径而传文件名
【execvpe】
后缀v、p、e,读者不妨自己想想作用是什么
除了以上六个由语言封装的函数,还有一个execve,他是一个系统接口,不难想像六个接口都是对该系统接口的封装。