为什么实现一个自己 shell?
你是一个 Window/Android 用户,你可以直接用图形化桌面直接双击跑一个程序,没压力吧,轻松吧。
图形化工具为我们屏蔽了底层进程调用的细节。或者你是一个 Linux 用户,你喜欢用 bash 执行 grep
命令来查找文件中的内容,喜欢用 git
来提交代码,喜欢用 ls
来查看目录内容,有时你还会去使用
将多个命令一起使用:cat cat.txt | grep "smelly cat" | wc -c
。作为一个 Linux Hacker,你理应
当了解这些图形化工具背后的原理,利用它实现一些更好玩的事情。
TASK
打造一个绝无伦比的 xxx-super-shell
(xxx
是你的名字),它能实现下面这些功能:
- 实现 管道 (也就是
|
)
- 实现 输入输出重定向(也就是
<
>
>>
)
- 实现 后台运行(也就是
&
)
- 实现
cd
,要求支持能切换到绝对路径,相对路径和支持 cd -
- 屏蔽一些信号(如
ctrl + c
不能终止)
- 界面美观
- 开发过程记录、总结、发布在个人博客中
要求:
- 不得出现内存泄漏,内存越界等错误
- 学会如何使用 gdb 进行调试,使用 valgrind 等工具进行检测
知识要点
- 懂得如何使用 shell
- 理解 shell 原理
- Linux系统编程:进程控制
- gdb
- valgrind
直接讲解代码
分割命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void parse_args(char* line, char** args) { char* token = strtok(line, " \t\r\n\a"); args[0] = token; int i = 1; if(strcmp(args[0],"ls")==0) { args[i++]=(char*)"--color=auto"; } if(strcmp(args[0],"ll")==0) { args[0]=(char*)"ls"; args[i++]=(char*)"-l"; args[i++]=(char*)"--color=auto"; } while (token != NULL) { token = strtok(NULL, " \t\r\n\a"); args[i++] = token; } COUNT=i-1; }
|
这里使用了string.h库中的strtok函数(详情见man手册)来分割字符串,方便之后使用execvp函数来执行命令,这里检测如果命令是ls的话,那么就加上–color=auto,这是为了让ls的结果具有颜色显示的效果,使界面更加美观。并且将ll识别为ls -l
设置后台运行与管道的标志
1 2 3 4 5 6 7 8 9 10 11 12 13
| void parse(char *line){ HT=false; PP=false; for(int i=0;i<strlen(line);i++){ if(line[i]=='&'){ HT=true; line[i]=' '; } if(line[i]=='|'){ PP=true; } } }
|
这里的HT和PP都是bool类型,每次进入函数都先熄灭它们两个,如果遍历命令中出现了&或|,那就点亮它们
重定向状态获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| char *CheckRedir(char *start) { char *end=start+strlen(start)-1; while(end>=start) { if(*end=='>') { if(*(end-1)=='>') { redir_status=APPEND_REDIR; *(end-1)='\0'; end++; while(*end==' ') ++end; break; } redir_status=OUTPUT_REDIR; *end='\0'; end++; while(*end==' ') ++end; break; } else if(*end=='<') { redir_status=INPUT_REDIR; *end='\0'; end++; while(*end==' ') ++end; break; } else { end--; } } if(end>=start) { return end; } else { return NULL; } }
|
这里先看一下我的宏
1 2 3 4 5 6
| #define INPUT_REDIR 1 #define OUTPUT_REDIR 2 #define APPEND_REDIR 3 #define NONE_REDIR 0
int redir_status=NONE_REDIR;
|
四种状态分别表示 重定向输入,输出,追加和无重定向工作,初始化状态为无重定向工作
这里观察命令中是否存在>>,>,<等标记从而确定重定向的状态,并返回需要重定向的文件名
找管道和执行管道(初始版/笑哭)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| int has_pipe(char** args, int* pipe_position) { for (int i = 0; args[i] != NULL; i++) { if (strcmp(args[i], "|") == 0) { *pipe_position = i; return 1; } } return 0; }
void run_command_with_pipe(char** args1, char** args2) { int pipefd[2]; if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); }
pid_t pid1 = fork(); if (pid1 == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid1 == 0) { close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); close(pipefd[1]); execvp(args1[0], args1); perror("execvp"); exit(EXIT_FAILURE); }
pid_t pid2 = fork(); if (pid2 == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid2 == 0) { close(pipefd[1]); dup2(pipefd[0], STDIN_FILENO); close(pipefd[0]); execvp(args2[0], args2); perror("execvp"); exit(EXIT_FAILURE); }
close(pipefd[0]); close(pipefd[1]); waitpid(pid1, NULL, 0); waitpid(pid2, NULL, 0); }
|
执行管道
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| void DoPipe(char **argv, int count) { pid_t pid; int ret[10]; int number=0; for(int i=0;i<count;i++) { if(!strcmp(argv[i],"|")) { ret[number++]=i; } } int cmd_count=number+1; char* cmd[cmd_count][10]; for(int i=0;i<cmd_count;i++) { if(i==0) { int n=0; for(int j=0;j<ret[i];j++) { cmd[i][n++]=argv[j]; } cmd[i][n]=NULL; } else if(i==number) { int n=0; for(int j=ret[i-1]+1;j<count;j++) { cmd[i][n++]=argv[j]; } cmd[i][n]=NULL; } else { int n=0; for(int j=ret[i-1]+1;j<ret[i];j++) { cmd[i][n++]=argv[j]; } cmd[i][n]=NULL; } } int fd[number][2]; for(int i=0;i<number;i++) { pipe(fd[i]); } int i=0; for(i=0;i<cmd_count;i++) { pid=fork(); if(pid==0) break; } if(pid==0) { if(number) { if(i==0) { dup2(fd[0][1],1); close(fd[0][0]); for(int j=1;j<number;j++) { close(fd[j][1]); close(fd[j][0]); } } else if(i==number) { dup2(fd[i-1][0],0); close(fd[i-1][1]); for(int j=0;j<number-1;j++) { close(fd[j][1]); close(fd[j][0]); } } else { dup2(fd[i-1][0],0); close(fd[i-1][1]); dup2(fd[i][1],1); close(fd[i][0]); for(int j=0;j<number;j++) { if(j!=i&&j!=(i-1)) { close(fd[j][0]); close(fd[j][1]); } } } } execvp(cmd[i][0],cmd[i]); perror("execvp"); exit(1); } for(i=0;i<number;i++) { close(fd[i][0]); close(fd[i][1]); } for(int j=0;j<cmd_count;j++) wait(NULL); }
|
执行命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| void run_command(char** args) { if(PP){ DoPipe(args,COUNT); } else { pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { execvp(args[0], args); perror("execvp"); exit(EXIT_FAILURE); } else { wait(NULL); } } }
|
这里如果PP亮着就说明有管道,就执行dopipe,没有管道就是单一命令,直接fork(),子进程execvp执行命令,父进程wait就行。另外这里注释掉的是我曾经的只能处理一个管道的代码。
主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| int main() { char line[MAX_LINE]; char* args[MAX_ARGS]; char prior[100]={'\0'}; args[0]="\0"; signal(SIGINT,SIG_IGN); int x=dup(0),y=dup(1); while (1) { printf("[dyx-super-shell]# "); if (fgets(line, MAX_LINE, stdin) == NULL) { perror("fgets"); exit(EXIT_FAILURE); } if(strcmp(line,"\n")==0||!line) continue; line[strlen(line)-1]='\0'; parse(line); char *sep=CheckRedir(line); parse_args(line, args); if (args[0] == NULL) { continue; } if (strcmp(args[0], "exit") == 0) { break; } if (strcmp(args[0], "cd") == 0) { if (args[1] && strcmp(args[1], "-") == 0) { if (prior) { char buf[100]; getcwd(buf, 100); printf("%s\n", prior); chdir(prior); strcpy(prior, buf); } else { printf("bash: cd: OLDPWD 未设定\n"); } } else { getcwd(prior, 100); chdir(args[1]); } continue; } if(HT){ int fd=-1; fd=open("/dev/null",O_WRONLY); dup2(fd,0); dup2(fd,1); close(fd); } if(sep!=NULL) { int fd=-1; switch(redir_status) { case INPUT_REDIR: fd=open(sep,O_RDONLY); dup2(fd,0); close(fd); break; case OUTPUT_REDIR: fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666); dup2(fd,1); close(fd); break; case APPEND_REDIR: fd=open(sep,O_WRONLY|O_APPEND|O_CREAT,0666); dup2(fd,1); close(fd); break; default: printf("bug?\n"); break; } } run_command(args); dup2(x,0); dup2(y,1); } return 0; }
|
讲一下重要的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| if (strcmp(args[0], "exit") == 0) { break; } if (strcmp(args[0], "cd") == 0) { if (args[1] && strcmp(args[1], "-") == 0) { if (prior) { char buf[100]; getcwd(buf, 100); printf("%s\n", prior); chdir(prior); strcpy(prior, buf); } else { printf("bash: cd: OLDPWD 未设定\n"); } } else { getcwd(prior, 100); chdir(args[1]); } continue; }
|
如果命令是exit和cd,直接执行,不需要fork,这里实现cd -就是在cd前现将此时的位置存起来,下次cd - 时直接切回来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| if(sep!=NULL) { int fd=-1; switch(redir_status) { case INPUT_REDIR: fd=open(sep,O_RDONLY); dup2(fd,0); close(fd); break; case OUTPUT_REDIR: fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666); dup2(fd,1); close(fd); break; case APPEND_REDIR: fd=open(sep,O_WRONLY|O_APPEND|O_CREAT,0666); dup2(fd,1); close(fd); break; default: printf("bug?\n"); break; } }
|
这里将重定向的工作做好
1 2 3 4 5 6 7
| if(HT){ int fd=-1; fd=open("/dev/null",O_WRONLY); dup2(fd,0); dup2(fd,1); close(fd); }
|
有后台运行就重入到dev/null
源码地址:https://github.com/mejomejo/mytask/blob/main/myshell/dyx.c
参考资料
- man 手册.
- MichaelKerrisk.Linux/UNIX系统编程手册[M].北京:人民邮电出版社.
- W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社.