TASK
打造一个绝无伦比的 xxx-super-shell (xxx 是你的名字),它能实现下面这些功能:
实现 管道 (也就是 |)
实现 输入输出重定向(也就是 < > >>)
1 2 3 4
| cat < 1.txt | grep -C 10 abc | grep -L efd | tac >> 2.txt
cat < 1.txt | grep -C 10 abc > test1.txt | test2.txt > grep -L efd | tac >> 2.txt
|
要求:
- 不得出现内存泄漏,内存越界等错误
- 学会如何使用 gdb 进行调试,使用 valgrind 等工具进行检测
测试用例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| xxx@xxx ~ $ ./xxx-super-shell xxx@xxx ~ $ echo ABCDEF xxx@xxx ~ $ echo ABCDEF > ./1.txt xxx@xxx ~ $ cat 1.txt xxx@xxx ~ $ ls -t >> 1.txt xxx@xxx ~ $ ls -a -l | grep abc | wc -l > 2.txt xxx@xxx ~ $ python < ./1.py | wc -c xxx@xxx ~ $ mkdir test_dir xxx@xxx ~/test_dir $ cd test_dir xxx@xxx ~ $ cd - xxx@xxx ~/test_dir $ cd - xxx@xxx ~ $ ./xxx-super-shell xxx@xxx ~ $ exit xxx@xxx ~ $ exit
|
框架主体
main()
从main函数来分析实现的整体框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int main(){ signal(SIGINT,SIG_IGN); signal(SIGTSTP,SIG_IGN); while(1){ char*argv[MAX]={NULL}; printname(); char*command=readline(" "); if (command == NULL) continue; if (strlen(command) == 0) continue; int argc=1; argv[0] = strtok(command, " "); for(int i=1;argv[i] = strtok(NULL, " ");i++) argc++; analyze_cmd(argc,argv); do_cmd(argc,argv); free(command); clear_para(); } }
|
- 一些声明如MAX可以结合文章最后的全部代码查看
- 首先要调用
signal函数屏蔽一些信号
- 由于shell是
交互进程,所以我们进入while(1)循环
- printname函数负责每次输入命令前和后的终端名字显示和路径显示
- 这里使用了一个动态链接库readline,需要我们单独下载并通过相应头文件使用
- 整体框架已经有了,下面给出各个部分的详细解释
接口详解
printname()
1 2 3 4 5 6 7 8
| void printname(){ char pathname[PATHMAX]; getcwd(pathname,PATHMAX); printf(BLUE"Whosefrienda-shell"CLOSE); printf(GREEN" :%s"CLOSE,pathname); printf("$ "); fflush(stdout); }
|
- 这里的BLUE和GREEN和CLOSE是通过宏定义实现的:
1 2 3
| #define BLUE "\033[34m" #define GREEN "\033[32m" #define CLOSE "\033[0m"
|
analyze_cmd(argc,argv)()
这里用了全局变量
1 2 3 4 5 6
| int cd =0 int i_redir=0 int o_redir=0 int _pipe=0 int a_o_redir=0 int pass=0
|
code
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int analyze_cmd(int argc,char*argv[]){ if (argv[0] == NULL) return 0; if (strcmp(argv[0], "cd") == 0) cd = 1; for (int i = 0; i < argc; i++){ if (strcmp(argv[i], ">") == 0) o_redir = 1; if (strcmp(argv[i], "|") == 0) _pipe = 1; if (strcmp(argv[i], ">>") == 0) a_o_redir = 1; if (strcmp(argv[i], "<") == 0) i_redir = 1; if (strcmp(argv[i], "&") == 0){ pass = 1; argv[i]=NULL; } } }
|
每个参数都在do_cmd函数中辅助判定,从而使用不同的接口来实现命令。
void do_cmd(int argc,char*argv[])
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
| void do_cmd(int argc,char*argv[]){ if(pass==1) argc--; if (cd == 1) mycd(argv); else if (strcmp(argv[0], "history") == 0) showhistory(); else if (strcmp(argv[0], "exit") == 0) { printf("exit\n"); printf("有停止的任务\n"); exit(0); } else if ( i_redir== 1) iredir(argv); else if ( o_redir== 1) oredir(argv); else if ( a_o_redir== 1) aoredir(argv); else if ( _pipe == 1) mymulpipe(argv, argc); else { if (pid < 0) { perror("fork"); exit(1); } else if (pid == 0) { execvp(argv[0], argv); perror("command"); exit(1); } else if (pid > 0) { if(pass==1) { printf("%d\n",pid); return; } waitpid(pid, NULL, 0); } } }
|
这个接口实际上通过判定参数真假值来调用其他函数来实现命令,本身只实现没有重定向和管道等的需要fork和execve的简单命令
- 这里只讲一下fork子进程实现的命令,其他在下面的具体接口再详解
- fork返回两个pid值,一个是父进程的,一个是子进程的,fork后的代码会被父进程和子进程分别执行一遍,所以需要进行判定来分别编写父进程和子进程需要执行的代码
- 这里,子进程需要调用execvp来加载命令实现需要的代码
- 父进程则调用waitpid来监控子进程的进行,并且在有&的情况下将控制权重新交给主函数,从而让子进程在后台执行命令的同时不影响shell前台继续执行新命令
void showhistory()
1 2 3 4 5 6 7 8
| void showhistory() { int i = 0; HIST_ENTRY **his; his = history_list(); while (his[i] != NULL) printf("%-3d %s\n", i, his[i++]->line); }
|
这里的HIST_ENTRY类型和history_list函数都在<readline/history.h>中有定义
void mycd(char *argv[])
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
| char lastpath[MAX]; void mycd(char *argv[]){ if (argv[1] == NULL) { getcwd(lastpath, sizeof(lastpath)); chdir("/home"); } else if (strcmp(argv[1], "-") == 0) { char newlastpath[MAX]; getcwd(newlastpath, sizeof(lastpath)); chdir(lastpath); printf("%s\n", lastpath); strcpy(lastpath, newlastpath); } else if (strcmp(argv[1], "~") == 0) { getcwd(lastpath, sizeof(lastpath)); chdir("/home/wanggang"); } else { getcwd(lastpath, sizeof(lastpath)); chdir(argv[1]); } }
|
- 为实现cd-,声明了lastpath来记录之前的路径
- 主要调用chdir来改变当前路径
void oredir(char *argv[])
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
| void oredir(char *argv[]){ char *preargv[MAX] = {NULL}; int i = 0; while (strcmp(argv[i], ">")) { preargv[i] = argv[i]; i++; } int preargc=i; i++; int fdout = dup(1); int fd = open(argv[i], O_WRONLY | O_CREAT | O_TRUNC,0666); dup2(fd, 1); pid_t pid = fork(); if (pid == 0) { if (_pipe=1) { mymulpipe(preargv, preargc); } else execvp(preargv[0], preargv); } else if (pid > 0) { if(pass==1) { printf("%d\n",pid); return; } waitpid(pid, NULL, 0); } dup2(fdout, 1); }
|
- 定义preargv将重定向符之前的命令保存,并获得重定向符之后的文件描述符(没有该文件就创建一个)
- fork子进程运行preargv里保存的命令
- 最后将获得的文件描述符重定向到标准输出
void mymulpipe(char *argv[], int argc)
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 109 110 111 112 113 114
| void mymulpipe(char *argv[], int argc ){ pid_t pid; int index[10]; int number=0; for(int i=0;i<argc;i++) if(!strcmp(argv[i],"|")) index[number++]=i; int cmdcount=number+1; char* cmd[cmdcount][10]; for(int i=0;i<cmdcount;i++) { if(i==0) { int n=0; for(int j=0;j<index[i];j++) { cmd[i][n++]=argv[j]; } cmd[i][n]=NULL; } else if(i==number) { int n=0; for(int j=index[i-1]+1;j<argc;j++) { cmd[i][n++]=argv[j]; } cmd[i][n]=NULL; } else { int n=0; for(int j=index[i-1]+1;j<index[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<cmdcount;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); } else{ for(i=0;i<number;i++) { close(fd[i][0]); close(fd[i][1]);
} if(pass==1) { pass=0; printf("%d\n",pid); return; } for(int j=0;j<cmdcount;j++) wait(NULL); }
|
- 这里分两大步,第一步是通过一个二维数组将各个管道两端的命令分隔开
- 第二步是fork出相应数量的进程并创建相应数量的管道来执行命令
参考资料
1.《Linux/Unix系统编程手册》
2.学长的shell