实现一个简单的 Shell

编程实验 (基于 Linux)

截止时间:2024年11月10日 (extended) 23:59

提交内容

  1. 代码源文件 esh.c:注意代码中应有必要的注释
  2. 实验报告 学号.pdf:简要描述实验中遇到的关键问题及解决方案、印象最深的 bugs、或者额外实现的功能等即可 (1~2 页)。

提交方式:请参考实验页面的具体说明,本次实验 LAB ID1

# 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 中的每个路径,其中 index1 开始,每个路径打印一次;
    • 当有参数时,用接收的参数更新 PATH,覆盖原来的信息;
    • 你需要定义相应的全局变量来记录 PATH,而不是使用 setenv 等命令修改操作系统的环境变量PATH 环境变量的初始值应为 /bin (注意 /bin 后面没有 /)。
  • bg: 使用框架代码中的 print_bg_info(int index, int pid, char *cmd) 函数依次打印每个后台任务。其中 index1 开始,pid 是进程 ID,cmd 是任务执行的命令 (去除首尾的空白字符),每个后台任务打印一次。
    • 你需要尝试维护记录后台进程信息的相关全局变量,当某个后台进程结束时,自动更新这些信息 (即删除对应进程,后续进程前移,保持相对顺序)。

2. 执行外部命令

esh 需要能执行外部的可执行文件,包括执行系统中的某个 GNU Core Utility (例如 lscatwc, 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_FILENOSTDERR_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 | wcecho 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 & & lsecho 1 > | a.txt 都是不合法的命令;
  • 对于通过合法性检查的命令,分成两种情况:
    • 如果命令中不包含 ; 则视为一条命令,此时最多输出一条错误信息;
    • 如果命令中包含 ; 则为多个命令,此时每个命令在出错时都会输出一条错误信息;
    • 例如,ech 1 | eho 1 | pwd 只会输出一条错误信息 (虽然有两个错误的部分 ech 1eho 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 的简要功能描述,可供参考。

一些额外的备注:

  • 注意内存的分配和回收,不然会出现各种奇奇怪怪的问题;
  • gdbvalgrind 会让你事半功倍。

📊 结果评估

我们会在一批具有不同难度的测试用例上评估你实现 esh 的功能正确性、以及对一些组合场景的支持情况 (各测试用例之间不会互相影响),并以此来返回 OJ 得分。

你可以使用如下测试用例先在本地进行测试。 假设当前 esh 可执行文件和目录 a 处于同一目录,目录 a 下又有三个子目录 a1a2a3

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 >