BUAA_OS_challenge_shell_实验报告
实验总览
本次实验旨在对 MOS 操作系统中现有的 Shell 进行功能扩展,使其从一个基础的命令解释器演进为一个功能更加完善、用户体验更佳的交互式程序。实验内容主要包括:
- 路径管理:引入当前工作目录(CWD)的概念,支持相对路径,并实现
cd和pwd内置命令。 - 环境变量:实现局部和全局环境变量的管理,包括
declare、unset命令和$变量展开。 - 输入与指令优化:实现指令自由输入、不带
.b后缀指令、快捷键、历史指令、注释功能、 - 指令条件执行:实现 Linux shell 中的
&&和||。 - 更多指令:增加
touch,mkdir,rm等常用文件系统操作命令和exit指令。 - 追加重定向:实现了追加重定向功能
限于篇幅,本报告并未给出全部修改的代码,仅给出了笔者认为比较关键,语言不能完全描述的部分代码。
实验设计与实现
1. 支持相对路径与内置命令 cd、pwd
1.1 设计思路
为了支持相对路径,核心是为每个进程维护一个“当前工作目录”(CWD)的状态。
- CWD 的存储:
- 最佳方案是利用共享内存。我分配了一个固定的虚拟地址
CWD_VA(0x7f000000),并将其映射为一个带有PTE_LIBRARY标志的共享页面。PTE_LIBRARY确保了通过fork或spawn创建的子进程能自动继承这个页面的映射,从而共享 CWD 字符串。 - 在 Shell 启动时(
libmain->fsutil_init),会检查该页面是否已映射。如果是第一个启动的 Shell,它会负责分配并初始化 CWD 为根目录/。
- 最佳方案是利用共享内存。我分配了一个固定的虚拟地址
- 路径解析:
- 创建一个
resolve_path函数,负责将用户输入的任何路径(绝对或相对)转换为一个唯一的、规范化的绝对路径。 normalize_path函数负责处理拼接后的路径,解析.和..,并移除多余的/,最终得到一个干净的绝对路径。这个绝对路径随后被传递给open、stat等底层系统调用。
- 创建一个
- 内置命令实现:
cd:作为一个内置命令,cd直接修改共享内存中的 CWD 字符串。它首先使用resolve_path解析目标路径,然后通过stat检查路径是否存在且为目录,验证通过后,更新 CWD。pwd:同样是内置命令,它直接读取共享内存中的 CWD 字符串并打印到标准输出。
1.2 关键代码
CWD 共享与初始化 (user/lib/fsutil.c)
1 | // 定义一个固定的虚拟地址用于存储CWD |
lib/libos.c中的libmain会在main函数执行前调用fsutil_init,确保 CWD 机制就绪。PTE_LIBRARY标志是实现父子进程共享 CWD 的关键。
路径解析 (user/lib/fsutil.c)
1 | // 将相对路径转换为绝对路径 |
这个函数统一了路径处理的入口,所有文件操作(如
open,stat,rm等)都调用它来获取绝对路径。cd作为一个改变 Shell 自身状态的命令,必须在主 Shell 进程中直接执行,而不是fork一个子进程来执行。我的代码在sh.c的主循环中正确地处理了这一点。pwd也可以这样处理,或者像现在一样在子进程中执行,因为它不改变 Shell 状态。
2. 环境变量管理
2.1 设计思路
数据结构:定义了一个
Var结构体来存储单个变量,包括名称、值以及is_env、is_readonly等标志位。所有变量存储在一个全局数组shell_vars中。命令实现:
declare:解析-x,-r选项和NAME=VALUE格式。调用add_var函数。该函数会先用find_var查找变量。如果存在且非只读,则更新;如果不存在,则在shell_vars数组中找一个空位创建。unset:调用remove_var,该函数会查找变量,如果存在且非只读,则将其从数组中移除。$变量展开:在执行命令前,增加一个expand_vars阶段。该函数遍历命令行字符串,查找$符号,提取后面的变量名,用find_var查找其值,并将$和变量名替换为其值。
2.2 关键代码
变量数据结构与管理 (user/include/var.h, user/lib/var.c)
1 | // user/include/var.h |
declare 命令处理 (user/sh.c)
1 | int handle_declare(int argc, char **argv) { |
变量展开 (user/sh.c)
1 | void expand_vars(char *in_buf, char *out_buf, int out_size) { |
3. 输入指令优化
3.1 设计思路
为了实现高级的行编辑功能,必须放弃简单的、带回显的 read 系统调用,转而使用更底层的、无回显的字符获取方式,并由 Shell 自己完全控制终端的回显和光标移动。
指令自由输入:
- 底层输入:使用
syscall_cgetc()获取单个字符,该调用不阻塞且无回显。 - 状态维护:在
readline函数中,维护两个核心状态:len(当前输入缓冲区的总长度)和cursor_pos(光标在缓冲区中的逻辑位置)。 - 处理普通字符:接收到可打印字符时,在
cursor_pos处将其插入缓冲区(需要移动后续字符),然后打印该字符,并使用 ANSI 转义序列重绘光标之后的内容,最后将光标移回正确位置。
- 底层输入:使用
不带
.b后缀:在spawn函数中,当尝试执行一个程序时,如果直接spawn失败,并且程序名不以.b结尾,就为其拼接上.b后缀再尝试一次。快捷键:
- 方向键:它们是 ANSI 转义序列(如
\x1b[A)。readline中有一个状态机来捕获这些序列。 - 退格键:修改缓冲区和
len、cursor_pos,并向终端发送“退格-空格-退格”序列来擦除字符,然后重绘光标后的内容。 - Ctrl-A/E/K/U/W:每个快捷键都对应一套对缓冲区
buf和len、cursor_pos的逻辑操作,以及一套相应的终端控制指令(如清屏\x1b[K,光标移动\r,\x1b[nC)来更新屏幕显示。
- 方向键:它们是 ANSI 转义序列(如
历史指令:使用一个全局数组
history_lines存储历史命令。load_history和add_to_history函数负责从/.mos_history文件中读写历史记录。**注释 **:我设计了一个辅助函数
find_and_split。这个函数负责从当前命令指针开始,查找第一个出现的有关字符。其中,#被视为行尾,它会立即停止对当前命令块的解析。反引号替换:
- 在变量展开之后、命令执行之前,增加一个反引号处理阶段。
- 该阶段遍历命令字符串,查找成对的
`。 - 对于每个
`...`块,提取其中的子命令。 - 创建一个管道,子进程执行子命令后通过管道把执行后的结果传给父进程。
一行多指令:为了支持在一行中通过
;分隔执行多个独立的命令,核心思路是将完整的命令行字符串分割成多个独立的命令块,然后按顺序依次执行。- 命令块分割:在
find_and_split当找到一个;时,它会将该位置的字符替换为\0,从而有效地将一个长命令字符串“截断”成一个独立的命令块。同时,函数返回下一个命令块的起始位置。 - 循环执行:在
sh.c的main函数中,我设计了一个主while循环。这个循环不断地调用find_and_split来获取下一个命令块。获取到命令块后,就立即对其进行变量展开、反引号替换,并最终通过fork和runcmd(或直接执行内置命令)来执行它。
通过这种“分割-执行”的循环模式,Shell 能够处理包含任意多个由
;分隔的指令行。- 命令块分割:在
3.2 关键代码
自由输入与快捷键 (user/sh.c: readline)
1 | void readline(char *buf, u_int n) { |
find_and_split函数
1 | char *find_and_split(char *s, char *op) { |
- 该函数负责查找命令中的
&&,||,#和;
4. 指令条件执行
4.1 设计思路
我把 Shell 的主循环设计成了一个能处理复杂命令行的状态机。
命令分隔与执行:
;:find_and_split来查找下一个命令分隔符(&&,||)。它将命令字符串在分隔符处用\0截断,形成一个个命令块。&&和||:主循环维护一个last_status(上一个命令的退出码) 和should_run_next(是否应执行下一个命令) 的状态。
exit返回状态:我修改了原有的exit()函数,使其能够支持获得命令执行的返回值。
5. 更多指令
5.1 设计思路
touch,mkdir,rm:- 这些命令都是独立的可执行程序 (
touch.c,mkdir.c,rm.c)。 - 它们通过
main函数的argc,argv解析命令行参数(如rm -rf)。 - 内部调用相应的库函数(最终是系统调用)来完成操作:
touch: 使用open的O_CREAT标志。mkdir: 使用新增的fsipc_mkdir->serve_mkdir->file_create_dir流程。-p选项通过递归调用mkdir_p实现。rm:stat判断是文件还是目录。对文件调用remove。对目录,如果带-r,则递归遍历并删除内容,最后删除自身。
- 这些命令都是独立的可执行程序 (
exit:- 这是一个内置命令,因为它需要终止当前的 Shell 进程。
handle_exit解析可选的数字参数,然后调用syscall_exit(code)退出。
6. 实现追加重定向
6.1 实现思路
追加重定向:
- 在
_gettoken中增加对>>的识别,返回一个特殊的 token。 - 在
parsecmd中,为这个新的 token 增加一个 case。 - 当遇到
>>时,使用open的O_WRONLY | O_CREAT | O_APPEND标志打开文件。O_APPEND是关键,它告诉文件系统后续的write操作都在文件末尾进行。 - 在
user/lib/fd.c的write函数中,也增加了对O_APPEND模式的检查,在每次写入前,先seek到文件末尾。
总结
本次实验成功地将 MOS 的 Shell 从一个简单的命令执行器,转变为一个具备现代 Shell 诸多核心功能的强大工具。通过实现CWD管理、环境变量、行编辑、历史命令、高级命令流控制等功能,不仅极大地提升了 MOS 的可用性和用户体验,更重要的是,在实践中深入理解了操作系统中进程状态管理、内存共享、IPC、文件系统交互等核心概念的底层实现细节。整个过程充满挑战,但也收获颇丰。