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、文件系统交互等核心概念的底层实现细节。整个过程充满挑战,但也收获颇丰。