Id: 3015218104
name: 于春钰
Homework: shell
作业链接:Homework: shell
这次作业主要是使大家更加了解Shell、系统调用以及Shell的工作原理。借助6.828 Shell来进行扩展,可以将其运行在支持Unix API的操作系统上,比如:Linux、MacOS。
可以阅读xv6 book的Chapter 0来了解一下操作系统的接口。
sh.c
,代码主要包含两部分:解析Shell命令和运行命令,并且这里只考虑简单的命令,如下:ls > y
cat < y | sort | uniq | wc > y1
cat y1
rm y1
ls | sort | uniq | wc
rm y
将上面的命令保存在 t.sh
文件中,以便之后使用。
注:编译 sh.c
需要使用C编译器,如果没有需要安装,并使用下面的命令进行编译:
$ gcc sh.c
然后会在同一文件夹下生成 a.out
可执行文件,
当前目录结构:
.
├── a.out
├── sh.c
└── t.sh
0 directories, 3 files
运行刚刚编译好的可执行文件:
$ ./a.out < t.sh
redir not implemented
exec not implemented
pipe not implemented
exec not implemented
exec not implemented
pipe not implemented
exec not implemented
执行后会报错,所以我们需要实现这里的一些功能。
sh.c
里都写了些什么。首先看 main()
函数:
int main(void)
{
// 用来存储输入的命令(字符串)
static char buf[100];
int fd, r;
// Read and run input commands.
// while循环监控用户的输入,有输入后开始执行while内的程序
while (getcmd(buf, sizeof(buf)) >= 0)
{
// 如果是[cd fileName]命令就进行目录切换,
// 注意要判断第三个字符为空格才执行
if (buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' ')
{
// Clumsy but will have to do for now.
// Chdir has no effect on the parent if run in the child.
buf[strlen(buf) - 1] = 0; // chop \n
if (chdir(buf + 3) < 0)
fprintf(stderr, "cannot cd %s\n", buf + 3);
continue;
}
// 否则fork出子进程,来执行输入的命令
// fork()函数详解 http://www.cnblogs.com/jeakon/archive/2012/05/26/2816828.html
// fork()功能:将新建一个子进程
// fork()返回值:返回两次,一次在父进程,一次在子进程
// 父进程返回子进程的 pid
// 子进程返回 0
// 可以根据返回的值来判断是父进程还是子进程
// 进而在子进程中执行相应的命令
if (fork1() == 0)
runcmd(parsecmd(buf));
// wait函数介绍 http://www.jb51.net/article/71747.htm
// wait()函数用于使父进程(也就是调用wait()的进程)阻塞,
// 直到一个子进程结束或该进程接收到一个指定的信号为止。
// 如果该父进程没有子进程或它的子进程已经结束,
// 则wait()就会立即返回。
// pid_t wait (int * status);
wait(&r);
}
exit(0);
}
当终端有输入后,会执行函数 getcmd()
:
// 获取命令,并将当前命令存入缓冲字符串,以便后面进行处理
int getcmd(char *buf, int nbuf)
{
// 判断是否为终端输入
if (isatty(fileno(stdin)))
fprintf(stdout, "6.828$ ");
// 清空存储输入命令的缓冲字符串
memset(buf, 0, nbuf);
// 将终端输入的命令存入缓冲字符串
if (fgets(buf, nbuf, stdin) == 0)
return -1; // EOF
return 0;
}
之后会进入 while
循环,首先判断如果输入的是切换目录的命令 cd
就直接执行目录切换操作,否则就要 fork()
一个子进程,进而进行处理:
if (fork1() == 0)
runcmd(parsecmd(buf));
wait(&r);
函数 fork1()
会fork一个子进程,并返回父/子进程的pid。main()
函数内通过判断返回的pid来判断当前执行的是哪个进程,从而在子进程中接着执行相应的命令;父进程中使用 wait(&r)
进行阻塞,等待子进程返回后再继续执行。
函数 parsecmd()
是为了解析输入的命令的,可以不用过多的关注,不过从函数 parsecmd
的定义
struct cmd *parsecmd(char *);
和结构体 cmd
的定义
// All commands have at least a type. Have looked at the type, the code
// typically casts the *cmd to some specific cmd type.
struct cmd
{
int type; // ' ' (exec), | (pipe), '<' or '>' for redirection
};
可以看出这个函数的功能主要是判断输入的命令的种类的。
然后我们要看一下函数 runcmd()
里面都有些什么:
void runcmd(struct cmd *cmd)
{
int p[2], r;
struct execcmd *ecmd;
struct pipecmd *pcmd;
struct redircmd *rcmd;
if (cmd == 0)
_exit(0);
switch (cmd->type)
{
default:
fprintf(stderr, "unknown runcmd\n");
_exit(-1);
case ' ':
ecmd = (struct execcmd *)cmd;
if (ecmd->argv[0] == 0)
_exit(0);
fprintf(stderr, "exec not implemented\n");
// Your code here ...
break;
case '>':
case '<':
rcmd = (struct redircmd *)cmd;
fprintf(stderr, "redir not implemented\n");
// Your code here ...
runcmd(rcmd->cmd);
break;
case '|':
pcmd = (struct pipecmd *)cmd;
fprintf(stderr, "pipe not implemented\n");
// Your code here ...
break;
}
_exit(0);
}
这个函数接受一个参数:结构体 cmd
,而且通过这个结构体中的 type
值进行进一步的处理。从 switch case
语句的判断条件可以看出,将命令的类型分成三类,分别是: case ''
可执行命令、 case '<'
case '>'
重定向命令和 case '|'
管道命令。我们要做的就是补全不同类型命令里具体执行命令的代码。
首先,要介绍一个函数 int access(const char *path, int mode);
,也可以在终端中使用命令 man access
来查看详细的介绍。
这个函数的功能是:确定文件或文件夹的访问权限,如:读、写等。
参数说明:
path:文件或文件夹的路径。
mode:操作类型,具体如下。
R_OK // 测试是否有读权限
W_OK // 测试是否有写权限
X_OK // 测试是否有执行权限
F_OK // 测试文件/文件夹是否存在
返回值类型是 int
,如果有相应的权限,则返回 0
,否则函数返回 -1
。
因为,Linux 中“一切都是文件”,所以我们执行的Linux中的命令也都是以文件的形式存在的。一些系统基本命令都是存在
/bin/
目录下的(可以使用ls /bin
来查看 ),还有一些命令存在/usr/bin/
目录下,其他命令会存在相应应用程序目录的bin
目录下,所以在执行这些命令前要使用access()
函数来确定一下这些命令(文件)是否存在(F_OK),再进行下一步操作。
$ ls /bin
[ date expr ln pwd sync
bash dd hostname ls rm tcsh
cat df kill mkdir rmdir test
chmod domainname ksh mv sh unlink
cp echo launchctl pax sleep wait4path
csh ed link ps stty zsh
然后,我们再来看一个函数 int exec(const char *path, char *const argv[]);
,也可以在终端中使用命令 man 3 exec
来查看详细的介绍。
这个函数的功能是:使用 path
路径的文件来执行存在 argv
内的命令。
参数说明: path
是要执行文件的路径; argv
是一个数组,数组里存的是要执行的命令,数组中的元素之间用空格相连就形成了要执行的命令。
返回值:如果执行成功则不返回,如果执行失败则返回 -1
,失败原因存在 errno
中。
使用
errno
需要引入头文件#include
可以使用函数
strerror(errno)
来获取错误码对应的说明信息。
下面,我们就开始“执行命令”!先说一下思路,首先使用 access()
函数检查要执行的命令文件是否存在,如果存在就直接执行,否则,在系统的 /bin/
目录和 /usr/bin/
目录下查找相应的命令,如果有就执行,否则抛出错误。代码段如下( case ' '
部分的):
case ' ':
ecmd = (struct execcmd *)cmd;
if (ecmd->argv[0] == 0)
_exit(0);
// fprintf(stderr, "exec not implemented\n");
// Your code here ...
if (access(ecmd->argv[0], F_OK) == 0)
{
execv(ecmd->argv[0], ecmd->argv);
}
else
{
const char *bin_path[] = {
"/bin/",
"/usr/bin/"};
char *abs_path;
int bin_count = sizeof(bin_path) / sizeof(bin_path[0]);
int found = 0;
for (int i = 0; i < bin_count && found == 0; i++)
{
int pathLen = strlen(bin_path[i]) + strlen(ecmd->argv[0]);
abs_path = (char *)malloc((pathLen + 1) * sizeof(char));
strcpy(abs_path, bin_path[i]);
strcat(abs_path, ecmd->argv[0]);
if (access(abs_path, F_OK) == 0)
{
execv(abs_path, ecmd->argv);
found = 1;
}
free(abs_path);
}
if (found == 0)
{
fprintf(stderr, "%s: Command not found\n", ecmd->argv[0]);
}
}
break;
修改完后,可以编译运行一下,效果如下:
$ gcc sh.c
$ ./a.out
6.828$ ls
a.out sh.c t.sh
6.828$
使用 Ctrl/Command + D
可以退出程序。
首先,我们看一下结构体 redircmd
的定义:
struct redircmd
{
int type; // < or >
struct cmd *cmd; // the command to be run (e.g., an execcmd)
char *file; // the input/output file
int flags; // flags for open() indicating read or write
int fd; // the file descriptor number to use for the file
};
主要用的就是这个结构体里的这些属性。
需要说明的就是 int fd;
,这是一个文件描述符。
文件描述符:通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。在读、写文件时,可以使用这个文件描述符。
然后,就可以理一下思路开始写代码了:先关闭当前的标准输入/输出,打开指定文件作为新的标准输入/输出,开始执行命令。代码段如下( case '<' '>'
部分):
case '>':
case '<':
rcmd = (struct redircmd *)cmd;
// fprintf(stderr, "redir not implemented\n");
// Your code here ...
close(rcmd->fd);
if (open(rcmd->file, rcmd->flags, 0644) < 0)
{
fprintf(stderr, "Unable to open file: %s\n", rcmd->file);
exit(0);
}
runcmd(rcmd->cmd);
break;
函数
int open(const char * pathname, int flags, mode_t mode);
的使用方法参见文件IO详解(五)—open函数详解
修改完后,可以编译运行一下,效果如下:
$ gcc sh.c
$ ./a.out
6.828$ ls > ls.tmp
6.828$ cat < ls.tmp
a.out
ls.tmp
sh.c
t.sh
6.828$
上面两条命令分别是将 ls
列出的文件名存入了文件 ls.tmp
和使用 cat
命令读取并显示文件 ls.tmp
中的内容。
管道是一种把两个进程(如fork出来的父子进程)之间的标准输入和标准输出连接起来的机制,从而提供一种让多个进程间通信的方法,当进程创建管道时,每次都需要提供两个文件描述符来操作管道。其中一个对管道进行写操作,另一个对管道进行读操作。对管道的读写与一般的IO系统函数一致,使用write()函数写入数据,使用read()读出数据。
同样的,我们先看一下结构体 pipecmd
的定义:
struct pipecmd
{
int type; // |
struct cmd *left; // left side of pipe
struct cmd *right; // right side of pipe
};
管道命令的标志是符号 |
,|
的左面和右面分别是不同的命令,我们需要逐步的执行这些命令。
然后,我们来了解一个函数: int pipe(int fd[2]);
这个函数的作用是在两个进程之间建立一个管道,并生成两个文件描述符 fd[0]
和 fd[1]
,分别对应管道的读取端和写入端,这两个进程可以使用这两个文件描述符进行读写操作,即实现了这两个进程之间的通信。
返回值:若成功则返回 0
,否则返回 -1
,错误原因存于 errno
中。
另一个函数: dup(int old_fd)
:
这个函数的功能是复制一个现存的文件描述符。
返回值:若成功则返回一个指向相同文件的新的文件描述符(且为当前可用文件描述符中的最小值 ),失败则返回 -1
。
代码段( case '|'
部分):
case '|':
pcmd = (struct pipecmd *)cmd;
// fprintf(stderr, "pipe not implemented\n");
// Your code here ...
// 建立管道
if (pipe(p) < 0)
fprintf(stderr, "pipe failed\n");
// 先fork一个子进程处理左面的命令,
// 并将左面命令的执行结果的标准输出定向到管道的输入
if (fork1() == 0)
{
// 先关闭标准输出再 dup
close(1);
// dup 会把标准输出定向到 p[1] 所指文件,即管道写入端
dup(p[1]);
// 去掉管道对端口的引用
close(p[0]);
close(p[1]);
// 此时 left 的标准输入不变,标准输出流入管道
runcmd(pcmd->left);
}
// 再fork一个子进程处理右面的命令,
// 将标准输入定向到管道的输出,
// 即读取了来自左面命令返回的结果
if (fork1() == 0)
{
// 先关闭标准输入再 dup
close(0);
// dup 会把标准输入定向到 p[0] 所指文件,即管道读取端
dup(p[0]);
// 去掉管道对端口的引用
close(p[0]);
close(p[1]);
// 此时 right 的标准输入从管道读取,标准输出不变
runcmd(pcmd->right);
}
close(p[0]);
close(p[1]);
wait(&r);
wait(&r);
break;
Linux命令
wc
(Word Count):用于统计文件中的行数、字数、字节数。例如文件
y
中包含以下内容:a.out sh.c t.sh y
使用命令
wc y
显示的结果如下:4 4 18 y
第一个
4
表示文件中有4行,第二个4
表示有4个字(使用空格作为分割符),第三个18
表示有18个字节(注意,换行符也算一个字节),第四个y
表示统计的文件名。
最后,可以编译运行了:
$ gcc sh.c
# 执行 t.sh 中的命令
$ ./a.out < t.sh
# 输出结果
4 4 18
4 4 18
Bingo~
参考资料:
Homework: shell
6.828 操作系统 Homework: Shell