什么是shell?
Shell是一种应用程序,它连接了用户和Linux内核,让用户能够更加高效、安全、低成本地使用Linux内核。 Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。 它接收用户输入的命令并把它送入内核去执行。Shell并不是内核的一部分,而是一个建立在内核基础上的应用程序,与QQ、迅雷、Firefox等其它软件类似。
说大白话:他就是一个进程!作为一个进程,他当然是可以实现的啦,那么我们就简简单单手撕一个简易的进程吧!
怎么实现shell?
shell命令提示符的实现
Shell提示符是Linux系统中的一种表示形式,它出现在用户登录并启动终端模拟包或从Linux控制台登录后。它是用户与Shell进行交互的重要途径,提示符就象征着通往Shell的大门,用户可以在提示符处输入Shell命令。对于普通用户而言,Base shell的默认提示符就是一个美元符号"$",表示等待用户输入命令。如下:
[amazon@iZ7xvfrafhk3mf5qwrf2gxZ myshell]$
要实现这个命令提示符,我们需要获取当前的用户、当前主机以及当前路径,因此根据之前所学的知识,写出以下的函数获取对应的数据:
对于getenv()的回忆—主要用于搜索和返回环境变量的值。这个函数的参数是环境变量的名称,如果对应的环境变量存在,那么getenv函数就会返回一个指向该环境变量值的指针。
//获取用户名 const char* getUsername() { const char* name = getenv("USER"); if (name) return name; else return "none"; } //获取主机名 const char* getHostname() { const char* hostname = getenv("HOSTNAME"); if (hostname) return hostname; else return "none"; } //获取当前路径 const char* getCwd() { const char* cwd = getenv("PWD"); if (cwd) return cwd; else return "none"; }
在得到这些数据后我们就可以组装出一个简易的命令行提示符了!再接收命令行输入的命令就可以完成基本的命令输入。需要注意的是:这里使用fgets是为了将空格也接收进来,为什么return strlen(command)呢?这是判断是否有命令输入,如果没有输入,即只是传了一个回车,那么会返回一个0,后续主函数中会用于接收,通过continue跳过后续的函数(总体是一个死循环)。
//获取用户输入命令 int getUserCommand(char* command, int num) { printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd()); char* r = fgets(command, num, stdin); // 最终你还是会输入\n if (r == NULL) return -1; // "abcd\n" "\n" command[strlen(command) - 1] = '\0'; // 为了将末尾的\n去掉,注意这样并不会越界 ,因为只要是输入都至少会有个\n return strlen(command); }
创建子进程执行命令
从上面的程序我们可知,存储的命令是整段的,对此我们需要进行命令的分割,用strtok按空格进行分割。从前面的知识中我们也知道:shell是创建子进程来让他执行任务的!对此,我们也创建一子进程,通过execvp的程序替换来执行对应的命令。而父进程需要执行的任务是waitpid等待子进程完成任务后储存对应的返回值,用于子进程任务完成怎么样的判断,对此shell的基本功能实现就完成了。但是还是需要改进的!
//按照空格分割命令 void commandSplit(char* in, char* out[]) { int argc = 0; out[argc++] = strtok(in, SEP); while (out[argc++] = strtok(NULL, SEP)); } //总体运行 int execute(char* argv[]) { pid_t id = fork(); if (id < 0) return -1; else if (id == 0) //child { //进程替换 execvp(argv[0], argv); // cd .. exit(1); } else // father { int status = 0;//存储返回值,echo $? pid_t rid = waitpid(id, &status, 0); if (rid > 0) { lastcode = WEXITSTATUS(status);//存储返回值,echo $? } } return 0; }
内建命令的引入
由上图我们可知,当我们使用ls、pwd等等命令时自定义的shell是能够正常运行的,但是对于cd命令还有export是不能运行的。回过头想想,我们是怎么实现shell的呢?我们运用了进程的替换,我们替换了子进程中的进程而上面的shell中并没有实现!对此我们可以肯定的是:这些命令并不是外部的进程!这也引出了—内建命令。内建命令,如其名称所示,是由Shell自身提供的命令。这些命令已经和shell编译为一体,不需要借助外部程序文件来运行。这意味着它们执行速度更快,效率更高。通俗的讲,内建命令实际上就是shell中的一个函数!
内建命令的实现
由于内建命令已经和shell编译为一体,不需要借助外部程序文件来运行,因此我们需要一一的实现对应的内建命令,这里就先实现三个,如果要接着实现可以接着else if 实现下去。详细解释见代码:
//获取家目录 char* homepath() { char* home = getenv("HOME"); if (home) return home; else return (char*)"."; } void cd(const char* path)//用于cd命令 { chdir(path);//用于改变当前工作目录 char tmp[1024];//临时储存,但不能直接使用,需要全局变量,否则栈帧销毁也会销毁 getcwd(tmp, sizeof(tmp));//获取当前工作目录的绝对路径 sprintf(cwd, "PWD=%s", tmp); //输出到全局变量中,保证不会失效 putenv(cwd);//改变或增加环境变量的内容 } // 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数! // 1->yes, 0->no, -1->err int doBuildin(char* argv[]) { if (strcmp(argv[0], "cd") == 0)//通过改变当前环境变量的内容从而改变->命令行提示符路径 { char* path = NULL; if (argv[1] == NULL) path = homepath();//cd 回到家目录 else path = argv[1];//根据命令到指定路径 cd(path); return 1; } else if (strcmp(argv[0], "export") == 0) { if (argv[1] == NULL) return 1; strcpy(enval, argv[1]);//同理需要全局变量防止失效 putenv(enval); // 改变环境变量 return 1; } else if (strcmp(argv[0], "echo") == 0) { if (argv[1] == NULL) {//echo 输出换行 printf("\n"); return 1; } if (*(argv[1]) == '$' && strlen(argv[1]) > 1) {//根据指令输出 char* val = argv[1] + 1; // $PATH $? if (strcmp(val, "?") == 0) { printf("%d\n", lastcode); lastcode = 0; } else { const char* enval = getenv(val); if (enval) printf("%s\n", enval); else printf("\n"); } return 1; } else {//只是输出到屏幕 printf("%s\n", argv[1]); return 1; } } else if (0) {}//接下来的内建命令 return 0; }
shell的拼接
shell在Linux中是一直运行的,因此为一个死循环!,我们定义一个usercommand字符数组用于接收储存从命令行收到的初步命令,定义一个字符串数组来接收通过commandSplit分割后的字符串,接下来区分是否为内建命令,按照argv里面的命令进行执行程序替换或者执行内建命令。
int main() { while (1) { char usercommand[NUM]; char* argv[SIZE]; // 1. 打印提示符&&获取用户命令字符串获取成功 int n = getUserCommand(usercommand, sizeof(usercommand)); if (n <= 0) continue; // 2. 分割字符串 // "ls -a -l" -> "ls" "-a" "-l" commandSplit(usercommand, argv); // 3. check build-in command n = doBuildin(argv); if (n) continue; // 4. 执行对应的命令 execute(argv); } }
shell的总体代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define NUM 1024 #define SIZE 64 #define SEP " " char cwd[1024]; char enval[1024]; // for test int lastcode = 0; //获取家目录 char* homepath() { char* home = getenv("HOME"); if (home) return home; else return (char*)"."; } //获取用户名 const char* getUsername() { const char* name = getenv("USER"); if (name) return name; else return "none"; } //获取主机名 const char* getHostname() { const char* hostname = getenv("HOSTNAME"); if (hostname) return hostname; else return "none"; } //获取当前路径 const char* getCwd() { const char* cwd = getenv("PWD"); if (cwd) return cwd; else return "none"; } //获取用户输入命令 int getUserCommand(char* command, int num) { printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd()); char* r = fgets(command, num, stdin); // 最终你还是会输入\n if (r == NULL) return -1; // "abcd\n" "\n" command[strlen(command) - 1] = '\0'; // 为了将末尾的\n去掉,注意这样并不会越界 ,因为只要是输入都至少会有个\n return strlen(command); } //按照空格分割命令 void commandSplit(char* in, char* out[]) { int argc = 0; out[argc++] = strtok(in, SEP); while (out[argc++] = strtok(NULL, SEP)); } //总体运行 int execute(char* argv[]) { pid_t id = fork(); if (id < 0) return -1; else if (id == 0) //child { //进程替换 execvp(argv[0], argv); // cd .. exit(1); } else // father { int status = 0;//存储返回值,echo $? pid_t rid = waitpid(id, &status, 0); if (rid > 0) { lastcode = WEXITSTATUS(status);//存储返回值,echo $? } } return 0; } void cd(const char* path) { chdir(path);//用于改变当前工作目录 char tmp[1024];//临时储存,但不能直接使用,需要全局变量,否则栈帧销毁也会销毁 getcwd(tmp, sizeof(tmp));//获取当前工作目录的绝对路径 sprintf(cwd, "PWD=%s", tmp); //输出到全局变量中,保证不会失效 putenv(cwd);//改变或增加环境变量的内容 } // 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数! // 1->yes, 0->no, -1->err int doBuildin(char* argv[]) { if (strcmp(argv[0], "cd") == 0) { char* path = NULL; if (argv[1] == NULL) path = homepath(); else path = argv[1]; cd(path); return 1; } else if (strcmp(argv[0], "export") == 0) { if (argv[1] == NULL) return 1; strcpy(enval, argv[1]);//同理需要全局变量防止失效 putenv(enval); // ??? return 1; } else if (strcmp(argv[0], "echo") == 0) { if (argv[1] == NULL) { printf("\n"); return 1; } if (*(argv[1]) == '$' && strlen(argv[1]) > 1) { char* val = argv[1] + 1; // $PATH $? if (strcmp(val, "?") == 0) { printf("%d\n", lastcode); lastcode = 0; } else { const char* enval = getenv(val); if (enval) printf("%s\n", enval); else printf("\n"); } return 1; } else { printf("%s\n", argv[1]); return 1; } } else if (0) {} return 0; } int main() { while (1) { char usercommand[NUM]; char* argv[SIZE]; // 1. 打印提示符&&获取用户命令字符串获取成功 int n = getUserCommand(usercommand, sizeof(usercommand)); if (n <= 0) continue; // 2. 分割字符串 // "ls -a -l" -> "ls" "-a" "-l" commandSplit(usercommand, argv); // 3. check build-in command n = doBuildin(argv); if (n) continue; // 4. 执行对应的命令 execute(argv); } }
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!