【Linux】进程控制(三) 自定义 Shell 命令行解释器的实现与进程协作实践

文章目录


一、自定义shell命令行解释器

学习了前面进程概念,进程控制的相关知识,我们对进程已经有了理性的认识,下面我们一起来实现一个自定义shell把这些知识串联起来,能对进程概念及进程相关各种用法,函数调用接口有一个更深刻是理解和记忆。

实现自定义shell的目标:能处理普通命令、能处理内建命令、能帮助我们理解内建命令/本地变量/环境变量这些概念、能帮助我们理解shell的运行原理。

构建框架

首先我们把要用到的所有文件创建出来,采用头源分离。未来方便,主要用C++编写。

cpp 复制代码
//myshell.h
#ifndef __MYSHELL_H__
#define __MYSHELL_H__

#include <iostream>

void Debug();

#endif
cpp 复制代码
//myshell.cc
#include "myshell.h"

void Debug()
{
    printf("hello shell!\n");
}
cpp 复制代码
//makefile
myshell:main.cc myshell.cc
	g++ -o $@  $^
.PHONY:clean
clean:
	rm -f myshell

输出命令行提示符

实现shell第一步是打印命令行提示符,我们先看xshell的命令行提示符,如上图所示,除了一些符号外有三个主要变量,这三个变量可以直接通过系统调用获取,但是这里我们为了复习一下学过的知识,故采用从环境变量中间接获取,系统的最后一个字符是$,为了区分我们用#。

由于这里我们是C语言和C++混编,所以需要注意一些细节,比如C语言printf字符串的时候不能直接打印string变量,因为它的字符串格式说明符 %s 要求传入的参数是C 风格字符串------ 即一个指向以空字符 '\0' 结尾的字符数组的指针(const char* 类型,所以需要用c_str把string类型变量装换为C 风格字符串再打印。下面是代码示例:

cpp 复制代码
//myshell.cc
static std::string GetUserName()
{
    string username = getenv("USER");
    return username.empty() ? "None" : username;
}

static std::string GetHostName()
{
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

static std::string GetPwd()
{
    string pwd = getenv("PWD");
    return pwd.empty() ? "None" : pwd;
}

void PrintCommandPrompt()
{
    std::string username = GetUserName();
    std::string hostname = GetHostName();
    std::string pwd = GetPwd();
    printf("[%s@%s %s]# ", username.c_str(), hostname.c_str(), pwd.c_str());
} 

GetUserName、GetHostName、GetPwd这三个接口我们不想暴露给外部使用,所以可以加static修饰使其只能在myshell.cc文件内部使用。

读取用户输入

因为我们要读取用户输入的整个字符串,所以还需要把空格字符串的空格读进去,所以不能用cin和scanf,因为它们遇到空格都会停止读取,这里我们选用C语言接口fgets,getline也可以,但fgets对于后续一些操作更友好,fgets具体使用介绍如下:

首先需要在main函数里创建一个字符数组commandstr作为参数存放读取用户输入的字符串。

cpp 复制代码
//main.cc
#include "myshell.h"

#define SIZE 1024

int main()
{
    char commandstr[SIZE];

    while(true)
    {
        // 1、打印命令行提示符
        PrintCommandPrompt();
        // 2、读取用户输入的命令
        GetCommandString(commandstr, SIZE);
        //测试
        printf("%s\n", commandstr);
    }
    return 0;
}

然后实现GetCommandString的内部逻辑,首先要判断传入的参数是否合法。接着通过fgets读取字符串,读取失败返回false,因为至少会读取一个回车键('\n'),所以不会读取为空。最后把读取到的最后一个字符'\n'置为'\0',因为长度为10的字符串最后一个字符下标为9,所以需要strlen(cmdstr_buff) - 1。若删掉'\n'后字符长度为0说明只读取到了'\n',返回false。

cpp 复制代码
//myshell.cc
bool GetCommandString(char cmdstr_buff[], int len)
{
    if(cmdstr_buff == NULL || len <= 0)
    {
        //参数不合法
        return false;
    }
    char* res = fgets(cmdstr_buff, len, stdin);
    if(res == NULL)
    {
        //读取字符串失败
        return false;
    }
    //把输入的回车也就是'\n'置为'\0'
    cmdstr_buff[strlen(cmdstr_buff) - 1] = 0;
    return strlen(cmdstr_buff) == 0 ? false : true; 
}

解析命令字符串

经过前面两步后下一步需要解析用户输入的命令字符串,解析命令字符串本质就是创建命令行参数表,并把用户输入的字符串按空格分开,依次放入命令行参数表中。而且系统中的命令行参数表原本就是由bash创建并维护的,我们自定义shell其实也是在一定程度上在模拟实现一个bash。

1、我们知道系统的命令行参数表是main函数的局部变量,而这里我们自定义shell时希望子进程能继承父进程的命令行参数表,所以我们这里需要把命令行参数表定义在全局。

2、有许多函数可以分割字符串,我们选取一个最简单的来使用:strtok

首次调用时第一个参数传入待分割的字符串,后续调用时传NULL,第二个参数传分隔符字符串,比如空格。切割成功返回分割出的子串的首元素地址,切割失败或者切割完毕返回NULL。它的底层原理是把原字符串里的分隔符字符串全部替换成'\0'。

3、开始提取子串写入命令行参数表gargv,第一次调用strtok得到的子串放入gargv[0],然后循环取子串放入,最后提取子串完毕strtok返回NULL写入gargv最后一个位置,正好命令行参数表要以NULL结尾。

4、但是目前解析命令字符串逻辑还有两个bug,其一因为gargv和gargc都是全局变量,所以在main函数死循环逻辑的开头需要初始化全局变量,gargc直接置为0就行了,gargv数组可以用memset初始化更方便:

void * memset ( void * ptr, int value, size_t num );

第一个参数传待设置的内存空间,因为memset是以字节为单位初始化的,第二个参数是要设置的数值(本质是ASCII码值),第三个参数是要设置的长度。

5、其二是如果用户啥都不输入直接按回车键,那么第二步读取用户命令什么都读取不到,commandstr数组将为空,第三步提取时会把strtok返回的NULL写入gargv[0],所以我们在主逻辑main函数的第二步多加一个判断,如果用户没有输入,直接回车,此时直接continue跳过此轮循环的后续逻辑。

cpp 复制代码
//myshell.cc

//全局定义命令行参数表
char* gargv[ARGS] = {NULL};
int gargc = 0;

void InitGlobal()
{
    gargc = 0;
    memset(gargv, 0, sizeof(gargv));
}

bool ParseCommandString(char cmd[])
{
    if(cmd == NULL)
    {
        //安全检查
        return false;
    }
    //可以在函数内部定义,SEP表示分隔符
#define SEP " "
    //"ls -a -l" -> "ls" "-a" "-l" 
    
    //把第一个子串写入gargv[0],然后gargc++
    gargv[gargc++] = strtok(cmd, SEP);
    //把子串全部写入gargv数组里,并且以NULL结尾
    while(gargv[gargc++] = strtok(NULL, SEP))
        ;//循环空语句
    //回退一次命令行参数的个数
    --gargc;
    
    //条件编译,测试代码
    //因为gargv,gargc定义在该文件,无法在main.cc里debug
    //#define DEBUG
    #ifdef DEBUG

    printf("gargc: %d\n", gargc);
    printf("--------------------------\n");
    for(int i = 0; i < gargc; i++)
    {
        printf("gargv[%d]: %s\n", i, gargv[i]);
    }
    printf("--------------------------\n");
    for(int i = 0; gargv[i]; i++)
    {
        printf("gargv[%d]: %s\n", i, gargv[i]);
    }
    #endif
    
    return true;
}
cpp 复制代码
//main.cc
#include "myshell.h"

#define SIZE 1024

int main()
{
    char commandstr[SIZE];

    while(true)
    {
        // 0、初始化全局变量
        InitGlobal();
        // 1、打印命令行提示符
        PrintCommandPrompt();
        // 2、读取用户输入的命令
        
        //如果用户没有输入,直接回车
        //会返回false,此时直接continue
        if(!GetCommandString(commandstr, SIZE))
            continue;
        // 3、解析命令行字符串
        ParseCommandString(commandstr);

    }
    return 0;
}
cpp 复制代码
//main.cc
#include "myshell.h"

#define SIZE 1024

int main()
{
    char commandstr[SIZE];

    while(true)
    {
        // 0、初始化全局变量
        InitGlobal();

        // 1、打印命令行提示符
        PrintCommandPrompt();

        // 2、读取用户输入的命令
        //如果用户没有输入,直接回车
        //会返回false,此时直接continue
        if(!GetCommandString(commandstr, SIZE))
            continue;

        // 3、解析命令行字符串
        ParseCommandString(commandstr);

        // 4、执行命令
        ForkAndExec();
    }
    return 0;
}

执行命令

首先我们要知道执行命令不能由shell本身来做,因为执行命令会发生程序提花,一但shell被替换那么就无法继续输出命令行提示符和读取用户输入了,所以执行命令需要交由子进程来做。

大体思路是先在main函数逻辑里fork一个子进程,子进程执行程序替换并运行命令,执行完毕后子进程直接退出。父进程等待子进程,不论等待成功还是失败都会继续循环执行shell主逻辑。

然后来选择使用哪个程序替换接口,因为我们要执行的命令没带路径所以要有p,命令行参数已经被我们维护成了表结构,所以要有v,子进程可以通过虚拟地址空间继承到环境变量,所以程序替换时可以不传,那么我们的最佳选择就是execvp。

cpp 复制代码
void ForkAndExec()
{
    pid_t id = fork();
    if (id < 0)
    {
        //fork失败
        perror("fork");
        return;
    }
    else if (id == 0)
    {
        //子进程
        execvp(gargv[0], gargv);
        exit(0);
    }
    else {
        //父进程
        pid_t rid = waitpid(id, nullptr, 0);
    }
}

内建命令

cd

我们目前已经实现了一个最基本的shell,还有许多优化工作需要我们做。首先当前的shell运行cd指令时无法切换shell进程的当前工作路径,因为cd命令交给子进程去执行了,改变的也只是子进程的工作路径,运行cd指令的子进程退出后不会对父进程有影响,再执行pwd时负责执行pwd的子进程还是继承原先父进程的工作路径,所以我们肉眼看到路径没有变化。而系统的cd路径切换本质是bash自己在切换,切换后创建的子进程继承了父进程的路径,再pwd就会看到切换后的路径。

1、下面我们来实现cd命令的运行逻辑,首先在主逻辑执行命令步骤之前添加一个检查内建命令、若为内建命令则执行的步骤(BuildInCommandExec),如果是内建命令,则执行完该步骤后直接continue,若不是则继续执行后续逻辑。

2、然后编写BuildInCommandExec的内部逻辑,首先判断gargv[0]是不是"cd",注意不能直接比较,直接比gargv[0]和"cd"是比的两个指针是否相同,我们需要比两个字符串是否相同。需要先将其中一方转换为string,然后再比较,这时另一方就会被隐式转换为string,然后就可以调用string的operator==比较两个字符串内容了。

3、接着通过父进程调用chdir改变当前工作路径,chdir 系统调用是 cd 指令的底层实现的一部分,我们要自己实现cd功能就需要让父进程自己调用chdir来切换自己的工作路径。下面是chdir的文档和使用介绍:

参数 path 是目标目录的路径,绝对路径或相对路径均可,调用成功返回 0,调用失败返回 -1。

4、这里小编补充一点,当我们只输入 "cd" 时功能和 "cd ~" 一样,会使当前工作路径返回家目录。所以我们实现时要考虑这两种情况,若为这两种情况,则需要从环境变量中获取家目录并跳转,若不是则跳转到gargv[1]指定的目录下,绝对路径、相对路径均可。

5、最后处理返回值,该接口默认认为提取到的命令不是内建命令返回false,只有是内建命令并且父进程执行了该指令后才返回true。

下面是示例代码:

cpp 复制代码
//myshell.cc
static std::string GetHostName()
{
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

bool BuildInCommandExec()
{
    //不能:gargv[0] == "cd" 这样比
    //这样比是比较指针是否相同,而非字符内容
    std::string cmd = gargv[0];
    bool ret = false; //默认不是内建命令
    if(cmd == "cd")//这里"cd"会被隐式类型转换为string
    {
        if(gargc == 2)
        {
            std::string target = gargv[1];
            if(target == "~")
            {
                //"cd ~"返回家目录
                chdir(GetHomePath().c_str());                                                          
                ret = true;
            }
            else{
                chdir(gargv[1]);
                ret = true;
            }
        }
        else if(gargc == 1)
        {
            chdir(GetHomePath().c_str());
            ret = true;
        }
        else{
            //错误
        }
    }
    return ret;
}

echo

echo命令也是一个内建命令,因为"echo ?"可以打印出上一个子进程的退出码,而退出码不是环境变量是本地变量,子进程是拿不到父进程的本地变量的,所以echo是由父进程直接执行的。所以echo指令也需要进BuildInCommandExec接口。首先定义一个全局变量lastcode存子进程的退出码,在执行命令接口ForkAndExec的子进程逻辑中获取子进程的退出码写入lastcode中。当用户输入"echo ?"指令时就把lastcode的值打印出来,lastcode里存的就是上一个子进程的退出码,所以InitGlobal不用初始化lastcode。当输入"echo $(环境变量)"时就通过getenv(const char* name)接口查找环境变量并打印。 当打印其它字符串时就把字符串原封不动的打印出来。

cpp 复制代码
//myshell.cc

//用于存储上一个子进程的退出码
int lastcode;

void ForkAndExec()
{
    pid_t id = fork();
    if(id < 0)
    {
        //fork失败
        perror("fork");
        return;
    }
    else if(id == 0)
    {
        //子进程
        execvp(gargv[0], gargv);
        exit(0);
    }
    else{
        //父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            //获取子进程退出码
            lastcode = WEXITSTATUS(status);
        }
    }
}

bool BuildInCommandExec()
{
    //不能:gargv[0] == "cd" 这样比
    //这样比是比较指针是否相同,而非字符内容
    std::string cmd = gargv[0];
    bool ret = false; //默认不是内建命令
    if(cmd == "cd")//这里"cd"会被隐式类型转换为string
    {
        if(gargc == 2)
        {
            std::string target = gargv[1];
            if(target == "~")
            {
                //"cd ~"返回家目录
                chdir(GetHomePath().c_str());   
                lastcode = 0;
                ret = true;
            }
            else{
                chdir(gargv[1]);
                lastcode = 0;
                ret = true;
            }
        }
        else if(gargc == 1)
        {
            chdir(GetHomePath().c_str());
            lastcode = 0;
            ret = true;
        }
        else{
            //错误
        }
    }
    else if(cmd == "echo")
    {
        if(gargc == 2)
        {
            std::string args = gargv[1];
            if(args[0] == '$')
            {
                if(args[1] == '?')
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                    ret = true; 
                }
                else{
                    //char *getenv(const char *name);
                    const char* name = &args[1];           
                    printf("%s\n", getenv(name));
                    lastcode = 0;
                    ret = true;
                }
            }
            else{
                printf("%s\n", gargv[1]);
                lastcode = 0;
                ret = true;
            }
        }
    }
    return ret;
}

更新命令行提示符中的当前路径

代码写到这里还有问题,我们cd后再pwd确实看到当前工作路径已经变了,但是为什么输出的命令行提示符的当前路径一直没变呢?为什么pwd后看到的路径却变了呢?

我们一步一步来,先解决命令行提示符的当前路径一直不变的问题。我们在讲环境变量时提到过,环境变量有两个来源,一个是从bash从配置文件中获取,一个是bash启动后自己动态获取并创建,就比如PWD,当用户执行 cd 命令切换目录时,bash 会先通过 chdir() 系统调用修改自身的 cwd,然后立即调用 getcwd() 获取新的 pwd路径,更新到 PWD环境变量中。到目前为止我们自定义的bash已经实现了chdir()的功能,接下来还需要我们实现getcwd()的功能。(getcwd的使用说明:请点击

命令行提示符的当前路径是通过GetPwd接口获取的,所以我们需要修改原来的GetPwd接口,不再直接getenv获取当前工作路径。

下面是初版代码,并没有更新当前的进程的环境变量中的PWD。

cpp 复制代码
static std::string GetPwd()
{
    //string pwd = getenv("PWD");
    //return pwd.empty() ? "None" : pwd;
    char pwd[1024];
    getcwd(pwd, sizeof(pwd));
    return pwd;
}

接下来我们需要更新环境变量表中的PWD,首先需要在全局定义一个字符数组pwd用来存储环境变量表的内容,因为我们知道环境变量表是一个字符指针数组,指向一个一个的字符串或者字符数组,而snprintf可以把拿到的tmp数组格式化输出到字符串中,用法和printf类似,只不过printf是往显示器上输出,而snprintf是往字符串中输出。然后通过putenv环境变量表修改环境变量表,我们之前已经介绍过了。两个接口如下所示:

优化后的代码:

cpp 复制代码
char pwd[1024];

static std::string GetPwd()
{
	char tmp[1024];
	getcwd(tmp, sizeof(tmp));
	//顺便更新一下自己shell的环境变量
	snprintf(pwd, sizeof(pwd), "PWD=%s", tmp);
	putenv(pwd);
	return pwd;
}

现在我们已经把基本功能实现完毕,还有最后一步,我们看到xshell的命令行提示符中只打印了一个类似"myshell"的路径,而不是 "/home/fdb/lesson21/myshell" 这样的长路径,所以需要截取子串,步骤如下:

cpp 复制代码
static std::string GetPwd()
{
    char temp[1024];
    getcwd(temp, sizeof(temp));
    //顺便更新一下自己shell的环境变量
    snprintf(pwd, sizeof(pwd), "PWD=%s", temp);
    putenv(pwd);

    //命令行提示符中输出单个路径(截取子串)
    std::string pwd_label = temp;
    const std::string pathsep = "/"; //路径分隔符
    //查找长路径中最后一个'/'的位置
    size_t pos = pwd_label.rfind(pathsep);
    if(pos == std::string::npos)
    {
        //整个路径都没有'/',返回None
        return "None";
    }
    //从pos位置的下一个位置开始截取,相当于跳过pathsep截取后续子串
    pwd_label = pwd_label.substr(pos + pathsep.size());

    //如果此时size为0说明什么都没截取到,说明截取前pwd_label中只有"/"
    //则返回"/"
    return pwd_label.size() ? pwd_label : "/";
}

自定义shell源码

main.cc:

cpp 复制代码
#include "myshell.h"

#define SIZE 1024

int main()
{
    char commandstr[SIZE];

    while(true)
    {
        // 0、初始化全局变量
        InitGlobal();

        // 1、打印命令行提示符
        PrintCommandPrompt();

        // 2、读取用户输入的命令
        //如果用户没有输入,直接回车
        //会返回false,此时直接continue
        if(!GetCommandString(commandstr, SIZE))
            continue;

        // 3、解析命令行字符串
        ParseCommandString(commandstr);

        // 4、检查命令,若为内建命令由父进程运行
        if(BuildInCommandExec())
            continue;
        
        // 5、执行命令
        ForkAndExec();
    }
    return 0;
}

myshell.cc:

cpp 复制代码
#include "myshell.h"

//using namespace std不放在头文件中,会污染命名空间
using namespace std;

//全局定义命令行参数表
char* gargv[ARGS] = {NULL};
int gargc = 0;

//用于存储环境变量PWD
char pwd[1024];

//用于存储上一个子进程的退出码
int lastcode;

void Debug()
{
    printf("hello shell!\n");
}

void InitGlobal()
{
    gargc = 0;
    memset(gargv, 0, sizeof(gargv));
}

static std::string GetUserName()
{
    string username = getenv("USER");
    return username.empty() ? "None" : username;
}

static std::string GetHostName()
{
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

static std::string GetPwd()
{
    //string pwd = getenv("PWD");
    //return pwd.empty() ? "None" : pwd;
    char temp[1024];
    getcwd(temp, sizeof(temp));
    //顺便更新一下自己shell的环境变量
    snprintf(pwd, sizeof(pwd), "PWD=%s", temp);
    putenv(pwd);

    //命令行提示符中输出单个路径
    std::string pwd_label = temp;
    const std::string pathsep = "/"; //路径分隔符
    //查找长路径中最后一个'/'的位置
    size_t pos = pwd_label.rfind(pathsep);
    if(pos == std::string::npos)
    {
        //整个路径都没有'/',返回None
        return "None";
    }
    //从pos位置的下一个位置开始截取,相当于跳过pathsep截取后续子串
    pwd_label = pwd_label.substr(pos + pathsep.size());

    //如果此时size为0说明什么都没截取到,说明截取前pwd_label中只有"/"
    //则返回"/"
    return pwd_label.size() ? pwd_label : "/";
}

static std::string GetHomePath()
{
    std::string home = getenv("HOME");
    //若环境变量缺失或被篡改home为空,为空则回退到家目录
    return home.empty() ? "/" : home;
}

void PrintCommandPrompt()
{
    std::string username = GetUserName();
    std::string hostname = GetHostName();
    std::string pwd = GetPwd();
    printf("[%s@%s %s]# ", username.c_str(), hostname.c_str(), pwd.c_str());
} 

bool GetCommandString(char cmdstr_buff[], int len)
{
    if(cmdstr_buff == NULL || len <= 0)
    {
        //参数不合法
        return false;
    }
    char* res = fgets(cmdstr_buff, len, stdin);
    if(res == NULL)
    {
        //读取字符串失败
        return false;
    }
    //把输入的回车也就是'\n'置为'\0'
    cmdstr_buff[strlen(cmdstr_buff) - 1] = 0;
    return strlen(cmdstr_buff) == 0 ? false : true; 
}

bool ParseCommandString(char cmd[])
{
    if(cmd == NULL)
    {
        //安全检查
        return false;
    }
    //可以在函数内部定义,SEP表示分隔符
#define SEP " "
    //"ls -a -l" -> "ls" "-a" "-l" 
    
    //把第一个子串写入gargv[0],然后gargc++
    gargv[gargc++] = strtok(cmd, SEP);
    //把子串全部写入gargv数组里,并且以NULL结尾
    while(gargv[gargc++] = strtok(NULL, SEP))
        ;//循环空语句
    //回退一次命令行参数的个数
    --gargc;
    
    //条件编译,测试代码
    //#define DEBUG
    #ifdef DEBUG

    printf("gargc: %d\n", gargc);
    printf("--------------------------\n");
    for(int i = 0; i < gargc; i++)
    {
        printf("gargv[%d]: %s\n", i, gargv[i]);
    }
    printf("--------------------------\n");
    for(int i = 0; gargv[i]; i++)
    {
        printf("gargv[%d]: %s\n", i, gargv[i]);
    }

    #endif

    return true;
}

void ForkAndExec()
{
    pid_t id = fork();
    if(id < 0)
    {
        //fork失败
        perror("fork");
        return;
    }
    else if(id == 0)
    {
        //子进程
        execvp(gargv[0], gargv);
        exit(0);
    }
    else{
        //父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            //获取子进程退出码
            lastcode = WEXITSTATUS(status);
        }
    }

}

bool BuildInCommandExec()
{
    //不能:gargv[0] == "cd" 这样比
    //这样比是比较指针是否相同,而非字符内容
    std::string cmd = gargv[0];
    bool ret = false; //默认不是内建命令
    if(cmd == "cd")//这里"cd"会被隐式类型转换为string
    {
        if(gargc == 2)
        {
            std::string target = gargv[1];
            if(target == "~")
            {
                //"cd ~"返回家目录
                chdir(GetHomePath().c_str());   
                lastcode = 0;
                ret = true;
            }
            else{
                chdir(gargv[1]);
                lastcode = 0;
                ret = true;
            }
        }
        else if(gargc == 1)
        {
            chdir(GetHomePath().c_str());
            lastcode = 0;
            ret = true;
        }
        else{
            //错误
        }
    }
    else if(cmd == "echo")
    {
        if(gargc == 2)
        {
            std::string args = gargv[1];
            if(args[0] == '$')
            {
                if(args[1] == '?')
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                    ret = true; 
                }
                else{
                    const char* name = &args[1];           
                    printf("%s\n", getenv(name));
                    lastcode = 0;
                    ret = true;
                }
            }
            else{
                printf("%s\n", gargv[1]);
                ret = true;
            }
        }
    }
    return ret;
}

myshell.h:

cpp 复制代码
#ifndef __MYSHELL_H__
#define __MYSHELL_H__

#include <stdio.h>
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <cstdbool>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//命令行参数表的大小
#define ARGS 64

void Debug();
//初始化全局变量
void InitGlobal();
//输出命令行字符串
void PrintCommandPrompt();
//读取用户输入字符串
bool GetCommandString(char cmdstr_buff[], int len); 
//解析命令行字符串
bool ParseCommandString(char cmd[]);
//执行命令
void ForkAndExec();
//检查是否是内建命令,若为内建命令交由父进程运行
 bool BuildInCommandExec();

#endif

二、子进程备份

我们前面实现的自定义shell创建子进程都是让它程序替换后执行与父进程完全不同的代码,下面小编再展示一份让父子进程分工合作的代码,让子进程运行父进程代码的一部分。

代码的业务逻辑是保存随机数据到全局数组并备份到文件中,让父进程负责保存数据,让子进程负责备份父进程的数据,这样就可以使保存数据和备份数据并发执行,提高效率。

因为有写时拷贝的存在,即使父子进程操作的是同一份全局数组,也互不影响。

cpp 复制代码
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int garray[100];

pid_t backup(const char* filename)
{
    //交由子进程完成备份
    pid_t id = fork();
    if(id == 0)
    {
        FILE* pf = fopen(filename, "w");
        for(int i = 0; i < 100; i++)
        {
            fprintf(pf, "%d ", garray[i]);
        }
        fclose(pf);
        exit(0);
    }
    return id;
}

int main()
{
    srand(time(NULL));
    for(int i = 0; i <100; i++)
    {
        garray[i] = rand() % 10;
    }
    pid_t sub1 = backup("log1.txt");

    for(int i = 0; i <100; i++)
    {
        garray[i] = rand() % 10;
    }
    pid_t sub2 = backup("log2.txt");

    for(int i = 0; i <100; i++)
    {
        garray[i] = rand() % 10;
    }
    pid_t sub3 = backup("log3.txt");

    waitpid(sub1, NULL, 0);
    waitpid(sub2, NULL, 0);
    waitpid(sub3, NULL, 0);

    return 0;
}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
熙客16 小时前
阿里云监控:SLS的使用
运维·阿里云·云原生·云计算
qiuiuiu41317 小时前
正点原子RK3568学习日志6-驱动模块传参
linux·c语言·开发语言·单片机·学习
华纳云IDC服务商17 小时前
洛杉矶服务器常见问题汇总与解决方案大全
运维·服务器·php
dddddppppp12317 小时前
linux sdl图形编程之helloworld.
linux·运维·ffmpeg
qq_4798754317 小时前
TimerFd & Epoll
java·服务器·数据库
做运维的阿瑞17 小时前
Kubernetes 存储核心理论:深入理解 PVC 静态迁移与动态扩容
运维·容器·kubernetes
小任今晚几点睡17 小时前
Docker 完整指南:从入门到企业实战
运维·docker·容器
郝学胜-神的一滴17 小时前
Linux系统函数link、unlink与dentry的关系及使用注意事项
linux·运维·服务器·开发语言·前端·c++
霍格沃兹软件测试开发17 小时前
借助 Dify 实现自动化工作流,每天节省3小时
运维·ai·自动化