(Linux操作系统)自定义shell的实现
讲自定义shell之前我们先看一个东西,那就是进程替换,我们想要父进程fork之后的子进程之后运行一个全新的程序那该怎么办呢?
这里就要用一个叫做进程替换的一个东西了,程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间。
exec进程替换函数
进程替换就是 替换当前进程的代码和数据段,使进程执行新的程序,PID 保持不变。
直接把当前程序的上下文数据替换为要执行的程序,一个进程想要执行其他的进程时,可以使用进程替换,而不用创建子进程分配进程id,这样可以提高程序的效率。
进程替换的原理是通过exec系统调用,用一个新的程序映像替换当前进程的代码和数据段,使进程的执行从新程序的入口点开始。在此过程中,进程的PID、开放的文件描述符、环境变量等资源得以保留,从而实现进程的替换而不改变其标识和部分状态。
我们下面来看看函数接口
他们都包含在unstd.h这个头文件里面;
int execl ( const char *path, const char *arg, ...); | 第一个参数是path(目的是让函数到指定path路径下面寻找要替换的程序)后面这些是可变参数,可以传递给新程序的其他参数。它们会被传递给新程序的主函数main 作为argv[1] 、argv[2] 等。 |
int execlp ( const char *file, const char *arg, ...); | 第一个参数这是要执行的文件名。与execl不同,execlp不会直接使用这个路径去查找文件。而是会在环境变量PATH指定的目录列表中搜索这个文件名。 第二个是这是传递给新程序的第一个参数,通常应该是新程序的名称。在新程序中,这个参数会被作为argv[0],后面以此类推 |
int execle ( const char *path, const char *arg, ..., char * const envp[]); | 第一个是这是要执行的文件的路径。execle会直接使用这个路径去查找文件,不会像execlp那样在环境变量PATH中搜索。第二个是这是传递给新程序的第一个参数,通常应该是新程序的名称。在新程序中,这个参数会被作为 |
int execv ( const char *path, char * const argv[]); | 第一个是这是指向要执行的文件路径的指针。 |
int execvp ( const char *file, char * const argv[]); | 第一个是这是指向要执行的文件名的指针。 |
int execve ( const char *path, char * const argv[], char * const envp[]) | 函数是exec家族中最基本的函数,用于执行进程替换。它用新的程序替换当前进程的代码段、数据段和堆栈段。 它会直接使用 同时,它还会传递 |
然而上面提到的函数除了最后一个都是进行库封装之后的函数,最后一个才是系统提供的接口
实现自定义shell
首先看看下面的设计思路图片
第一步:打印我们的终端提示符
我们登陆linux的时候一般都会打印一个
这个的提示符界面
我们首先来实现这个功能,首先分析一下这个界面的组成部分,第一个是登陆的用户名其次是登陆的机器名称,最后是当前所在的目录。
这三个东西可以从环境变量上面获取,我们可以调用get_env()函数来获取对应的环境变量,他会返回一个字符指针,指向一个字符串,我们得到这个三个字符串之后我们可以用snprintf()函数对我们获得的字符串进行格式化一下(这里不是打印)来看看下面的代码
const char* user()
{char* user = nullptr;user = getenv("USER");return user == nullptr?"NONE!!":user;
}
//获得当前路径
const char* pwd()
{char* pwd = nullptr;// pwd = getenv("PWD");pwd = getcwd(cwdenv,sizeof(cwdenv));return pwd == nullptr?"NONE!!":pwd;
}
//获得家目录
const char* get_home()
{char* home = nullptr;home = getenv("HOME");return home == nullptr?"":home;
}
//获得主机名称
const char* hostname()
{char* hostname = nullptr;hostname = getenv("HOSTNAME");return hostname == nullptr?"NONE!!":hostname;
}
这里的获取pwd为什么没调用getenv()而是调用getcwd(),这个函数是一个系统调用函数,会返回当前所在的文件路径,而环境变量里面的当前路径的获得是在我们切换目录之后再获得的,有一个滞后性,而这里我们可以立即得到我们的当前路径
得到这三个字符串之后,我们就要按照我的指定格式进行格式化了,使用snprintf()函数进行格式化
第一个参数是一个字符数组的地址,第二个是大小,第三个是一个宏# define FORMAT "[%s@%s %s ]:)#-->"这就是按照我们指定的格式进行格式化,最后是可变参数,我们按顺序传递了三个字符串进去。这个cmd字符数组定义在主函数里面的。
void init_print_cmd_line(char* cmd,int size)
{snprintf(cmd,size,FORMAT,user(),hostname(),pwd());
}
当我们格式化好之后,我们就可以打印了,直接传一个字符串的一个首地址过去
void printf_cmd(char* cmd)
{fflush(stdout);printf("%s",cmd);
}
做完这些之后,在主函数内部合适的位置调用我们就可以打印这个命令提示符了。
第二步 处理输入的命令
看看下面的代码
bool input_cmd(char* out,int size)
{char* ret = fgets(out,size,stdin);if(ret == nullptr){return false ;}int tmp = strlen(out)-1;if(tmp == 0){return false;}out[tmp] = 0;return true;
}
我们这个函数传递了两个参数,第一个是一个字符数组,第二个是大小
首先这里调用fgets函数来从stdin标准流里面读入我们输入的数据,然后放到out数组里面。注意这里的fgets函数会把\n换行符给读入进去,所以我们最后需要特殊处理一下把数组末尾的\n置为0。
最后我们就处理好了,这里假如我们输入 "ls" "-al"在我们的数组里面是这样存放的 "ls -al" 所以还需要特殊处理一下
bool input_cmd(char* out,int size)
{char* ret = fgets(out,size,stdin);if(ret == nullptr){return false ;}int tmp = strlen(out)-1;if(tmp == 0){return false;}out[tmp] = 0;return true;
}
我们来看看处理的代码
bool cmd_to_argv(char* inputcmd)
{my_argc = 0;my_argv[my_argc++] = strtok(inputcmd,FLAG);while((bool)(my_argv[my_argc++] = strtok(NULL,FLAG)));my_argc--;return true;
}
首先我们知道我们的shell里面会有两张表一个是命令行参数列表,一个是环境变量表,这里设计的函数就是把我们的输入的命令按照空格的方式进行分割,这里调用strok函数进行字符串分割,依次放到我们自己定义的my_argv[]全局数组里面,strok函数进行字符分割的时候第一次调用就要传要分割字符串的数组的首地址,然后需要传入以什么分割的方式进行分割判断,如需再次分割下一次首参数就传入NULL
然后就一个循环一直分割就完事
最后我们的my_argc要--一下因为我们前面一直都是使用过后再++,所以就多统计了一个所以要--。我们来看看效果
做完这一步之后我们就可以开始进行根据命令进行程序替换了
第三步:创建子进程程序替换
我们来看看下面的代码
void execute()
{pid_t id = fork();if(id == 0){execvp(my_argv[0],my_argv);}else if(id < 0){printf("fall!!\n");}int status = 0;pid_t retid = waitpid(id,&status,0);if(retid > 0){lastcode_exit_status = WEXITSTATUS(status);}}
我们输入完命令之后就需要通过父进程我们自己设计的bash来创建子进程来执行我们的程序,
所以需要fork创建一个然后我们父进程进行等待,等待我们子进程结束,这个代码就很简单,当写完上述逻辑之后我们的bash就能执行除了内建命令的命令了,然后我们下面的这个WEXITSTATUS(status);宏函数就是用来获取子进程退出信息的这个我们等下处理内建命令echo的时候要使用。
第四步:处理内建命令
cd命令
我们这里就只处理两个内建命令:echo,cd命令,我们要处理这两个命令之前,我们需要判断输入的命令是不是内建命令,一个很简单的代码就搞定
bool cheakbulitin()
{std::string str = my_argv[0];if(str == "cd"){cd();return true;}else if (str == "echo"){echo();return true;}return false;
}
然后我们先说一下cd命令,看看下面的代码
这里我们就处理两个最常用的一个是cd不带任何路径,第二个是cd到哪去
bool cd()
{if(my_argc == 1){std::string home = get_home();if(home.empty()){return true;}chdir(home.c_str());}else {std::string where = my_argv[1];if(where == "~"){//.......return true;}else if(where == "-"){//.......return true;}else {chdir(where.c_str());return true;}}return false;
}
首先说第一个cd不带任何路径,就直接切换回到自己的家目录上,首先使用get_home函数获取当前用户的家目录,然后这里我们使用chdir函数来帮助我们切换当前所在的路径,很简单的一个逻辑就完成了 然后我们处理切第二个,我们的命令输入的格式一般是 cd /usr/bin 这种类似的,要切换的路径是存在后面my_argv[1]这个里面,我们只需要把这个字符串给传入chdir函数就行了,就实现了我们两个cd的最核心的功能。
echo命令
来先看看代码
bool echo()
{if(my_argc == 2){std::string tmp = my_argv[1];if(tmp == "$?"){printf("%d\n",lastcode_exit_status);return true;}else if(tmp[0] == '$'){//输出指定的环境变量std::string envname = tmp.substr(1);char* ret = getenv(envname.c_str()); if(ret == nullptr){std::cout<<"找不到指定的环境变量!"<<std::endl;return true;}std::cout<<ret<<std::endl;}else {printf("-->%s\n",tmp.c_str());}}return false;
}
首先我们需要判断my_argc是不是等于2,如果是就代表我们要让echo做事情,首先就需要判断echo 后面跟的是不是特殊的转意字符,如果是$?就代表我们要获取上一个子进程退出的状态对这个进行特殊处理,如果是$就代表我们要输出指定的环境变量,这里调用c++string里面的substr函数进行字符串截取,如果什么都不带就代表我们就是纯在屏幕上打印内容
完成这些之后我们的shell就已经能进行基本的正常使用了
最后我们来模拟一下shell的启动过程
第五步模拟shell启动过程
我们知道shell启动的时候会到配置文件里面读取环境变量表,这个配置文件是用shell脚本语言编写的,我们这里就用C语言进行实现,我们来看看下面的代码
void init_env()
{genv_s = 0;extern char** environ;memset(g_env,0,sizeof(g_env));for(int i = 0;environ[i];i++){char* tmp = (char*)malloc(strlen(environ[i])+1);if(tmp == nullptr){perror("malloc_fail");exit(1);}g_env[i] = tmp;strcpy(g_env[i],environ[i]);genv_s++;}g_env[genv_s++] = (char*)"qqqqqqqtest=HAHAHAHAHAHAHAHHAHAHA11111111111111111111111111111111111111";g_env[genv_s] = NULL;for(int i = 0;g_env[i];i++){putenv(g_env[i]);}environ = g_env;
}
首先我们要声明这个environ这个全局二级指针变量,这个是指向一个char*的环境变量表的,这个是由系统提供的,假设我们这个指针没指向任何地方,然后我们进行模拟从系统配置文件里面读取环境变量,然后我们自己需要首先定义一个全局g_env[]环境变量表,然后对这个表里面进行初始化,然后把环境变量表里面的ket=value结构的字符串依次开空间放入到我们提供的环境变量表里面,最后一个元素设置为NULL,这里我们自己手动输入了一个环境变量进行检测 ,最后调用putenv进行导成环境变量,把environ这个指针指向我们的环境变量表,这样我们的环境变量都会被子进程给继承,我们来检测一下,看看我们自己输入的环境变量存不存在
这样我们一个简单的shell就完成了
下面是源码
源码
# include<cstdio>
# include<cstdlib>
# include<cstring>
# include<unistd.h>
# include<sys/types.h>
# include<wait.h>
# include<string>
# include<iostream># define SIZE 1024
# define FORMAT "[%s@%s %s ]:)#-->"
# define ARGVSIZE 128
# define FLAG " "
# define ENVSIZE 1024
# define GENVSIZE 128
int lastcode_exit_status = 0;
int my_argc = 0;
char* my_argv[ARGVSIZE];
char* g_env[GENVSIZE];
char cwdenv[1024];int genv_s = 0;const char* user()
{char* user = nullptr;user = getenv("USER");return user == nullptr?"NONE!!":user;
}const char* pwd()
{char* pwd = nullptr;// pwd = getenv("PWD");pwd = getcwd(cwdenv,sizeof(cwdenv));return pwd == nullptr?"NONE!!":pwd;
}const char* get_home()
{char* home = nullptr;home = getenv("HOME");return home == nullptr?"":home;
}const char* hostname()
{char* hostname = nullptr;hostname = getenv("HOSTNAME");return hostname == nullptr?"NONE!!":hostname;
}void init_print_cmd_line(char* cmd,int size)
{snprintf(cmd,size,FORMAT,user(),hostname(),pwd());
}void printf_cmd(char* cmd)
{fflush(stdout);printf("%s",cmd);
}bool input_cmd(char* out,int size)
{char* ret = fgets(out,size,stdin);if(ret == nullptr){return false ;}int tmp = strlen(out)-1;if(tmp == 0){return false;}out[tmp] = 0;return true;
}bool cmd_to_argv(char* inputcmd)
{my_argc = 0;my_argv[my_argc++] = strtok(inputcmd,FLAG);while((bool)(my_argv[my_argc++] = strtok(NULL,FLAG)));my_argc--;return true;
}bool cd()
{if(my_argc == 1){std::string home = get_home();if(home.empty()){return true;}chdir(home.c_str());}else {std::string where = my_argv[1];if(where == "~"){//.......return true;}else if(where == "-"){//.......return true;}else {chdir(where.c_str());return true;}}return false;
}bool echo()
{if(my_argc == 2){std::string tmp = my_argv[1];if(tmp == "$?"){printf("%d\n",lastcode_exit_status);return true;}else if(tmp[0] == '$'){//输出指定的环境变量std::string envname = tmp.substr(1);char* ret = getenv(envname.c_str()); if(ret == nullptr){std::cout<<"找不到指定的环境变量!"<<std::endl;return true;}std::cout<<ret<<std::endl;}else {printf("-->%s\n",tmp.c_str());}}return false;
}bool cheakbulitin()
{std::string str = my_argv[0];if(str == "cd"){cd();return true;}else if (str == "echo"){echo();return true;}return false;
}void execute()
{pid_t id = fork();if(id == 0){execvp(my_argv[0],my_argv);}else if(id < 0){printf("fall!!\n");}int status = 0;pid_t retid = waitpid(id,&status,0);if(retid > 0){lastcode_exit_status = WEXITSTATUS(status);}}
void init_env()
{genv_s = 0;extern char** environ;memset(g_env,0,sizeof(g_env));for(int i = 0;environ[i];i++){char* tmp = (char*)malloc(strlen(environ[i])+1);if(tmp == nullptr){perror("malloc_fail");exit(1);}g_env[i] = tmp;strcpy(g_env[i],environ[i]);genv_s++;}g_env[genv_s++] = (char*)"qqqqqqqtest=HAHAHAHAHAHAHAHHAHAHA11111111111111111111111111111111111111";g_env[genv_s] = NULL;for(int i = 0;g_env[i];i++){putenv(g_env[i]);}environ = g_env;
}int main()
{char cmd[SIZE];char input_to_cmd[SIZE]; init_env();while(1){init_print_cmd_line(cmd,sizeof((cmd)));printf_cmd(cmd);if(!(input_cmd(input_to_cmd,sizeof(input_to_cmd)))){continue;}cmd_to_argv(input_to_cmd);if(cheakbulitin()){continue;}//printf_argv();execute();//创建进程}return 0;
}