如何编写自己的shell?(附进程控制详细讲解)

关于进程创建、进程等待、进程终止:

1.进程创建

我们使用fork函数创建子进程,fork函数有两个返回值,一是父进程返回子进程pid,二是子进程返回0

子进程和父进程代码共享,当父子进程有一方需要写入时,操作系统采用写时拷贝,单独开辟一份空间,以供写入,否则共享数据

父子进程拥有独立的地址空间。

关于fork和vfork的区别:

vfork用于创建一个子进程,其子进程和父进程共享地址空间,而fork的子进程具有独立的地址空间。

vfork保证子进程先运行,在子进程调用exec或者exit之后父进程才可能被调度运行。

fork创建进程具体代码实现:


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int main()
{
    pid_t pid;
    printf("before: pid is %d\n",getpid());
    if((pid = fork()) == -1)//fork()子进程返回0,父进程返回子进程pid
    {   
        perror("fork");
        exit(1);
    }   
    printf("after:pid is %d,fork return is %d\n",getpid(),pid);
    sleep(1);
    return 0;
}

vfork创建进程具体代码实现:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int main()
{
    int flag = 100;
    pid_t pid = vfork();
    if(pid == -1) 
    {   
        perror("vfork()");
        exit(1);
    }   
    if(pid == 0)//child
    {   
        flag = 200;
        printf("child flag is %d\n",flag);
        exit(0);
    }   
    else//parent
    {   
        printf("parent flag is %d\n",flag);
    }   
    return 0;
}

由结果可见:子进程直接改变了父进程的变量值,那是因为子进程在父进程的地址空间中运行。

2.进程等待

进程等待的方式有两种:

# 阻塞式等待:子进程没有退出时,父进程一直等着子进程退出

## 非阻塞式等待(轮询式等待)条件不满足时,父进程会返回去做其他事情。

有没有考虑过为什么要进行进程等待呢?

# 子进程退出,如果父进程不及时回收子进程,就会造成子进程变成僵尸进程,从而造成内存泄露

## 父进程派给子进程的任务完成的如何,我们需要知道,例如:子进程运行完成,结果是对还是不对,或者是否正常退出。

### 父进程通过进程等待的方式,回收子进程,获取子进程的退出信息。

进程等待的方法有以下几种:

1.wait方法:

wait方法
    调用方法:
     #include<sys/types.h>
     #include<sys/wait.h>
     pid_t wait(int* status);
返回值:成功返回被等待进程pid,失败返回-1
参数status:输出型参数,获取子进程退出状态,不关心子进程的退出原因时可设置为NULL

附具体代码实现:

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h> //exit
 
int main()
{
    pid_t id = fork();
    if(id == 0)//child
    {   
        int count = 5;
        while(count--)
        {   
            printf("heelo %d,pid is %d\n",count,getpid());
            sleep(2);
        }   
    }   
    else if(id > 0)//father
    {   
       int st; 
       int ret = wait(&st);
       if(ret>0 && (st&0X7F)==0)//正常退出(status参数的低8位未收到任何信号)
       {   
            printf("child exit code:%d\n",(st>>8)&0XFF);//让st的次低8位与8个比特位均为1按位与,得到退出码
       }
       else if(ret > 0)
       {
            printf("sig code:%d\n",st&0X7F);//让st的低7位分别与1按位与,得到终止信号
       }
    }
    else
    {
        perror("fork");
        exit(1);
    }
    return 0;
}

  当程序自己执行完成,程序正常退出,收到0号退出码。

        当在另一个终端kill该进程,程序异常退出,收到15号SIGTERM程序结束信号。与SIGKILL不同的是该信号可以被阻塞和处理,通常用来要求程序正常退出。

        下面测试的是,利用9号信号杀死该进程,程序异常退出,收到9号信号。

2.waitpid方法

waitpid方法
调用方法:
     pid_t waitpid ( pid_t pid, int* status, int options );
参数 :
     pid : pid = -1,等待任一个子进程,与wait等价;
           pid > 0,等待进程ID与pid相等的子进程;
     status : WIFEXITED (status):若为正常终止子进程的返回状态,则为真;(查看进程是否正常退出)
               WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码;(查看进程退出码)
     options :(默认为0)
     WNOHANG:若pid指定的子进程没有结束,则waitpid( )函数返回0,不予以等待。若正常结束,则返回该子进程的pid。
    当正常返回时,waitpid返回收集到的子进程的pid;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。 

具体代码实现如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
 
int main()
{
    pid_t id = fork();
    if(id < 0)
    {   
        perror("fork");
        return 1;
    }   
    else if(id == 0)//child
    {   
        printf("child is run,pid is %d\n",getpid());
        sleep(5);
        exit(3);
    }   
    else//parent
    {   
        int status = 0;
        pid_t ret = waitpid(-1,&status,0);//阻塞式等待
        printf("I am waiting\n");
        if(WIFEXITED(status) && ret == id)//WIFEXITED(status)为真且正常返回才进入该条件语句
        {   
            printf("wait success,child return code is %d\n",WEXITSTATUS(status));//WEXITSTATUS(status))查看进程的退出码
        }   
        else
        {   
            printf("wait child failed,return \n");
            return 2;
        }   
    }   
    return 0;
}

 让程序运行,自己退出。因为子进程用exit(3)退出,所以SEXITSTATUS(status)非零,提取出子进程的退出码3。

       当我在另一个终端下,kill掉该进程,子进程异常退出,所以WIFEXITED(status)不为真,所以父进程中走else语句,输出结果如下:

以下是waitpid函数的非阻塞式等待实现:


#include <stdio.h>
#include <unistd.h>//fork
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
 
int main()
{
    pid_t id = fork();
    if(id < 0)
    {   
        perror("fork");
        return 1;
    }   
    else if(id == 0)//child
    {   
        printf("child is run,pid is %d\n",getpid());
        sleep(5);
        exit(3);
    }   
    else//parent
    {   
        int status = 0;
        pid_t ret = 0;
        do  
        {   
            ret = waitpid(-1,&status,WNOHANG);//非阻塞式等待
            if(ret == 0)//无退出子进程可收集
            {   
                printf("child is running\n");
            }   
            sleep(1);
        }while(ret == 0);//无退出子进程时,会一直询问(轮询式访问),这里1s询问一次
        if(WIFEXITED(status) && ret == id)//WIFEXITED(status)为真且正常返回才进入该条件语句
        {   
            printf("wait success,child return code is %d\n",WEXITSTATUS(status));//WEXITSTATUS(status))查看进程的退出码
        }   
        else
        {
            printf("wait child failed,return \n");
            return 2;
        }
    }
    return 0;
}

子进程正常退出结果

换终端将子进程异常终止,运行结果如下

3.获取子进程status

        1)wait和waitpid,均有一个status参数,该参数是一个输出型参数,由操作系统填充;

        2)若传NULL,表示不关心子进程的退出状态信息;

        3)否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程;

        4)status不能当做简单整型看待,可以当做位图来看待,具体如下图(这里只有低16位比特位):

        由图可知,当程序正常终止时,status的位图的低8位不会收到任何信号,保持全0状态,此时位图的次低8位则保存着程序的退出码。一旦低8位收到信号,则表示程序被信号杀死即异常终止。此时位图的低8位保存程序的终止信号,次低8位不会用。不过需要注意的是,实际上用来保存收到终止信号的只有7个比特位,因为低8位中有一个比特位是core dump标志。

让我们试一试,一起来编写自主my_shell


#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>  //isspace
 
int main()
{
    char buf[1024] = {}; 
    while(1)
    {   
        pid_t id = fork();
        if(id == 0)//child
        {   
            char* my_arg[32];
            printf("myshell>");
            fgets(buf,sizeof(buf),stdin);
            buf[strlen(buf)-1] = 0;
            char* p = buf;
            int i = 1;
            my_arg[0] = buf;
            while(*p)
            {   
                if(isspace(*p))
                {   
                    *p = 0;
                    p++;
                    my_arg[i++] = p;
                }
                else
                {
                    p++;
                }
            }
            my_arg[i] = NULL;
            execvp(my_arg[0],my_arg);
        }
        else
        {
            int status = 0;
            pid_t ret = waitpid(id,&status,0);
            if(ret > 0)
            {
                //printf("%d\n",status&0X7F);
            }
            else
            {
                printf("waitpid runnning error\n");
            }
        }
    }
                    p++;
                    my_arg[i++] = p;
                }
                else
                {
                    p++;
                }
            }
            my_arg[i] = NULL;
            execvp(my_arg[0],my_arg);
        }
        else
        {
            int status = 0;
            pid_t ret = waitpid(id,&status,0);
            if(ret > 0)
            {
                //printf("%d\n",status&0X7F);
            }
            else
            {
                printf("waitpid runnning error\n");
            }
        }
    }
                    p++;
                    my_arg[i++] = p;
                }
                else
                {
                    p++;
                }
            }
            my_arg[i] = NULL;
            execvp(my_arg[0],my_arg);
        }
        else
        {
            int status = 0;
            pid_t ret = waitpid(id,&status,0);
            if(ret > 0)
            {
                //printf("%d\n",status&0X7F);
            }
            else
            {
                printf("waitpid runnning error\n");
            }
        }
    }
    return 0;
}

封装fork/wait等操作, 编写函数 process_create(pid_t* pid, void* func, void* arg), func回调函数就是子进程执行的入口函数, arg是传递给func回调函数的参数. 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
 
void process_create(int(*func)(), const char* file, char* argv[])
{
    int ret = 0;
    pid_t pid = fork();
    if(pid == 0)//child
    {   
        ret = func(file,argv);
        if(ret == -1) 
        {   
            perror("func");
            exit(1);
        }   
    }   
    else
    {   
        int st; 
        pid_t ret = wait(&st);
        if(ret == -1) 
        {   
            perror("wait");
            exit(2);
        }   
    }   
}
 
int main()
{
    char* argv1[] = {"ls"};
    char* argv2[] = {"ls","-l","/home/may/Code/Linux/class4",0};
    process_create(execvp,*argv1,argv2);
    return 0;
}

popen/system,理解这两个函数与fork的区别

popen函数:

 #include <stdio.h>

       FILE *popen(const char *command, const char *type);

       int pclose(FILE *stream);

 popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。这个进程必须由pclose关闭。

system函数:

#include <stdlib.h>

       int system(const char *command);

system( )会调用fork( )产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令。此命令执行完后随即返回原调用的进程。在调用system( )期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。调用/bin/sh来执行参数指定的命令,/bin/sh 一般是一个软连接,指向某个具体的shell。

        实际上system()函数执行了三步操作:

            fork一个子进程;

            在子进程中调用exec函数去执行command;

            在父进程中调用wait去等待子进程结束。

区别:

        (1)system 在执行期间,调用进程会一直等待 shell 命令执行完成(waitpid),但是 popen 无需等待 shell 命令执行完成就返回。

        (2)popen 函数执行完毕后必须调用 pclose 来对所创建的子进程进行回收,否则会造成僵尸进程的情况。

        (3)open 没有屏蔽 SIGCHLD ,如果在调用时屏蔽了 SIGCHLD ,且在 popen 和 pclose 之间调用进程又创建了其他子进程并调用进程注册了 SIGCHLD 来处理子进程的回收工作,那么这个回收工作会一直阻塞到 pclose 调用。

猜你喜欢

转载自blog.csdn.net/weixin_40123831/article/details/81188194
今日推荐