最近按照mit的Operating System Engineering课程(6.828/Fall 2014)学习从零编写一个简单的操作系统。
第一节课的作业1就是写一个简单的shell,能够运行command,并且支持重定向(‘>’, ‘<’)和管道(‘|’),但不支持脚本编程。
课程给的源码已经实现了参数的解析
1 struct cmd * parsecmd(char *s);
该函数根据每行的命令中是否含有’>’/’<’和’|’返回不同的cmd结构:
1 struct cmd { 2 int type; 3 }; 4 5 struct execcmd { 6 int type; 7 char *argv[MAXARGS]; 8 }; 9 10 struct redircmd { 11 int type; 12 struct cmd *cmd; 13 char *file; 14 int mode; 15 int fd; 16 }; 17 18 struct pipecmd { 19 int type; 20 struct cmd *left; 21 struct cmd *right; 22 };
C语言虽然天生不支持面向对象技术,但是通过仔细的指针操作仍然能够达到一定的类似于面向对象的效果。
parsecmd()函数会创建3种cmd:execcmd, pipecmd或redircmd。但是如何返回能够让调用者识别出是哪一种cmd呢?注意到3种cmd的第一个成员变量都是int type
,并且3种cmd的type值不一样(取值分别为’ ‘, ‘>’/’<’, ‘|’),这是故意为之的。parsecmd会将这3种cmd的指针(指向具体对象的指针)强制转化为struct cmd*
(指向基类的指针),然后调用者根据type值来确定是哪种具体的cmd,再将之强制转换回去。
而作业的内容就是在runcmd()
中实现3种cmd的处理代码:
1 void 2 runcmd(struct cmd *cmd) 3 { 4 int p[2], r; 5 struct execcmd *ecmd; 6 struct pipecmd *pcmd; 7 struct redircmd *rcmd; 8 9 if(cmd == 0) 10 exit(0); 11 12 switch(cmd->type){ 13 default: 14 fprintf(stderr, "unknown runcmd\n"); 15 exit(-1); 16 17 case ‘ ‘: 18 ecmd = (struct execcmd*)cmd; 19 if(ecmd->argv[0] == 0) 20 exit(0); 21 fprintf(stderr, "exec not implemented\n"); 22 // Your code here ... 23 break; 24 25 case ‘>‘: 26 case ‘<‘: 27 rcmd = (struct redircmd*)cmd; 28 fprintf(stderr, "redir not implemented\n"); 29 // Your code here ... 30 runcmd(rcmd->cmd); 31 break; 32 33 case ‘|‘: 34 pcmd = (struct pipecmd*)cmd; 35 fprintf(stderr, "pipe not implemented\n"); 36 // Your code here ... 37 break; 38 } 39 exit(0); 40 }
第1种,没有重定向也没有管道的单一命令。只需要调用execvp()
替换可执行二进制文件即可。
1 case ‘ ‘: 2 ecmd = (struct execcmd*)cmd; 3 if(ecmd->argv[0] == 0) 4 exit(0); 5 if (execvp(ecmd->argv[0], ecmd->argv) == -1) //失败返回-1,成功返回0 6 perror("exec error"); 7 break;
效果如下:
1 [email protected]:~/src/6.828$ ./mysh 2 6.828$ ls 3 file1 file2 mysh sh.c tags t.sh 4 6.828$
第2种,支持重定向的命令,例如 echo "hello, world!" > txt
。这需要借助close()/open()
的特性。每次进程调用open()返回的fd总是当前可用的最小fd。因此如果是重定向标准输入,需要先关闭标准输入(fd为0),再打开文件;如果是重定向标准输出,需要先close标准输出(fd为1)。
1 case ‘>‘: 2 case ‘<‘: 3 rcmd = (struct redircmd*)cmd; 4 if (close(rcmd->fd) < 0) { 5 perror("close error"); 6 exit(-1); 7 } 8 if (rcmd->mode & O_CREAT) { // 如果是重定向输出,还需要新建输出文件 9 if (open(rcmd->file, rcmd->mode, S_IRUSR | S_IWUSR) < 0) { 10 perror("open file error"); 11 exit(-1); 12 } 13 } 14 else if (open(rcmd->file, rcmd->mode) < 0) { 15 perror("open file error"); 16 exit(-1); 17 } 18 runcmd(rcmd->cmd); 19 break;
效果如下:
1 6.828$ echo "hello, world!" > txt 2 6.828$ cat txt 3 "hello, world!" 4 6.828$
第3种,支持管道的命令,例如ls | sort | uniq | wc
。这需要fork新的进程来处理不同的cmd,并且需要借助pipe()来实现父子进程间通信。这里需要注意的是,是由父进程执行第一个命令还是由子进程执行第一个命令。在main()
中父进程p0循环读标准输入,每读入一行就fork一个子进程p1来处理命令并等待子进程p2退出后继续循环。
因此,在这里,如果由父进程p1执行ls
命令的话,它将数据写入pipe后就会退出,这样就会造成这种效果:main进程已经等待到p1进程退出了,就会打印出6.828$
来接受新的输入,但是p2进程还没有执行完,它的输出就会在main进程输出6.828$
之后了。所以,需要由子进程p2来执行第一个命令,接着如果仍然有管道,则由p3子进程来执行第二命令,最后p1进程来执行最后一个命令。只有等p1执行完最后一个命令退出后,main进程才会继续下一次循环。
1 case ‘|‘: 2 pcmd = (struct pipecmd*)cmd; 3 if (pipe(p)) { 4 perror("create pipe error"); 5 exit(-1); 6 } 7 if (fork1() > 0) { 8 // parent 9 close(0); 10 close(p[1]); 11 if (dup(p[0]) < 0) 12 perror("dup"); 13 runcmd(pcmd->right); 14 } 15 close(1); 16 close(p[0]); 17 if (dup(p[1]) < 0) 18 perror("dup"); 19 runcmd(pcmd->left); 20 break;
效果如下:
1 6.828$ ls | sort | uniq | wc 2 7 7 36 3 6.828$
版权声明:本文为博主原创文章,未经博主允许不得转载。