[Linux] 进程等待 | 进程替换

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

主厨:邪王真眼

主厨的主页:Chef's blog

所属专栏:青果大战linux

总有光环在陨落,总有新星在闪烁

我有一个朋友,拿了个国二,还找了个小学妹,被上压力了啦,

我们在上一篇进程退出码提到了退出码,但其实他的相关知识还有一半没讲,因为这个要结合进程阻塞才可以。

进程等待

我们在讲进程状态时就提到了,当子进程结束,如果父进程不对子进程进行回收,那 子进程就会一直处于僵尸状态,现在我们就要开始讲父进程是如何对子进程进行回收的了。

进程回收的必要性

  1. 子进程结束后,如果不回收,就会进入僵尸状态,那么他的一部分内存就无法回收,造成内存泄漏,即便是kill命令也不行,因为你无法杀死一个死掉的进程
  2. 子进程结束后,父进程需要知道子进程是否完成任务,如果失败了,失败原因是什么,这些可以通过回收进程来获取相关信息。

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");
    else
        printf("回收成功了\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");
    else
    printf("回收成功了\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");
    else
        printf("回收成功了\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++

  1. WIFEXITED:检测进程是否正常退出,返回一个布尔值,如果进从正常退出,返回真

  2. 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;
}

举一反三时间

观察三个函数, 我们不难发现这些函数名的含义

  1. 首先都是exec系列,所以前缀都是exec

  2. execl 后缀l(list列表)表示传入的命令行参数argv是以一个个字符串作为参数进行传入的

  3. execlp 后缀p的含义同上,p表示第一个参数不用传路径,直接传文件名

  4. 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,他是一个系统接口,不难想像六个接口都是对该系统接口的封装。

相关推荐
坊钰几秒前
【Java 数据结构】时间和空间复杂度
java·开发语言·数据结构·学习·算法
武昌库里写JAVA6 分钟前
一文读懂Redis6的--bigkeys选项源码以及redis-bigkey-online项目介绍
c语言·开发语言·数据结构·算法·二维数组
禊月初三14 分钟前
LeetCode 4.寻找两个中序数组的中位数
c++·算法·leetcode
儿时可乖了17 分钟前
Linux 定时任务全解析
linux·oracle
北'辰27 分钟前
使用ENSP实现默认路由
运维·网络
学习使我飞升28 分钟前
OSPF路由状态数据库、type 类型、完整的LSA
服务器·网络·智能路由器
北'辰29 分钟前
使用ENSP实现静态路由
运维·网络
学习使我飞升33 分钟前
spf算法、三类LSA、区间防环路机制/规则、虚连接
服务器·网络·算法·智能路由器
hgdlip35 分钟前
重装系统后ip地址错误,网络无法接通怎么办
服务器·网络协议·tcp/ip·重装系统
程序员奇奥43 分钟前
CentOS操作系统下安装Nacos
linux·运维·centos