结果我们前面七节有关进程的系统性学习,我们已经积累了不少知识了,本篇文章是对前面所学知识的一个归纳与运用,目的是完成一个小代码,模拟实现Shell的运行的基本原理与功能,旨在更好地帮助大家理解、明白进程的概念与操作。
我们想要实现的SHell一般有什么功能呢?
在启动Shell的时候,系统会从配置文件中加载配置文件,形成一个自己的环境变量表。
我们这里的Myshell是系统Shell的子进程,所以为了模拟这个过程,我们选择从系统Shell拷贝一份自己的环境变量表,存储到Myshell的本地里面: 这个过程是对SHell的初始化,我们用InitEnv来表达:
void InitEnv()
{
extern char**environ;
int index=0;
while(environ[index])
{
genv[index]=(char*)malloc(strlen(environ[index])+1);
strncpy(genv[index],environ[index],strlen(environ[index])+1);
index++;
}
genv[index]=nullptr;
}在初始化后,SHell就开始了无限的循环过程。
我们可以想到,平时的SHell会先打印出一截字符串信息,随后停留住,等待用户输入相应指令后,在分析指令,最后执行。于是完成闭环。
我们把以上行为划分为:打印命令行提示符,获取用户指令,分析用户指令,执行指令四大部分,分别用PrintCommandLine,GetCommandLine,ParseCommandline,ExecuteCommandLine四个函数来进行实现。这四个函数都一直在while死循环中:
#include<iostream>
#include<cstdio>
void PrintCommandLine();
void GetCommandLine();
void ParseCommandLine();
void ExecuteCommandLine();
void InitEnv()
{
extern char**environ;
int index=0;
while(environ[index])
{
genv[index]=(char*)malloc(strlen(environ[index])+1);
strncpy(genv[index],environ[index],strlen(environ[index]+1));
index++;
}
genv[index]=nullptr;
}
int main()
{
InitEnv();
while(1)
{
PrintCommandLine();
GetCommandLine();
ParseCommandLine();
ExecuteCommandLine();
}
return 0;
}划分出这四大步骤后,其他的代码就都是为了实现这些功能而出现的了。
在我们没有输入指令时,Shell就会一直在这个界面等待着我们:

可以看得出来,打印前面的命令行肯定是在我们输入之前的。怎么打印呢?
我们仿照Shell写,规格采取一样的布局,都是用户加主机名加当前目录的布局方式,而用户主机名这些参数,不就正是系统的环境变量里保存着的吗?
我们可以通过系统调用,获取相关的环境变量信息。
也就是getenv这个接口,可以直接得到主机名,用户名等信息,获取之后,需要将这些信息合并为一个字符串,方便我们进行打印。为了代码的可读性与耦合性,我们把这些专门写成一个个小接口,方便调用。
我们这里选择使用snprintf,将这些字符串打印到一个专门的字符数组里充当缓冲区,缓冲区的大小我们默认设置为1024(不用去计较大小)。最后就是调用printf打印出这个字符数组,不过大家要记住的是,由于我们命令行提示符后面是不能有\n的,否则用户就不会紧跟在后面输入指令而是下面了。所以没有\n,使用print就无法刷新缓冲区,导致打印结果无法凸显出来。
为了解决这个问题,我们需要再print后面加一个fflush手动刷新缓存区。
const int basesize=1024;
//接口,获取用户名,倘若不存在或者获取失败就返回None
std::string GetUsername()
{
std::string username=getenv("USER");
return username.empty()?"None":username;
}
//接口,获取主机名,倘若不存在或者获取失败就返回None
std::string GetHostname()
{
std::string hostname=getenv("HOSTNAME");
return hostname.empty()?"None":hostname;
}
//接口,获取当前路径,倘若不存在或者获取失败就返回None
std::string Getcwd()
{
std::string cwd=getenv("PWD");
return cwd.empty()?"None":cwd;
}
//接口,负责生成完整的命令行并返回
std::string MakeCommandLine()
{
char command_line[basesize];
snprintf(command_line,basesize,"%s@%s:%s$",GetUsername().c_str(),GetHostname().c_str(),Getcwd().c_str());
return command_line;
}
//打印命令行并刷新缓冲区
void PrintCommandLine()
{
printf("%s",MakeCommandLine().c_str());
fflush(stdout);
}这上面的代码是正确的,但面对不同的操作系统可能有不同的情况,比如ubuntu系统的hostname并不是环境变量的一员,所以你使用getenv是获取不到它的,所以我们可以改成大众一点的:
std::string GetHostname()
{
char hostname[basesize];
if(gethostname(hostname,sizeof(hostname))!=0)
{
return "None";
}
hostname[sizeof(hostname)-1]='\0';
return hostname;
}将main函数里的未完成函数注释掉,我们运行一下试试:
int main()
{
InitEnv();
while(1)
{
PrintCommandLine();
sleep(10);
// GetCommandLine();
// ParseCommandLine();
// ExecuteCommandLine();
}
return 0;
}可以看见运行打印效果是符合我们预期的:

既然要获取用户的指令,我们自然要有位置给指令存储,所以我们可以在main函数开始时,初始化一个专门的字符数组来记录指令:我们可以把它命名为Command_buffer,大小为1024.
而我们的GetCommandLine函数,自然要用到这个数组,负责把我们输入的指令存储到数组里,所以这个数组实际上是一个输出型参数。
读取一行的输入的方式有很多,比如getline与fgets,我们这里采用fgets从输入流中读取数据:
void GetCommandLine(char Command_buffer[],int size)
{
//我们这里选择使用fgets来读取一行的字符串,可能包含:ls -l -a等
fgets(Command_buffer,size,stdin);
//fgets会将换行符也读取进来,我们需要将其替换为\0
size_t len=strlen(Command_buffer);
Command_buffer[strlen(Command_buffer)-1]='\0';
}由于fgets也会出现出错的情况,所以我们可以把这个函数的返回值改为bool,随后在函数内部判断是否出错,以及如果只输入了换行符的情况:
bool GetCommandLine(char Command_buffer[],int size)
{
//我们这里选择使用fgets来读取一行的字符串,可能包含:ls -l -a等
char *result=fgets(Command_buffer,size,stdin);
if(!result)
{
return false;
}
//fgets会将换行符也读取进来,我们需要将其替换为\0
size_t len=strlen(Command_buffer);
if(len==0)
{
return false;
}
Command_buffer[len-1]='\0';
return true;
}这样一来,我们就可以根据返回值情况知道用户输入是否合法,如果不合法,就直接从打印命令行提示符的步骤重新开始(包括我们只按下换行符,此时len==0),由于函数的返回值类型改变,我们使用函数的方式也应该随之变化,另外,我们可以在后面打印一下Comman_buffer看是否输入正确
int main()
{
InitEnv();
char Command_buffer[basesize];
while(1)
{
PrintCommandLine();
if(!GetCommandLine(Command_buffer,basesize))
{
continue;
}
printf("%s\n",Command_buffer);
// ParseCommandLine();
// ExecuteCommandLine();
}
return 0;
}:

输出结果是符合我们预期的。第二步,获取用户的指令,也就完成了。
我们拿到用户输入的指令后,应该怎么分析呢?
答案是以空格为单位对他们进行一一的分解切割,还记得到我们之前讲过的main函数的参数吗?
argc与argv表格!
没错,我们可以定义一个全局变量argv,负责存储这些一个一个的参数,用argv来记录参数的个数:

那我们我们要做的,就是通过分隔符将他们分割成许多个小部分,存储在char*这个数组里。
这里使用的是strtok函数,还是非常简单的:
void ParseCommandLine(char Command_buffer[])
{
//每次分析前我们都要重置一下,防止被历史堆积数据影响
memset(gargv,0,sizeof(gargv));
gargc=0;
//我们这里采用strtok来进行切分
gargv[gargc++]=strtok(Command_buffer," ");
while(gargv[gargc++]=strtok(nullptr," "));
//因为我们的while判断条件一定会多执行一次,所以我们后面要把减一
gargc--;
}我们这里故意使用赋值等号,当strtok返回的时空指针,就会赋值给gargv的末尾,并结束while循环。
同样的,我们可以进行打印一下来看看是否出错:
ParseCommandLine(Command_buffer);
printf("argc: %d\n", gargc);
for(int i = 0; gargv[i]; i++)
{
printf("argv[%d]: %s\n", i, gargv[i]);
}
可以看见是没问题的。
我们之前已经获取了命令,根据之前所学内容,大部分命令执行都是靠的fork-exec模式进行,所以这里也是一样的情况。我们需要fork出一个子进程,随后让这个子进程进行进程替换:
bool ExecuteCommandLine()
{
pid_t id = fork();
if(id==0)
{
//子进程代码
execvpe(gargv[0], gargv, genv);
exit(127);//非正常退出
}
int status;
int rid=waitpid(id,&status,0);
if(rid>0)
{
if(WIFEXITED(status))
{
return true;
}
else
{
return false;
}
}
return false;
}我们这里选择使用execvpe,这个函数需要我们把自己的本地变量传进去(因为我们自己设置的garv我们自己在维护)
执行命令这里还是比较简单的,编译后生成的可执行程序运行也十分正确,但这样就真的结束了吗?实则不然:

我们可以看见 ,为什么,明明我们已经输入了,执行了cd指令,为什么我们的pwd却没有任何的改变呢?
这是因为,在Shell中,存在着一种由 Shell 自身直接执行的命令,而不是通过创建新进程来运行外部程序。
cd正这样的命令,因为我们cd一个目录,是要变化自己的环境变量的。但是如果你是用fork出的子进程改变环境变量,改变的也是子进程的环境变量(写时拷贝),对Shell程序自己是没有作用的。
所以,我们就不能把这些内建命令与其他程序一样放在执行程序的函数里。我们要对这些命令进行判断,随后特殊处理:
于是我们新增一个接口CheckAndExecBuildCommand函数,负责在分析用户指令与执行系统命令之间去判断指令类型。
在这个函数中,我们要使用if判断是否是我们的内建函数,由于内建函数在众多指令中是比较少的,所以我们这里只需要列几个常见的内建指令:
cd env echo export
怎么比较两个字符串呢?我们可以使用strcmp函数来比较两个字符串是否相等,相等的话会返回0:
比较之后,如果判断是cd指令,我们就使用chdir这个函数改变Shell自己的当前工作路径。
具体可以用这个代码:
if(strcmp(gargv[0],"cd")==0)
{
if(gargc==2)
{
chdir(gargv[1]);
}
return true;
}export的实现就相对较麻烦。我们先来了解一下export指令的功能。export命令用于将变量设置为环境变量,使其对当前Shell会话及其所有子进程可见。
所以我们可以在这个if分支里检测是否为export,如果是,执行添加环境变量这个效果。
为了整体的耦合性,我们选择把这个添加为环境变量的功能写成一个独立的接口:AddEnv
这个AddEnv的实现,我们可以先找到环境变量表genv的末尾空指针,随后使用malloc与拷贝函数将数据拷贝到一开始加载的我们自己拷贝的环境变量表上。
void AddEnv(const char* item)
{
int index=0;
while(genv[index])
{
index++;
}
genv[index]=(char*)malloc(strlen(item)+1);
strncpy(genv[index],item,strlen(item)+1);
genv[++index]=nullptr;
}然后在export中复用该函数:
else if(strcmp(gargv[0],"export")==0)
{
if(gargc==2)
{
AddEnv(gargv[1]);
}
return true;
}env命令就很简单了,直接打印一下我们自己拷贝来的环境变量表genv就行
else if(strcmp(gargv[0],"env")==0)
{
for(int i=0;genv[i];i++)
{
printf("%s\n",genv[i]);
}
return true;
}当我们在Shell本地定义变量a=144时,我们可以通过echo a来打印出144,,当我们执行ceho ?时,可以检查上一个指令是否执行正确。
一样是由if条件判断分支进行。 但值得注意的时,echo $?会返回上次执行指令的错误码,所以我们需要一个全局变量Lastcode来监视。在每个指令执行结束时,都要改变Lastcode的值。这里可以自己规定一套枚举常量作为错误码体系。
所以代码(包括执行系统命令的代码也要加上Lastcode,相互呼应)可以写成:
enum
{
OK=0,
EXECUTE_FAIL,
INCORRECT_COMMAND_LINE,
};
void AddEnv(const char* item)
{
int index=0;
while(genv[index])
{
index++;
}
genv[index]=(char*)malloc(strlen(item)+1);
strncpy(genv[index],item,strlen(item)+1);
genv[++index]=nullptr;
}
bool ExecuteCommandLine()
{
pid_t id = fork();
if(id==0)
{
//子进程代码
execvpe(gargv[0], gargv, genv);
exit(INCORRECT_COMMAND_LINE);//非正常退出,因为exec之后只有失败才会执行到这里
}
int status;
int rid=waitpid(id,&status,0);
if(rid>0)
{
if(WIFEXITED(status))
{
Lastcode=WEXITSTATUS(status);
return true;
}
else
{
Lastcode=EXECUTE_FAIL;
return false;
}
}
Lastcode=EXECUTE_FAIL;
return false;
}
//判断是否是内建命令
bool CheckAndExecBuildCommand()
{
if(strcmp(gargv[0],"cd")==0)
{
if(gargc==2)
{
chdir(gargv[1]);
Lastcode=OK;
}
else
{
Lastcode=INCORRECT_COMMAND_LINE;
}
return true;
}
else if(strcmp(gargv[0],"export")==0)
{
if(gargc==2)
{
AddEnv(gargv[1]);
Lastcode=OK;
}
return true;
}
else if(strcmp(gargv[0],"env")==0)
{
for(int i=0;genv[i];i++)
{
printf("%s\n",genv[i]);
}
Lastcode=OK;
return true;
}
else if(strcmp(gargv[0],"echo")==0)
{
if(gargc==2)
{
if(gargv[1][0]=='$')
{
if(gargv[1][1]=='?')
{
printf("%d\n",Lastcode);
Lastcode=0;
}
}
else
{
printf("%s\n",gargv[1]);
Lastcode=0;
}
}
else
{
Lastcode=INCORRECT_COMMAND_LINE;
}
}
return false;
}写到这里,代码就完成的差的不多了,但是还是有点小bug:

我们使用cd,他的路径确实发生变化了,但是我们的没有手动去维护我们的环境变量表。环境变量是需要我们去手动维护的,并不是自己变得。
所以我们可以在获取环境变量的时候顺便维护该变量。于是我们新增两个全局变量pwd来找到当目录与pwdenv来存储,因此,前面的获取pwd代码可以写成:
//当前路径
char pwd[basesize];
char pwdenv[basesize];
std::string Getcwd()
{
if(nullptr == getcwd(pwd, sizeof(pwd))) return "None";
snprintf(pwdenv, sizeof(pwdenv),"PWD=%s", pwd);
putenv(pwdenv);
return pwd;
}另外,我们在日常使用时
snprintf(command_line,basesize,"%s@%s:%s$",GetUsername().c_str(),GetHostname().c_str(),Getcwd().c_str());
这里获得cwd有可能非常长,影响感官,所以我们可以封装一个接口,负责返回该目录下的第一个上级目录(没有就 /)
std::string LastDir()
{
std::string curr = Getcwd();
if(curr == "/" || curr == "None") return curr;
// /home/whb/XXX
size_t pos = curr.rfind("/");
if(pos == std::string::npos) return curr;
return curr.substr(pos+1);
}具体优化就如上。
我们本篇文章只是对前文的内容进行一个简单的总结与练,并且我们是模拟的一个简单shell,与真实shell肯定会存在部分差异与不足。但最重要的是给大家展示一下在操作系统里的关于进程的简单应用,让大家看看代码。
最后,我们进程的部分就到这里结束了,后面我会开始文件部分的讲解
总的代码如下:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<unistd.h>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
enum
{
OK=0,
EXECUTE_FAIL,
INCORRECT_COMMAND_LINE,
};
int Lastcode=0;
const int basesize=1024;
const int argvnum=64;
//全局的命令行参数列表
char*gargv[argvnum];
int gargc=0;
//全局的环境变量列表
char*genv[argvnum];
int genvc=0;
//当前路径
char pwd[basesize];
char pwdenv[basesize];
//接口,获取用户名,倘若不存在或者获取失败就返回None
std::string GetUsername()
{
std::string username=getenv("USER");
return username.empty()?"None":username;
}
//接口,获取主机名,倘若不存在或者获取失败就返回None
std::string GetHostname()
{
char hostname[basesize];
if(gethostname(hostname,sizeof(hostname))!=0)
{
return "None";
}
hostname[sizeof(hostname)-1]='\0';
return hostname;
}
void InitEnv()
{
extern char**environ;
int index=0;
while(environ[index])
{
genv[index]=(char*)malloc(strlen(environ[index])+1);
strncpy(genv[index],environ[index],strlen(environ[index])+1);
index++;
}
genv[index]=nullptr;
}
//接口,获取当前路径,倘若不存在或者获取失败就返回None
std::string Getcwd()
{
if(nullptr == getcwd(pwd, sizeof(pwd))) return "None";
snprintf(pwdenv, sizeof(pwdenv),"PWD=%s", pwd);
putenv(pwdenv);
return pwd;
}
std::string LastDir()
{
std::string curr = Getcwd();
if(curr == "/" || curr == "None") return curr;
size_t pos = curr.rfind("/");
if(pos == std::string::npos) return curr;
return curr.substr(pos+1);
}
//接口,负责生成完整的命令行并返回
std::string MakeCommandLine()
{
char command_line[basesize];
snprintf(command_line,basesize,"%s@%s:%s$",GetUsername().c_str(),GetHostname().c_str(),LastDir().c_str());
return command_line;
}
//打印命令行并刷新缓冲区
void PrintCommandLine()
{
printf("%s",MakeCommandLine().c_str());
fflush(stdout);
}
bool GetCommandLine(char Command_buffer[],int size)
{
//我们这里选择使用fgets来读取一行的字符串,可能包含:ls -l -a等
char *result=fgets(Command_buffer,size,stdin);
if(!result)
{
return false;
}
//fgets会将换行符也读取进来,我们需要将其替换为\0
size_t len=strlen(Command_buffer);
if(len==0)
{
return false;
}
Command_buffer[len-1]='\0';
return true;
}
void ParseCommandLine(char Command_buffer[])
{
//每次分析前我们都要重置一下,防止被历史堆积数据影响
memset(gargv,0,sizeof(gargv));
gargc=0;
//我们这里采用strtok来进行切分
gargv[gargc++]=strtok(Command_buffer," ");
while(gargv[gargc++]=strtok(nullptr," "));
//因为我们的while判断条件一定会多执行一次,所以我们后面要把减一
gargc--;
}
bool ExecuteCommandLine()
{
pid_t id = fork();
if(id==0)
{
//子进程代码
execvpe(gargv[0], gargv, genv);
exit(INCORRECT_COMMAND_LINE);//非正常退出,因为exec之后只有失败才会执行到这里
}
int status;
int rid=waitpid(id,&status,0);
if(rid>0)
{
if(WIFEXITED(status))
{
Lastcode=WEXITSTATUS(status);
return true;
}
else
{
Lastcode=EXECUTE_FAIL;
return false;
}
}
Lastcode=EXECUTE_FAIL;
return false;
}
void AddEnv(const char* item)
{
int index=0;
while(genv[index])
{
index++;
}
genv[index]=(char*)malloc(strlen(item)+1);
strncpy(genv[index],item,strlen(item)+1);
genv[++index]=nullptr;
}
//判断是否是内建命令
bool CheckAndExecBuildCommand()
{
if(strcmp(gargv[0],"cd")==0)
{
if(gargc==2)
{
chdir(gargv[1]);
Lastcode=OK;
}
else
{
Lastcode=INCORRECT_COMMAND_LINE;
}
return true;
}
else if(strcmp(gargv[0],"export")==0)
{
if(gargc==2)
{
AddEnv(gargv[1]);
Lastcode=OK;
}
return true;
}
else if(strcmp(gargv[0],"env")==0)
{
for(int i=0;genv[i];i++)
{
printf("%s\n",genv[i]);
}
Lastcode=OK;
return true;
}
else if(strcmp(gargv[0],"echo")==0)
{
if(gargc==2)
{
if(gargv[1][0]=='$')
{
if(gargv[1][1]=='?')
{
printf("%d\n",Lastcode);
Lastcode=0;
}
return true;
}
else
{
printf("%s\n",gargv[1]);
Lastcode=0;
return true;
}
}
else
{
Lastcode=INCORRECT_COMMAND_LINE;
}
}
return false;
}
int main()
{
InitEnv();
char Command_buffer[basesize];
while(1)
{
PrintCommandLine();
if(!GetCommandLine(Command_buffer,basesize))
{
continue;
}
ParseCommandLine(Command_buffer);
if(CheckAndExecBuildCommand())
{
continue;
}
ExecuteCommandLine();
}
return 0;
}