实现一个简单的 Shell
编程实验 (基于 Linux)
截止时间:2024年11月10日 (extended) 23:59
提交内容:
- 代码源文件
esh.c
:注意代码中应有必要的注释- 实验报告
学号.pdf
:简要描述实验中遇到的关键问题及解决方案、印象最深的 bugs、或者额外实现的功能等即可 (1~2 页)。提交方式:请参考实验页面的具体说明,本次实验
LAB ID
为1
。# code curl -F "token=[TOKEN]" -F "lab_num=1" -F "file=@esh.c" http://114.212.81.7:9999/upload_code # report curl -F "token=[TOKEN]" -F "lab_num=1" -F "file=@[FILE NAME]" http://114.212.81.7:9999/upload_report # score curl "http://114.212.81.7:9999/download?token=[TOKEN]&lab_num=1"
*提交冷却时间 2 小时
Shell 是计算机中运行的一个应用程序,其提供了用户与操作系统内核交互的接口,通过解释用户输入 (prompts) 来执行相关操作,如管理文件、运行程序和控制进程等。Shell 既可以是命令行界面 (CLI),也可以是图形用户界面 (GUI)。在本实验中,我们将实现一个简单的命令行 Shell,以此来熟悉操作系统的进程管理、以及基本的系统调用。
运行如下命令下载本实验的框架代码:
curl ftp://114.212.81.7/lab1/esh.c -O
🗒️ 实验内容
在本次实验中,我们需要实现一个叫做 esh (Easy Shell) 的简化版 Shell。esh 运行后将不断接受用户输入并进行处理,直到退出为止,即类似如下的使用形式:
prompt > gcc esh.c -o esh
prompt > ./esh
esh > ls
esh >
本实验中所实现的 esh 需要具备以下功能:
1. 内置命令 (builtin commands)
内置命令是指由 Shell 本身实现的一些命令或功能,其直接在 Shell 程序里执行,而不是由 Shell 去调用其它外部程序。esh 需要支持以下四个内置命令:
exit
:退出 Shell。cd [directory]
:接收一个参数 (在无参数或多个参数时报错,打印错误信息),用于切换当前工作目录。若接收的输入为cd ~
,则切换到当前用户的根目录。paths [path ...]
:接收零个或多个参数,参数间以空格分隔,用于打印或修改当前PATH
环境变量 (当需要执行某个外部命令时,esh 会自动遍历PATH
中的每个路径去寻找对应的可执行文件,若找不到则打印错误信息):- 当无参数时,使用框架代码中的
print_path_info(int index, char *path)
函数依次打印PATH
中的每个路径,其中index
从1
开始,每个路径打印一次; - 当有参数时,用接收的参数更新
PATH
,覆盖原来的信息; - 你需要定义相应的全局变量来记录
PATH
,而不是使用setenv
等命令修改操作系统的环境变量;PATH
环境变量的初始值应为/bin
(注意/bin
后面没有/
)。
- 当无参数时,使用框架代码中的
bg
: 使用框架代码中的print_bg_info(int index, int pid, char *cmd)
函数依次打印每个后台任务。其中index
从1
开始,pid
是进程 ID,cmd
是任务执行的命令 (去除首尾的空白字符),每个后台任务打印一次。- 你需要尝试维护记录后台进程信息的相关全局变量,当某个后台进程结束时,自动更新这些信息 (即删除对应进程,后续进程前移,保持相对顺序)。
2. 执行外部命令
esh 需要能执行外部的可执行文件,包括执行系统中的某个 GNU Core Utility (例如 ls
,cat
,wc
, sort
等,其路径位于 /bin
)、或者某个自己编写的程序 (例如 ./helloworld
等)。
3. 管道 (pipe)
管道 |
可用来连接多个命令,将上一个命令的输出作为下一个命令的输入。例如,ls | wc -l
将通过程序 ls
列出当前目录下的文件,并将此作为程序 wc
的输入,用于进一步统计文件的数量。
esh 需要能支持任意多个命令通过管道进行连接。如果出现两个连续的 |
符号时可认为出错 (两个 |
中间可能有空格,例如 | |
),使用框架代码中的 print_error_info()
函数打印错误信息。注意该需求与一般的 Shell 不同。
4. 重定向 (redirection)
esh 可通过 >
来将简单命令的输出重定向到某个文件中。例如,对于 cmd > filepath
,若 filepath
存在,则清空文件内容然后写入;若不存在则新建文件写入。
对于一般的 Shell 来说,其在重定向时如果 cmd
执行出错,则不会写入文件。对于 esh,无论 cmd
执行成功与否,都将其写入文件 (分别通过 STDOUT_FILENO
和 STDERR_FILENO
)。
在本次实验中,我们只需要让 esh 支持最基础的 >
重定向即可,其它包括 <
和 >>
在内的重定向符可简单视为错误。
5. 顺序执行
我们将内置命令 (例如 paths
)、外部命令 (例如 ls -l
)、以及管道连接的命令 (例如 ls | wc -l
) 称为简单命令。 esh 可以通过 ;
来将多个简单命令合并起来,按从左到右的顺序依次执行。
注意在上述需求中,前一条命令执行是否出错将不影响后续命令的执行。如果出现两个连续的 ;
符号时可认为出错 (两个 ;
中间可能有空格),使用框架代码中的 print_error_info()
函数打印错误信息。
在真实 Shell 中除了
;
外还可以使用&&
和||
来连接多个命令,此时前一条命令的执行结果会决定是否执行后一条命令。
6. 后台执行
esh 可通过在简单命令后加上 &
符号来将该任务放到后台执行,此时 esh 需使用框架代码中的 print_current_bg(int pid, char *cmd)
函数打印当前后台任务信息,然后就可以继续响应用户的输入。
例如,如果用户输入 sleep 10 &
,则此命令会在后台执行,并打印 Process xxxxx sleep 10: running in backround
。 这里的 &
也可以用来连接多条简单命令,例如 ls | wc & echo 1 &
表示将 ls | wc
和 echo 1
都放入后台执行。
在实现上述需求时,注意后台进程的创建和结束要及时更新 bg
维护的全局变量,当后台进程结束时,会向父进程发送 SIGCHLD
信号,你需要处理此信号,防止后台进程变成僵尸进程。
在本实验中,如果出现两个连续的 &
符号时可认为出错,使用框架代码中的 print_error_info()
函数打印错误信息 (两个 &
中间可能有空格)。符号 &
直接出现在 ;
前也认为出错 (例如 ls & ;
视为出错,但 ls & ps ;
则是合法输入)。
7. 错误处理
esh 无论遇到什么类型的错误,只需要使用框架代码中的 print_error_info()
函数打印错误信息即可:
void print_error_info() {
printf(ERROR_INFO);
fflush(stdout);
}
在本实验中,你需要考虑上述 1~6 中各种场景组合出现的情况,Be careful! 我们不要求你实现的 esh 能应对所有可能的 corner cases,但要能处理一些较为简单和常见的组合情况。
一些需要注意的实现细节
- 输出提示必须是
esh >
,注意>
符号前后各有一个空格; - 你可以使用
getline()
来获得用户输入; - 在
getline()
返回-1
时,代表读取失败,此时请结束程序; - 在你的代码中不需要用到
perror()
,需要输出的地方用printf()
即可。注意每次printf()
会后调用fflush(stdout)
清空输出缓冲区。 - 不需要担心
bg
命令返回的 PID,我们会进行模式匹配; - 在执行每个命令前应该先做合法性检查,即判断
&
、|
、;
和>
这四个操作符出现的位置是否合适。如果不合适,直接输出错误信息,不执行命令的任何一部分。例如,ls & & ls
和echo 1 > | a.txt
都是不合法的命令; - 对于通过合法性检查的命令,分成两种情况:
- 如果命令中不包含
;
则视为一条命令,此时最多输出一条错误信息; - 如果命令中包含
;
则为多个命令,此时每个命令在出错时都会输出一条错误信息; - 例如,
ech 1 | eho 1 | pwd
只会输出一条错误信息 (虽然有两个错误的部分ech 1
和eho 1
);而ech 1; eho 1; pwd
会输出两条错误信息。注意上述两个例子中pwd
都会正确输出。
- 如果命令中不包含
其它说明
在实现上述 esh 的过程中,你可能会用到以下一些系统调用:
- 用于进程创建和结束的
fork()
、execve()
(以及相关的exec()
系列函数) 和exit()
等 - 用于等待进程状态改变的
wait()
和waitpid()
等 - 用于操作文件描述符的
open()
、close()
和dup()
等 - 用于改变当前工作目录的
chdir()
等
关于这些系统调用或 C 标准库等 API 的说明,可以查看 Linux man pages。
你或许知道 C 标准库中有一个 API 叫做
system()
,其功能是执行一个 Shell 命令。当然,在本实验中你不能使用这个 API。
对 Shell 功能如有不清楚的,可以参考自己系统 Shell 的对应执行行为 (但注意 esh 和一般 Shell 的规格说明存在不同)。Bash Reference Manual 给出了 bash 这一常见 Shell 的简要功能描述,可供参考。
一些额外的备注:
- 注意内存的分配和回收,不然会出现各种奇奇怪怪的问题;
- gdb 和 valgrind 会让你事半功倍。
📊 结果评估
我们会在一批具有不同难度的测试用例上评估你实现 esh 的功能正确性、以及对一些组合场景的支持情况 (各测试用例之间不会互相影响),并以此来返回 OJ 得分。
你可以使用如下测试用例先在本地进行测试。 假设当前 esh 可执行文件和目录 a
处于同一目录,目录 a
下又有三个子目录 a1
、a2
和 a3
。
1. 内置命令
esh > cd a
esh > ls
a1 a2 a3
esh >
esh > cd
An error has occurred
esh >
esh > paths
1 /bin
esh > paths a
esh > paths
1 a
esh >
2. 执行外部命令
esh > cd a
esh > rm -r a1
esh > ls
a2 a3
esh >
esh > cd a
esh > rm -r b
An error has occurred
esh >
注意此时系统可能会通过错误流输出一些信息,esh 的实现中不需要处理这些信息,直接通过 print_error_info()
输出错误信息即可。
3. 管道
esh > ls |echo 1
1
esh >
esh > cd a
esh > ech 1 | ls
a1 a2 a3
An error has occurred
esh >
注意如果管道的最后一个命令有输出则需要输出。按照管道的功能,如果我们在执行 ech 1
失败之后通过 printf()
输出错误信息,此时这个信息不会输出到屏幕。但是为了标记该管道命令中是否有错,我们在正常输出 (如果有的话) 之后输出错误信息。
4. 重定向
esh > cd a
esh > ls > 1
esh > cat 1
1
a1
a2
a3
esh >
注意到 ls
会列出重定向的目标文件本身,这由 ls
自动处理,无须担心。
esh > cd a
esh > ech 1 | ls > 1.txt
esh > cat 1.txt
a1
a2
a3
An error has occurred
esh >
5. 顺序执行
esh > cd a
esh > echo 1; ls
1
a1 a2 a3
esh >
esh > ech 1; eoh 1; echo 1;
An error has occurred
An error has occurred
1
esh >
6. 后台执行
esh > sleep 5 &
Process pid sleep 5: running in backround
esh > bg
1 pid sleep 5
esh > bg # 隔 6s 再输入
esh >