Lab 2¶
在实验一中,我们使用了许多命令行操作来完成一系列工作,如编译 Linux 内核、打包 initrd 等。所有的命令行都由一个叫做 shell 的程序解释并执行。本实验我们将自己编写一个简单的 shell 并理解 Linux shell 程序的工作原理。
编写 Shell 程序¶
首先,大家可以将本页底部助教编写的一个示例程序命名为 shell.rs
:
rustc shell.rs
以上命令会调用 rustc 编译器编译出一个可执行文件 shell
,你可以继续输入 ./shell
来运行它。
这是一个非常简陋的 shell,它会提示你输入命令,你可以输入 cd
来切换工作目录,输入 pwd
显示当前目录,输入 export
导出环境变量,输入 exit
退出,或者调用系统中有的其他命令来运行,例如 ls
, cat
等。
你的任务是对这个 shell 程序进行修改升级(当然,你也可以选择自己从头或选择其他语言编写),并完成下面的任务。
更健壮(推荐,但不要求)¶
目前的示例程序非常脆弱,不检查各种可能的错误(chdir
、fork
、exec
等系统调用都可能出错)。建议你将它改得更健壮,以方便之后的进一步开发和调试。
支持管道¶
形如 env | wc
这样的命令利用了「管道」语法,将两条不同的命令对接在一起同时运行。|
的意思是将前面的命令 env
(输出所有环境变量)的标准输出连接到后面命令 wc
(统计行数)的标准输入(这样就能统计出环境变量的总数)。请你观察并学习这个语法的效果,为你的 shell
程序实现这一功能。
你可能要用到的函数:pipe
、close
、dup
。你可以运行 man 函数名
来查看系统自带的文档,或者上网搜索更多信息。
支持基本的文件重定向¶
形如 ls > out.txt
会将 ls
命令的输出重定向到 out.txt
文件中,具体地说,会将 out.txt
关联到程序的标准输出,然后再运行相应的命令。
类似的,ls >> out.txt
会将输出追加(而不是覆盖)到 out.txt
文件,cat < in.txt
会将程序的标准输入重定向到文件 in.txt
。
请为你的 shell
程序实现 >
、>>
和 <
的功能。
你可能要用到的函数:open
、close
、dup
。
处理 Ctrl-C 的按键¶
在使用 shell 的时候按下 Ctrl-C 可以丢弃当前输入到一半的命令,重新显示提示符并接受新的命令输入。当有程序运行时,按下 Ctrl-C 可以终结运行中的程序,立即回到 shell 开始新的命令输入(shell 没有随程序一起结束)。
例如(^C
表示遇到 Ctrl-C 的输入):
$ echo taokystrong
taokystrong
$ echo taokystrong^C
$ sleep 5 # 几秒之后
^C
$ # sleep 没有运行完
$ ^C
请为你的 shell
实现对 Ctrl-C 的处理。
提示:当你正确处理第一种情况后(丢弃未输入完的命令),第二种情况(终结运行中的程序)并不需要你做任何工作。
你可能要用到的函数:signal
、waitpid
。
处理 Ctrl-D 的按键¶
在使用 shell 的时候输入 exit
即可退出 shell,Ctrl-D (EOF) 在这种场景下等同于 exit
。
支持 Bash 风格的 TCP 重定向(选做)¶
在精简的 Linux 环境中(如 Docker 容器里),常常是没有 nc
命令用来进行原始的 TCP 网络通信的。Bash 和一些其他 shell 支持一种特殊的重定向语法:/dev/tcp/<host>/<port>
。
通过查看 Bash 的 man 文档,REDIRECTION
一节,当重定向目标是下面几种路径,且操作系统没有提供这个路径时,Bash 会自行处理它们:
/dev/fd/<fd>
/dev/stdin
/dev/stdout
/dev/stderr
/dev/tcp/<host>/<port>
/dev/udp/<host>/<port>
阅读相关文档,模拟 Bash 的行为实现 cmd > /dev/tcp/<host>/<port>
和 cmd < /dev/tcp/<host>/<port>
的重定向。
方便起见,你只需要处理 <host>
是典型的 IPv4 地址(即 a.b.c.d
的形式,其中 abcd 均为 0 ~ 255 之间的整数)且 <port>
为 1 ~ 65535 之间的整数时的情况。
你可能要用到的函数:socket
, connect
支持基于文件描述符的文件重定向、文件重定向组合(选做)¶
形如 cmd 10> out.txt
和 cmd 20< in.txt
以及 cmd 10>&20 30< in.txt
这样的命令会将打开文件描述符 10、20 和 30 并重定向到相应的文件。请自行查找资料,实现这些文件重定向。
cmd << EOF
this
output
EOF
上述命令会将字符串 "this\noutput\n"
作为标准输入重定向给 cmd
。请实现这种重定向方式。
cmd <<< text
会将 "text\n"
作为标准输入重定向给 cmd
。请实现这种重定向方式。
更多功能(选做)¶
我们一般使用的 shell 非常强大,你还可以自行了解下面这些语法的含义:
echo $SHELL
A=1 env
alias ll='ls -l'
echo ~root
(sleep 10; echo aha) &
if true; then ls; fi
ifdown eth0 && ifup eth0
set -u # check existence of variable
请自行选择一个或多个功能并实现它们。
使用 strace 工具追踪系统调用¶
Linux 系统中有许多用于监控、追踪系统状态的工具,如下图所示。
strace
是一个用于监控进程系统调用的程序,例如,在 Debian 系统中使用 strace
追踪 true
命令(一个什么都不做并返回 0 的命令),可以看到类似以下输出:
execve("/usr/bin/true", ["true"], 0x7ffc07ef2ae0 /* 48 vars */) = 0
brk(NULL) = 0x55cc2c5dc000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fff1b8ec3e0) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=113477, ...}) = 0
mmap(NULL, 113477, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fcc2e447000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360r\2\0\0\0\0\0"..., 832) = 832
lseek(3, 64, SEEK_SET) = 64
/* 此处省略多行 */
exit_group(0) = ?
+++ exited with 0 +++
请使用 strace
工具追踪你编写的 shell,找出 3 个你代码里没有出现,但出现在 strace 的输出中的系统调用(open
, read
, write
除外)。查阅资料,简单说说它们的功能。
实验要求¶
请按照以下目录结构组织你的 GitHub 仓库:
. // Git 仓库目录
├── lab2 // 实验二根目录
│ ├── shell.c // 你的 Shell 的源代码
│ ├── other.c // (可选)更多代码文件
│ ├── Makefile // (可选)你提供的 Makefile
│ └── README.md // 简单描述的实验报告,不必过长
├── .gitignore // 这两个文件在实验一的文档里说过了
└── README.md
本次实验满分 12 分。你需要完成:
- 按照上面的要求组织仓库结构,提交你的 shell 源代码
- 如果你提交的内容无法正常编译,我们会尝试修复并酌情扣除一定分数
- 实验一中对于 Git 工具的使用要求仍然适用于本实验,即当出现以下情况时,我们会酌情扣除一定分数
- 很大一部分的 commit 由 GitHub 网页版生成,即通过网页版文件上传的方式提交实验的文件
- 只有寥寥无几的 commit
- 上传了大量与实验要求无关的文件,没有设置
.gitignore
- 你的 shell 能够正确处理含有 1 个管道的命令,如
ls | grep hello
- 你的 shell 能够正确处理含有多个管道的命令,如
ls | cat -n | grep hello
- 你的 shell 能够正确处理含有多个管道的命令,如
- 你的 shell 支持
>
,>>
,<
重定向 - 你的 shell 在遇到 Ctrl-D 时能识别为 EOF
- 你的 shell 在遇到 Ctrl-C 时能丢弃已经输入一半的命令行,显示
#
提示符并重新接受输入 - 以上必做项目全部完成可以获得 7 分。对于额外的选做项目,由助教评估确定分数,最高 4 分
- 完成 shell 获得加分后,总分不超过 9 分
- 按照「strace 工具」一节的实验要求有效地描述了 3 个系统调用(1 分)
- 使用 Rust 继续完成 shell 的编写(2 分,可用选做项目替代)
关于实验报告¶
尽管本实验除了「strace 工具」一节以外对实验报告并无要求,但是我们仍然推荐你在 README.md
中写少量内容:
- 你的 shell 实现可能与系统中的
bash
(或助教期望的表现)有所不同,简要介绍这些潜在的区别,以免产生误会,导致不必要的扣分。 - 你完成了一些选做项目,也可以简单介绍,方便助教进行更准确的评估
本实验的主要内容为 shell 程序的编写,因此不必花费太多工夫在实验报告上。
关于选做项目¶
如果你按照本文档要求实现了上述三个功能,你将获得至少 7 分。如果你想获得更高的分数,请参考标记为「选做」的几个小节中介绍的 Linux shell 的常见功能并实现(不限制为本文档列出的功能,见下)。
每一项额外功能都会由助教讨论评估,但通常单个项目不会超过 1 分。
我们鼓励进行与操作系统相关的实验探究,因此过度脱离主题的项目可能不会获得加分,例如:
- 过于简单的内置命令,如
:
(colon),true
,false
,help
等 -
严重偏离 shell 的基本功能的项目,例如你模仿 Zsh 为你的 shell 内置了一个俄罗斯方块游戏
作为一个参考基准,GNU Bash 具有的功能大部分都会被认可。
其他说明¶
本实验可以使用 C/C++ 或 Rust 语言完成。(注:使用 Rust 会得到额外的 2 分)
本实验可以使用 libc, libstdc++, libm 以及 iostream, STL 等 C/C++ 语言标准和常用库。如果你愿意,你也可以使用 readline 和 ncurses 等 Linux 程序常用库。使用此处没有列出的库前请询问助教。
关于编译¶
本实验中,如果你提供了 Makefile
或者 cargo.toml
文件,我们将使用它来编译你提交的程序;否则,我们将编译 lab2/
目录下所有的 *.rs
文件。在任何情况下,你也可以在 README.md
中说明编译与运行相关的注意事项。
示例程序¶
use std::io::{stdin, BufRead};
use std::env;
use std::process::{exit, Command};
fn main() -> ! {
loop {
let mut cmd = String::new();
for line_res in stdin().lock().lines() {
let line = line_res.expect("Read a line from stdin failed");
cmd = line;
break;
}
let mut args = cmd.split_whitespace();
let prog = args.next();
match prog {
None => panic!("Not program input"),
Some(prog) => {
match prog {
"cd" => {
let dir = args.next().expect("No enough args to set current dir");
env::set_current_dir(dir).expect("Changing current dir failed");
}
"pwd" => {
let err = "Getting current dir failed";
println!("{}", env::current_dir().expect(err).to_str().expect(err));
}
"export" => {
for arg in args {
let mut assign = arg.split("=");
let name = assign.next().expect("No variable name");
let value = assign.next().expect("No variable value");
env::set_var(name, value);
}
}
"exit" => {
exit(0);
}
_ => {
Command::new(prog).args(args).status().expect("Run program failed");
}
}
}
}
}
}