前言
前面我们讲述了进程的相关知识,包括进程创建、进程等待、进程替换等,这些我们都在Linux上进行了测试,并且通常使用的shell来执行命令,那么我们能不能自己来实现一个简单的shell呢?
我们知道在shell上执行命令时,其原理不过也只是调用和执行文件罢了,也就是创建进程来执行程序,而shell一般是不退出的,那么我们现在开始玩一下
01. 框架搭建
命令行解释器一定是一个常驻内存的进程,不退出,所以我们使用while包起来
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #define NUM 1024 #define SIZE 32 #define SEP " " //保存完整的命令行字符串 char cmd_line[NUM]; //保存打散之后的命令行字符串 char *g_argv[SIZE]; // 写一个环境变量的buffer,用来测试 char g_myval[64]; // shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令 int main() { extern char**environ; //0. 命令行解释器,一定是一个常驻内存的进程,不退出 while(1) { } }
02. 打印提示信息
参考shell的提示信息:
它们都有各自的含义和获取方式,但是这里为了简化,不考虑这些细枝末节,由大家自己改进!
我这里就写死了哦~
我们可以直接使用printf函数打印,但是会有一个问题,如果设置了\n,它会换行,但是我们使用shell时并不会换行,那么我们就需要用到fflush函数来冲刷缓存区。
//1. 打印出提示信息 [venus@localhost myshell]# printf("[root@localhost myshell]# "); fflush(stdout);
03. 获取用户键盘输入
如何获取用户在命令行的输入呢?
我们用一个数组来存储命令,使用fgets函数来获取输入
步骤:
- 先初始化数组
- 获取存储命令
- 将最后的回车符号设置为'\0'
//2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"] if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } cmd_line[strlen(cmd_line)-1] = '\0';
04. 命令行字符串解析
到这一步,我们已经将命令行的字符串存储到数组中了,接下来就是解析它
步骤:
- 这里要使用strtok函数来裁剪字符串
- 将存储的命令和系统内部命令做比对,如果有就执行
g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串 int index = 1; if(strcmp(g_argv[0], "ls") == 0) { g_argv[index++] = "--color=auto";//加入配色 } while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL
05. 创建子进程执行命令
怎么知道要调用的程序在哪里呢?
直接使用进程替换,使用execvp函数,它可以直接使用环境变量不用自己写了,也就是直接掉用系统中的指令的程序来使用即可。
为什么要替换?
一切和应用场景有关,我们有时候必须要让子进程执行新的程序
环境变量相关的数据,会被替换吗??
没有!它不会被替换,它会把父进程的环境变量拷贝继承过来,它具有全局属性
pid_t id = fork(); if(id == 0) //child { printf("下面功能让子进程进行的\n"); execvp(g_argv[0], g_argv); // ls -a -l -i exit(1); } //father int status = 0; pid_t ret = waitpid(id, &status, 0); if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
此时程序基本功能就已经实现了
但是,我们发现一个问题,使用cd命令时,他的路径不会改变,这是个bug
原因是:
- 在cd的时候,自己写的shell都会执行execvp,它只会影响子进程的路径
- 但是我们需要改变父进程的路径,所以像cd这种命令,我们不想让子进程去执行它而让父进程去执行它
- 这种让父进程自己执行的命令叫做内置命令、内建命令,它的本质是shell中的一个函数调用
我们来修改下功能
06. 内置命令 —— cd
这里可以使用chdir函数来实现
chdir函数可以改变文件路径
我们可以使用下面代码,使得cd命令的使用合理,但是可能其他类似的命令也会出现相似的bug,需要一一比对实现,这里仅针对cd命令
if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute { if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd .. continue; }
07. 内置命令 —— export
上面我们讲了cd的bug,而export也和cd一样,也需要进行处理,export的作用是导入环境变量,我们既不想覆盖父进程的环境变量,又想导入自己的环境变量,该怎么做呢?
代码如下:
//导入环境变量 //比较第一个是不是export if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL) { strcpy(g_myval, g_argv[1]);//是的就取出后面的值 int ret = putenv(g_myval);//将它导入环境变量中 if(ret == 0) printf("%s export success\n", g_argv[1]);//如果导入成功就打印出来 continue; }
shell 执行的命令通常有两种
- 第三方提供的对应的在磁盘中具有二进制文件的可执行程序(由子进程执行)
- shell内部自己实现的方法,由自己(父进程)来执行,有些命令就是要影响shell本身,如改变路径的(cd、export),shell代表的是用户。
shell的环境变量从哪里来的?(了解)
环境变量是写在配置文件中的,shell启动的时候,通过读取配置文件获得的起始环境变量
08. 类似ll这种别名命令无法识别
ll是ls -l的别名
想要支持就要当识别到ll时执行ls命令即可
if(strcmp(g_argv[0], "ll") == 0) { g_argv[0] = "ls"; g_argv[index++] = "-l"; g_argv[index++] = "--color=auto"; }
系统中肯定不是这样实现的,但是大致原理相同
后记
最后我们就实现了一个简易的shell解释器
全部代码如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #define NUM 1024 #define SIZE 32 #define SEP " " //保存完整的命令行字符串 char cmd_line[NUM]; //保存打散之后的命令行字符串 char *g_argv[SIZE]; // 写一个环境变量的buffer,用来测试 char g_myval[64]; // shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令 int main() { extern char**environ; //0. 命令行解释器,一定是一个常驻内存的进程,不退出 while(1) { //1. 打印出提示信息 [whb@localhost myshell]# printf("[root@localhost myshell]# "); fflush(stdout); memset(cmd_line, '\0', sizeof cmd_line); //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"] if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } cmd_line[strlen(cmd_line)-1] = '\0'; //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i" // export myval=105 g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串 int index = 1; if(strcmp(g_argv[0], "ls") == 0) { g_argv[index++] = "--color=auto"; } if(strcmp(g_argv[0], "ll") == 0) { g_argv[0] = "ls"; g_argv[index++] = "-l"; g_argv[index++] = "--color=auto"; } while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL) { strcpy(g_myval, g_argv[1]); int ret = putenv(g_myval); if(ret == 0) printf("%s export success\n", g_argv[1]); continue; } //4.内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令 //内建命令本质其实就是shell中的一个函数调用 if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute { if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd .. continue; } //5. fork() pid_t id = fork(); if(id == 0) //child { printf("下面功能让子进程进行的\n"); printf("child, MYVAL: %s\n", getenv("MYVAL"));//测试环境变量 printf("child, PATH: %s\n", getenv("PATH"));//测试环境变量 //环境变量相关的数据,会被替换吗??没有! execvp(g_argv[0], g_argv); // ls -a -l -i exit(1); } //father int status = 0; pid_t ret = waitpid(id, &status, 0); if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status)); } }