【Linux】进程控制(shell的模拟实现)

目录

一.进程终止

进程退出 

 exit(int status)和_exit(int status)

exit的方式退出 

 _exit的方式退出

退出码 

二.进程等待

进程等待方法 

wait

waitpid 

非阻塞等待 

三.进程替换 

execl 

execv 

execle 

四.shell模拟实现

一.进程终止

什么是进程终止,这应该很好理解,就是一个进程结束了,但是一个进程结束又分为三种情况

1.代码跑完了,结果正确

2.代码跑完了,结果错误

3.代码没跑完,直接崩溃了


对于第三种情况来说,我们很好判断,但是我们怎么判断第一种和第二种情况呢,一段代码跑完了,我怎么知道结果正确呢还是错误呢? 


在解释之前,我们来思考一个问题,当我们在写一个C/C++程序的时候,在main函数的末尾总要写一个return 0

这个return 0到底是什么,为什么要写return 0,对于其他函数的调用来说,写一个return,很好理解,因为return可以帮我们把想要的结果带回来, 同样的,main函数中的return也有这样的用处,main函数的return可以把程序的结果正确性反馈给我们,具体怎么做呢,看下面的代码。

 1 #include<stdio.h>
  2 
  3 int main()
  4 {
  5     int begin=1;
  6     int end=100;
  7     int count=0;
  8     int i=0;
  9     for(i=begin;i<end;i++)                                                                                                                                                                    
 10     {
 11         count+=i;
 12     }
 13     if(count==5050)
 14     {
 15         return 0;
 16     }
 17     else
 18     {
 19         return 1;
 20     }
 21 }

在这段代码中,我写了一个从1加到100的程序,但是当程序执行完后,我需要知道我写的程序是否正确,即最终结果是否是5050,所以这个时候我需要用到return来判断,如果结果正确,则返回0,错误返回1,通过这样的方式,可以判断我们所写的程序的运行结果是否正确。

那这个结果又如何判断呢查看呢?

当我们的程序运行完后,可以通过下面这命令来获取程序的运行结果 

echo $?

全过程如下图所示。

echo $?

这个命令是将最近一次运行的程序的程序的退出码打印出来 

进程退出 

在上面我们提到了程序的退出码,那么程序的退出码又是什么呢?

其实程序的退出码就是我们上面所说的程序退出结果是否正确。


进程退出的方式又有三种

1.通过main函数的return退出

2.在任意地方调用exit()退出

3.在任意地方调用_exit()退出


对于第一种方法,我们已经讲解过了,那些现在来看一下第二、三种方式,在任意地方调用exit(),_exit(), 

 exit(int status)和_exit(int status)

 

exit()和_exit(),它们的作用是立即终止当前进程,同时参数中的status就是所谓的退出码,当进程通过exit或者_exit来退出的时候,其程序的退出码就是参数status

那么exit_exit有什么区别呢?

1.exit是C语言库所提供的函数,_exit是系统所提供的接口。

2.exit会主动刷新缓冲区等操作,而_exit不会。


对于区别1,这个应该很好理解,那区别2呢?我们来看一下代码来解释。

exit的方式退出 

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     printf("hello world");
  8     exit(1);                                                                                                                                                                                  
  9 }

对于exit的方式退出,我们可以看到,这是一个正常的结果,把hello world打印出来后再退出的。 


 _exit的方式退出

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     printf("hello world");
  8     _exit(1);                                                                                                                                                                                 
  9 }

对于_exit的退出方法,可以看到,并没有把hello world打印出来。 


所以exit_exit的区别是会不会刷新缓冲区,exit会把缓冲区的东西全部刷新出来打印到屏幕了,而_exit不会,但是远不止这样的区别,exit还会执行用户自定义的清理函数和关闭流等操作。

同时exit和_exit是一个包含与被包含的关系,其实在exit中,是通过调用_exit来实现的,只不过在调用之前会先进行缓冲区的刷新执行用户自定义的清理函数关闭流操作等等。

总的来说,一般都是使用exit来终止我们的进程的。

退出码 

在上面,我们解释可以通过退出码来判断一个程序是否运行正确,同时也讲到了程序退出的三种办法,但是现在有一个问题,程序的退出码都是数字,那这些数字都代表什么呢?

退出码0代表结果正确

退出码非0代表结果错误

但是非0的数字有很多,且这些数字都代表什么错误呢?

别怕,我们来写一个程序将所有的退出码的退出信息打印出来。

在C语言库函数中,有这么一个函数:strerror(),这个函数可以将相应退出码转换为对应的退出信息。

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<string.h>
  5 int main()
  6 {
  7     int i=0;
  8     int j=150;
  9     for(i=0;i<j;i++)                                                                                                                                                                          
 10     {
 11         printf("退出码:%d,退出信息:%s\n",i,strerror(i));
 12     }
 13     return 0;
 14 }

运行这段代码,就可以将所有的退出码所对应的退出信息打印出来,如下图所示。 

 

这里的退出码信息很多,有133条,这里就不全截图了。 

二.进程等待

什么是进程等待?为什么要有进程等待?

进程等待是指父进程等待子进程的一种行为,那么为什么父进程要等待子进程呢?

首先我们要先知道,在一个父进程中,它是可以创建一个子进程的,一般来说,当这个子进程结束的时候,父进程回收子进程相应的资源,但是如果子进程运行时间比父进程长,导致父进程提前结束而被它的父进程回收了资源,那么此时的子进程是一种孤儿进程,当子进程运行结束的时候,由于没有父进程回收资源,则子进程会陷入一种僵尸状态

一旦一个进程变成了僵尸状态,那么就没有办法可以将它杀死,用kill -9也没有办法,则这个僵尸状态的进程就会占用系统资源导致内存泄漏的问题。

所以进程等待的必要性是避免这种情况发生,同时还有另外一个原因,在上面我们提到,当一个程序结束的时候,我们需要知道这个程序的退出信息,这个程序的退出信息会传递给它的父进程,如果它的父进程比它先走,那么这个退出信息又怎么办呢?

所以这个时候就要有进程的等待。


所以要进程等待的两个原因是:

1.父进程要回收子进程的资源

2.父进程要获取子进程的退出信息

进程等待方法 

那么既然要有进程的等待,那么具体怎么做呢?

在linux系统中,提供了几个系统接口给我们用于进程的等待,如下图所示: 

wait

wait是最简单的等待方法,其中这个函数的返回值是所等待进程的pid,status参数是一个int类型的指针,这个指针所指向的内容存储着所等待进程的退出信息

具体用法,如下所示:

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<unistd.h>
  5 #include<stdlib.h>                                                                                                                                                                            
  6 int main()
  7 {
  8     pid_t id=fork();//创建子进程
  9 
 10     if(id==0)//说明是子进程
 11     {
 12         int cnt=5;
 13         while(cnt--)
 14         {
 15             printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());
 16             sleep(1);
 17         }
 18         exit(10);//进程退出
 19     }
 20     else if(id>0)//说明是父进程
 21     {
 22         int status=0;
 23         pid_t ret=wait(&status);
 24         printf("父进程等待成功,等待进程的id是:%d\n",ret);
 25     }
 26 
 27     return 0;
 28 }

运行结果:

waitpid 

通过查手册可以看到waitpid有三个参数

pid_t waitpid(pid_t id,int* status,int option) 

其中,id为需要等的进程的id,option以什么方式等待,status保存着所等进程的退出信息,option阻塞等待非阻塞等待两种方式,在这里我们先以阻塞等待方式来进行演示。

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<unistd.h>
  5 #include<stdlib.h>
  6 int main()
  7 {
  8     pid_t id=fork();//创建子进程
  9 
 10     if(id==0)//说明是子进程
 11     {
 12         int cnt=5;
 13         while(cnt--)
 14         {
 15             printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());
 16             sleep(1);
 17         }
 18         exit(10);//进程退出
 19     }
 20     else if(id>0)//说明是父进程
 21     {
 22         int status=0;
 23         pid_t ret=waitpid(id,&status,0);//0就是阻塞等待的意思
 24         printf("父进程等待成功,等待进程的id是:%d,其退出信息是:%d\n",ret,status);                                                                                                          
 25     }
 26 
 27     return 0;
 28 }

 运行结果:

 通过运行结果我们可以看到,与wait的方式大差不差,但是输出的退出信息却是2560,而我们所写的子进程的退出码是10,就是exit(10)那一句。为什么会这样呢?

那是因为status不是我们所想的那样只是一个单独的整形,它是一个位图结构,具体我们看图

对于这个status,我们只关心它的前16位,其中在这16位中,它的后八位记录着这个进程的退出状态(退出码)前7位记录着这个进程所收到的终止信号。具体这么做呢,我们来看代码。

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<unistd.h>
  5 #include<stdlib.h>
  6 int main()
  7 {
  8     pid_t id=fork();//创建子进程
  9 
 10     if(id==0)//说明是子进程
 11     {
 12         int cnt=5;
 13         while(cnt--)
 14         {
 15             printf("我是子进程,我的pid是:%d,我的ppid是:%d\n",getpid(),getppid());
 16             sleep(1);
 17         }
 18         exit(10);//进程退出
 19     }
 20     else if(id>0)//说明是父进程
 21     {
 22         int status=0;
 23         pid_t ret=waitpid(id,&status,0);//0就是阻塞等待的意思
 24         printf("父进程等待成功,等待进程的id是:%d,其退出状态是:%d,收到的信号是:%d\n",ret,(status>>8)&0xff,status&0x7f);//通过位运算,将其退出状态和信号取出来                                                                  
 25     }
 26 
 27     return 0;
 28 }

 运行结果

非阻塞等待 

在上面的waitpid中,我们默认用的是阻塞等待,但是这里还有一个等待方式:非阻塞等待,那么这两个等待方式又有什么区别呢?

阻塞等待:当子进程没有退出的时候,则父进程进行一个干等,直到子进程退出了,才等待成功,在这个等待的过程中,父进程是不执行其他任何操作,只是干巴巴的等待子进程的退出。

非阻塞等待: 不管子进程有没有退出,父进程只要等待了,就等待成功了,换句话说,就是阻塞等待期间,父进程不能执行其他任何操作,而非阻塞等待期间,父进程可以执行其他操作,这种操作也称为轮询


这句话可能很难理解,我们来看一下代码。

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<unistd.h>
  5 #include<stdlib.h>
  6 int main()
  7 {
  8     pid_t id = fork();//创建子进程
  9     if(id == 0)//说明是子进程
 10     {
 11         int cnt1=9;
 12         while(cnt1--)
 13         {
 14             printf("子进程正在运行\n");
 15             sleep(1);
 16         }
 17     }
 18     else if(id > 0)//说明是父进程 
 19     {
 20         int status = 0;
 21         while(1)//进行轮询操作
 22         {
 23             pid_t ret = waitpid(id,&status,WNOHANG);//WHOHANG代表非阻塞等待
 24             if(ret == 0)                                                                                                                                                                      
 25             {
 26                 //说明等待成功,但是子进程没有退出
 27                 printf("等待子进程成功,子进程没有退出,还在运行\n");
 28             }
 29             else if(ret > 0)
 30             {
 31                 //说明等待成功,子进程退出了
 32                 printf("等待子进程成功,子进程退出\n");
 33                 break;
 34             }
 35             else
 36             {
 37                 //说明等待失败
 38                 printf("等待子进程失败\n");
 39                 break;
 40             }
 41             sleep(3);
 42         }
 43     }
 44     return 0;
 45 }

运行结果:

从代码和运行结果我们可以看到,与阻塞等待不同的是,非阻塞等待是一个一瞬间的过程,只要等待成功了,不管子进程是否退出,等待都结束,但是我们通过一个while循环,就可以达到一直循环等待的操作,这种操作叫做轮询,在轮询的期间,父进程可以执行其他任务,这是阻塞等待做不到的。

下面来演示一下非阻塞等待期间,父进程执行其他的任务。 


 如下图代码所示,在父进程等待子进程的期间,父进程去执行别的任务。

#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>

typedef void (*pfunc)();//typedef函数指针

pfunc arr[4];//定义函数指针数组

void Task1()//任务1
{
    printf("正在执行任务1\n");
    return;
}

void Task2()//任务2
{
    printf("正在执行任务2\n");
    return;
}

void Task3()//任务3
{
    printf("正在执行任务3\n");
    return;
}

void LoadTask()//加载任务
{
    arr[0]=Task1;
    arr[1]=Task2;
    arr[2]=Task3;
    arr[3]=NULL;
}

int main()
{
    LoadTask();//加载任务
    pid_t id = fork();//创建子进程
    if(id == 0)//说明是子进程
    {
        int cnt1=9;
        while(cnt1--)
        {
            printf("子进程正在运行\n");
            sleep(1);
        }
    }
    else if(id > 0)//说明是父进程 
    {
        int status = 0;
        while(1)
        {
            pid_t ret = waitpid(id,&status,WNOHANG);//WHOHANG代表非阻塞等待
            if(ret == 0)
            {
                //说明等待成功,但是子进程没有退出
                printf("等待子进程成功,子进程没有退出,还在运行\n");
                int i=0;
                for(i=0;i<3;i++)//遍历任务
                {
                    arr[i]();
                }
            }
            else if(ret > 0)
            {
                //说明等待成功,子进程退出了
                printf("等待子进程成功,子进程退出\n");
                break;
            }
            else 
            {
                //说明等待失败
                printf("等待子进程失败\n");
                break;
            }
            sleep(3);
        }
    }
    return 0;
}

运行结果:

三.进程替换 

在上面我们所讲到的父进程创建子进程中,都是子进程执行父进程的一部分代码,那么能不能创建一个子进程去执行磁盘中其他的代码了,答案是可以的,这个过程叫做进程替换

在C语言库函数中,提供下面一系列的函数来给我们使用去进行进程替换。

可以看到有很多函数,但是不要怕,其实你只会其中一个,其他的也就很容易了,我们拿第一函数来举一个例子。

execl 

我们先写两份代码,其中在myexec.c中,我们通过进程的替换将子进程替换成./mybin这个程序。

mybin.c

#include<stdio.h>

int main()
{
    printf("我是被替换的进程\n");
    return 0;
}

myexec.c

#include<stdio.h>
#include<unistd.h>

int main()
{
    pid_t id = fork();//创建子进程
    if(id == 0)//说明是子进程
    {
        //在子进程中进行进程的替换
        execl("./mybin","./mybin",NULL);//将子进程替换成./mybin,并且也./mybin的方式执行    
    }
    else if(id > 0)//说明是父进程
    {

    }
    sleep(1);
    printf("进程替换结束\n");
    return 0;
}

在myexec.c中,我们通过execl来进行一个子进程的替换,其中,在这个函数中,要传递的参数如上,第一个参数是你要替换程序的路径,这个路径可以是绝对路径,也可以是相对路径,后面的参数是你要用什么样的方式去执行最后一定要加一个NULL

这就是进程的替换。 

当我们的代码运行起来的时候,我们创建的子进程就会去执行./mybin的代码


那么在exec一类的函数中,有那么多个,这些又有什么区别呢? 

我来挑一些演示一下。 

execv 

#include<stdio.h>
#include<unistd.h>

int main()
{
    pid_t id = fork();//创建子进程
    if(id == 0)//说明是子进程
    {
        //在子进程中进行进程的替换
        char* const  _argv[]={(char*)"./mybin",(char*)NULL};
        execv("./mybin",_argv);//将子进程替换成./mybin,并且也./mybin的方式执行    
    }
    else if(id > 0)//说明是父进程
    {

    }
    sleep(1);
    printf("进程替换结束\n");
    return 0;
}

execle 

这个函数的进程替换的作用是,将我们的环境变量也传过去。如下面所示: 

myexec.c 

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>

int main()
{
    
    printf("父进程running......\n");
    pid_t id = fork();

    if(id==0)//子进程
    {
        char* const env_[]={(char*)"MYPATH=111222333",NULL};
        execle("./mybin","./mybin",NULL,env_);
        exit(1);
    }

    return 0;
}

 mybin.c

#include<stdio.h>
#include<stdlib.h>

int main()
{
    printf("SHELL:%s\n",getenv("SHELL"));//获取环境变量
    printf("HOME:%s\n",getenv("HOME"));
    printf("MYPATH:%s\n",getenv("MYPATH"));
    printf("替换进程!!!\n");
    printf("替换进程!!!\n");
    printf("替换进程!!!\n");
    printf("替换进程!!!\n");
    printf("替换进程!!!\n"); 
    return 1;
}

运行结果:

通过代码和结果,我们可以看到当使用execle的时候,可以将我们自定义的环境变量传递过去


同时进程的替换不仅可以替换相同类型的语言程序,也能替换其他类型语言的程序,也就是说在我们的程序中,你可以去替换python的程序以及所有后端语言的程序。 

四.shell模拟实现

当进程替换学会了,我们就可以来实践一下了,其实在我们的命令行解释器shell中就有用到进程的替换,我们可以来模拟实现一下。

代码如下: 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>

#define NUM 100

int main()
{
    while(1)
    {
        //输出
        printf("[用户名@主机名 当前路径]");//输出提示符
        fflush(stdout);//刷新缓冲区

        char linemanned[NUM];//定义数组存储我们输入的命令
        char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);//将命令输入到linemanned数组中
        linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n
        
        //ls -a -l  假设我们输入的名字是这样的,所以要进字符串分割
        char* arr[NUM];//定义一个指针数组存储分割后的字符串
        int i=0;
        arr[i]=strtok(linemanned," ");
        i++;
        while(arr[i-1]!=NULL)
        {
            arr[i]=strtok(NULL," ");
            i++;
        }
        arr[i]=NULL;

        int id = fork();//创建子进程来运行我们的命令
        if(id == 0)//说明是子进程
        {
            execvp(arr[0],arr);//进行进程替换去运行我们的命令
            exit(1);//如果子进程创建失败,则用exit退出,且退出码为1
        }

        int ret = waitpid(id,NULL,0);//进行一个进程的等待
    }
    return 0;
}

当代码运行起来的时候,我们就可以用我们自己的命令行解释器了。如下图所示: 

 

但是我们的这个命令行解释器还不完善,比如说,当我们使用cd命令来切换路径的时候,是做不到的,为什么呢?

在讲解之前,我们先来理解一下什么是当前路径


当前路径可以理解为当前进程的工作目录,在我们没有输入命令之前,当前的进程是我们的命令行解释器shell,所以当前路径可以理解为当前shell的工作目录,当我们进行cd命令的时候,其实是在改变shell的工作目录,但是在我们上面的代码中,我们是通过创建一个子进程来运行我们的命令,所以当我们使用cd命令的时候,其实是在改变这个子进程的工作目录不是shell的工作目录,所以固然达不到我们想要的效果。


所以要想实现cd命令,则需要shell亲自去执行(这种命令也叫内建命令/内置命令),而不是派子进程去执行,同时在这里要用到chdir这个函数。

这个函数的作用是改变当前工作目录,所以我们的代码要进行修改,修改完成后如下:

int main()
{
    while(1)
    {
        //输出
        printf("[用户名@主机名 当前路径]");//输出提示符
        fflush(stdout);//刷新缓冲区

        char linemanned[NUM];//定义数组存储我们输入的命令
        char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);//将命令输入到linemanned数组中
        linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n
        
        //ls -a -l  假设我们输入的名字是这样的,所以要进字符串分割
        char* arr[NUM];//定义一个指针数组存储分割后的字符串
        int i=0;
        arr[i]=strtok(linemanned," ");
        i++;
        while(arr[i-1]!=NULL)
        {
            arr[i]=strtok(NULL," ");
            i++;
        }
        arr[i]=NULL;

        //进行判断是否cd命令
        if(arr[0]!=NULL && strcmp(arr[0],"cd") == 0)
        {
            if(arr[1]!=NULL)
            {
                chdir(arr[1]);
            }
            continue;
        }

        int id = fork();
        if(id == 0)//说明是子进程
        {
            execvp(arr[0],arr);
            exit(1);//如果子进程创建失败,则用exit退出,且退出码为1
        }

        int ret = waitpid(id,NULL,0);//进行一个进程的等待
    }
    return 0;
}

此时我shell就能实现cd的功能了,但现在的shell功能还不是很完善,我在这里再改进一下,就不过多讲解了。

增加了一个颜色的实现以及获取子进程的退出信息,并稍作修改。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>

#define NUM 100

char linemanned[NUM];//定义数组存储我们输入的命令
char* arr[NUM];//定义一个指针数组存储分割后的字符串

int lastcode = 0;//存储退出码
int lastsign = 0;//存储信号
int status = 0;//存储子进程的退出信息

int main()
{
    while(1)
    {
        //输出提示符
        printf("[用户名@主机名 当前路径]");
        fflush(stdout);//刷新缓冲区

        //将我们在控制台输入的命令  输入到  linemanned数组中
        char* in = fgets(linemanned,sizeof(linemanned)-1,stdin);
        linemanned[strlen(linemanned)-1] = 0;//去掉末尾\n
        
        //ls -a -l  假设我们输入的命令是这样的,所以要进行字符串分割
        int i=0;
        arr[i++]=strtok(linemanned," ");//使用strtok函数来完成分割
        
        if(strcmp(arr[0],"ls") == 0)//如果此时的命令是ls命令,则我们给它带上颜色显示
        {
            arr[i++]="--color=auto";
        }

        //继续进行字符串分割
        while(arr[i-1]!=NULL)
        {
            arr[i++]=strtok(NULL," ");
        }
        arr[i]=NULL;

        //进行判断是否cd命令
        if(arr[0]!=NULL && strcmp(arr[0],"cd") == 0)
        {
            if(arr[1]!=NULL)
            {
                chdir(arr[1]);
            }
            continue;
        }
        
        //创建子进程执行我们的命令
        int id = fork();
        if(id == 0)//说明是子进程
        {
            execvp(arr[0],arr);
            exit(1);//如果子进程创建失败,则用exit退出,且退出码为1
        }
        
        //子进程运行完成后,要进行一个进程的等待来获取子进程的运行结果
        int ret = waitpid(id,&status,0);
        lastcode = (status>>8) & 0xff;
        lastsign = status & 0x7f;
    }
    return 0;
}
  • 16
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值