通过编写一个支持作业控制的 Unix Shell 程序,深入理解进程控制和信号处理的核心概念。
Shell 是一个交互式命令行解释器,负责接收用户指令并执行相应程序。本实验要求实现一个简化版的 Shell,包含基本的命令解析、进程创建、信号处理、作业控制和I/O重定向功能。实验提供了骨架代码和完整的命令解析器,你需要实现核心执行逻辑和控制机制。
关于 Shell 原理和实现方法的详细指导,请参阅实验指导文档。
- 命令提示符为
"tsh> " - 执行命令行程序(前台或后台运行)
- 实现作业控制(bg、fg 等)
- 处理键盘信号(Ctrl+C、Ctrl+Z)
- 实现基本的 I/O 重定向(
<、>) - 回收终止的子进程
quit/exit:终止 shelljobs:列出所有后台作业bg <job>:将一个停止状态的后台作业转为运行状态fg <job>:将一个后台作业转为前台运行cd <dir>:切换当前工作目录
为了确保自动化测试的正确性,你的 Shell 必须严格遵循以下输出格式:
-
后台作业启动:
[%d] (%d) %s &- 例如:
[1] (12345) /bin/ls -l &
- 例如:
-
作业被信号停止:
Job [%d] (%d) stopped by signal %d- 例如:
Job [1] (12345) stopped by signal 20
- 例如:
-
作业被信号终止:
Job [%d] (%d) terminated by signal %d- 例如:
Job [1] (12345) terminated by signal 2
- 例如:
-
运行中的作业:
[%d] (%d) Running %s- 例如:
[1] (12345) Running /bin/ls -l &
- 例如:
-
停止的作业:
[%d] (%d) Stopped %s- 例如:
[1] (12345) Stopped /bin/ls -l
- 例如:
-
bg命令输出:[%d] (%d) %s &- 例如:
[1] (12345) /bin/ls -l &
- 例如:
-
fg命令输出:不需要特殊输出,只显示命令本身
-
命令未找到:
%s: Command not found.- 例如:
foo: Command not found.
- 例如:
-
未指定作业:
%s command requires PID or %%jobid argument- 例如:
bg command requires PID or %jobid argument
- 例如:
-
bg/fg参数格式不合法:%s: argument must be a PID or %%jobid- 例如:
bg: argument must be a PID or %jobid
- 例如:
-
进程不存在:
(%d): No such process- 例如:
(12345): No such process
- 例如:
-
作业 ID 不存在:
%%%d: No such job- 例如:
%1: No such job
- 例如:
本实验提供多项选做任务,你需要任选两项完成:
- 基本管道支持 📊
- 实现单级管道连接(如
cmd1 | cmd2) - 相关函数:在
shell.c的eval函数中处理command_t的next字段
- 高级管道支持 📈
- 实现多级管道链(如
cmd1 | cmd2 | cmd3 | ...) - 需要先完成基本管道支持
- 环境变量展开 🔄
- 实现
$VAR和${VAR}格式的环境变量替换 - 相关函数:在
parser.c中实现env_expand函数
- 命令替换功能 🔁
- 实现
$(command)格式的命令输出替换 - 相关函数:在
parser.c中实现command_substitute函数
- 终端控制机制 💻
- 实现对终端的完整控制,支持运行 vim、gdb 等交互式程序
- 实现要点:正确管理进程组、控制终端所有权、终端属性设置
- 相关函数:在
shell.c和signals.c中添加终端控制相关代码
- PATH 环境变量支持 🔍
- 实现通过 PATH 环境变量查找可执行文件
- 实现要点:不需要输入完整路径即可执行在 PATH 中的程序
- 脚本执行功能 📜
- 支持从文件读取并执行一系列 Shell 命令,只需实现顺序执行
- 相关函数:在
shell.c中实现eval_script函数
- 环境变量支持(export) 🔧
- 实现
export VAR=value内建命令,支持在 shell 运行期间设置环境变量 - 设置的环境变量应当被子进程继承
- 相关函数:在
builtins.c中添加export命令支持
- 子 Shell(基础) 🐣
- 支持通过语法
(...)启动一个子 Shell,并在子进程中顺序执行其中的命令 - 子 Shell 中的环境修改(如
cd、设置变量等)不影响父 Shell - 实现要点:在命令解析阶段识别
(...)结构;在执行阶段,使用fork()创建新进程来执行子 Shell 命令序列
- 子 Shell(高级) 🚀
- 在基础子 Shell 的实现上,进一步支持子 Shell 作业控制、与父 Shell 管道连接等场景
- 需要更深入地管理进程组、终端控制与信号,使子 Shell 在前台/后台模式下都能正确处理信号
- 可探索让子 Shell 与父 Shell 共享部分状态(如环境变量)或实现更灵活的隔离
每项选做任务的详细要求及参考实现方法可参阅实验指导文档。
项目采用模块化设计,文件结构如下:
.
├── include
│ └── shell.h # 头文件,包含函数声明与数据结构
├── Makefile # 用于编译项目
├── src
│ ├── builtins.c # 内置命令的实现
│ ├── jobs.c # 作业控制相关函数
│ ├── main.c # 主函数
│ ├── parser.c # 命令解析相关函数
│ ├── shell.c # Shell 核心逻辑
│ ├── signals.c # 信号处理函数
│ └── utils.c # 工具函数
├── tests # 测试用例目录
│ └── cases # 包含多个测试点
└── tshref # 参考实现
Note
提供的代码框架更像是一个起步代码(starter code),如果你觉得现有框架限制了你的实现方式,可以根据需要直接修改代码结构,只要最终实现满足功能要求且能通过测试即可。
命令解析器提供了以下结构:
typedef struct command {
char *argv[MAXARGS]; /* 命令和参数 */
int argc; /* 参数数量 */
char *infile; /* 输入重定向文件 */
char *outfile; /* 输出重定向文件 */
int append; /* 输出重定向的追加模式标志 */
struct command *next; /* 管道:指向下一个命令 */
} command_t;
int parse_command_line(const char *cmdline, command_t **cmd, int *bg);必做任务需要实现的主要函数:
builtins.c:builtin_cmd,do_bgfg,do_cdshell.c:evalsignals.c:sigchld_handler,sigint_handler,sigtstp_handler
本实验提供了一个自动评测脚本,可以帮助你验证 Shell 实现的正确性。脚本支持多种测试模式,能够模拟用户输入、检查程序输出并提供详细反馈。
python grader.py # 运行所有测试
python grader.py 5-jobs # 只运行特定测试点我们还提供了一个参考实现的二进制可执行文件,你可以用它来对比你的实现。
./tshref如果想了解每个测试点具体执行的内容,可以运行:
# 查看测试会执行哪些命令但不实际运行
python grader.py -d 5-jobs你也可以直接查看测试点目录内容,每个测试点包含一个 config.toml 配置文件,描述了测试的元数据和执行步骤。
这些功能可能也会对你有所帮助:
- 详细输出:使用
-v参数查看每个测试步骤的详细输出,包括程序的所有输出和错误信息:python grader.py -v 6-sigint
- 对比参考实现:使用
--compare参数将你的Shell输出与参考实现进行对比:脚本会显示两者输出的差异,帮你识别输出格式问题或功能缺陷。python grader.py --compare 5-jobs
- 重新运行失败的测试:实现复杂功能时,可以先修复一部分问题,然后专注于剩余的失败测试:
python grader.py -f
- 生成VS Code调试配置:
这将为失败的测试生成VS Code调试配置,让你能够在调试器中步进执行代码,观察变量值和程序状态。
python grader.py --vscode
实验总分由以下三个部分组成:
- 功能正确性(50%)
- 评分依据:自动化测试通过率
- 计算公式:$50 \times \frac{\text{测试得分}}{\text{测试总分}}$
- 实现方式与扩展功能(20%)
- 满分条件(二选一):
- 使用 Rust 语言完成基础功能实现
- 或使用 C 语言实现并完成至少两项选做任务
- 评分方法:
- Rust 实现:基础功能完整即可获得全部分数
- C 语言实现:每完成一项选做任务得 10 分,上限 20 分
- 实验报告与代码质量(30%)
- 实验报告评分要点(15%):
- 实现思路清晰完整
- 关键功能设计与分析深入
- 测试结果分析全面
- 问题解决过程与经验总结
- 代码质量评分要点(15%):
- 代码结构设计合理
- 命名规范与注释完善
- 错误处理机制健全
- 实现的技术深度与创新性
为了鼓励同学们追求卓越,本实验将对成绩排名前三的优秀实现进行特别奖励:
评优标准将基于实验总评分,包含以下所有评分维度:
- 自动化测试的功能正确性(50%)
- 实现方式与扩展功能完成情况(20%)
- 实验报告质量与代码设计水平(30%)
获选的优秀实现将:
- 进行公示表彰
- 在征得作者同意后开源,作为学习参考资源
- 为作者提供平时成绩 5 分附加分作为奖励
参与评优无需额外申请,所有按时提交且功能完整的作业均自动纳入评选范围。我们期待看到你精心打造的高质量 Shell 实现,展现你对操作系统概念的深刻理解与应用能力。
我们高度重视学术诚信,它是计算机科学教育的基石。我们期望你能够独立完成实验,真正掌握系统编程的核心概念和技能。
-
鼓励的行为 ✅
- 与同学讨论实验的概念性问题和整体设计思路
- 在课堂或实验课上请教助教关于实验中遇到的困难
- 查阅官方文档、教科书和其他公开学习资源
-
禁止的行为 ❌
- 抄袭或共享代码(包括但不限于同学之间、网络资源等)
- 试图绕过或破解测试系统
- 让他人代为完成作业
Warning
我们使用代码相似度检测工具对所有提交的代码进行检查。一旦发现抄袭或其他学术不端行为,将严格按照学校相关规定处理。
在现代编程环境中,ChatGPT、Claude、DeepSeek 等大型语言模型以及基于它们的工具(如 GitHub Copilot、Cursor 等)已成为许多开发者的辅助工具。我们认可这些工具在学习过程中的价值,同时也希望你合理使用它们:
- 理解优先:AI 工具可以帮助解释概念、提供思路或优化代码片段,但不应替代你对底层原理的理解。使用这些工具前,请先尝试自己分析问题。
- 学习而非依赖:将 AI 工具视为学习助手而非解决方案提供者。如果使用 AI 生成代码,确保你完全理解每一行代码的作用及其背后的原理。同时,使用 AI 工具直接生成完整的实验解决方案并提交为自己的工作是不允许的。这不仅违背了学术诚信原则,也会阻碍你获得通过实验设计的学习体验。
- 批判性思考:AI 生成的内容可能存在错误或不适合特定场景。建议始终以批判性思维评估其建议,并根据你对操作系统概念的理解进行调整。
- 在报告中声明:如果你在实验过程中使用了 AI 工具获取重要帮助,建议在实验报告中简要说明使用方式和范围。
合理使用AI工具可以增强学习效果,但最终的理解和代码实现应反映你自己的努力和思考。操作系统是计算机科学的核心领域,亲自实现这些机制将为你的技术成长奠定坚实基础。
使用 GitHub Classroom 进行提交。请你确保所有代码已提交到你的对应仓库,GitHub Actions 会自动运行测试,其输出作为我们的评分依据。
提交截止日期:2025 年 3 月 21 日 23:59
提交内容:
- 所有源代码文件(通过 GitHub 仓库提交)
- 实验报告(需同时提交源文件和转换后的 PDF,详细要求请参阅报告模板与要求)
请确保在截止日期前完成最终提交。GitHub 会记录你的所有提交历史,我们将以截止日期前的最后一次提交作为最终版本进行评分。
完成基础实验后,你可能会对 Shell 的原理和实现产生更浓厚的兴趣。本节提供一些探索方向,帮助你将实验 Shell 逐步打磨成一款真正可用的现代 Shell 工具。你可以在完成必做与选做后,根据自身兴趣与时间投入进行深度拓展。
现代 Shell 的一大特色是提供丰富的交互体验。你可以考虑实现命令提示符的自定义与主题化,让用户能够展示当前时间、用户名、主机名、路径等信息,甚至支持彩色显示和动态更新。
命令自动补全是另一个极大提升效率的功能,当用户按下 Tab 键时,Shell 可以根据当前目录下的文件名或可执行命令列表进行智能补全。
更进一步,你可以实现命令历史管理,将用户执行过的命令保存到如 ~/.tsh_history 这样的文件中,并支持类似 Ctrl+R 的历史搜索功能。
行编辑能力也是现代 Shell 的标配。你可以引入 GNU Readline 或 libedit 库,或者自己实现简单的行编辑功能,支持光标移动、删除、撤销等操作。
语法高亮则能让用户在输入时直观地区分命令、参数、字符串等不同元素,甚至可以实时提示语法错误,大大降低使用门槛。
Shell 不仅是执行命令的工具,也是一种编程语言。你可以为你的 Shell 添加内建函数或脚本模块化加载功能,让用户能够编写并调用复杂的功能模块。
命令别名(alias)是另一个实用功能,让用户可以为常用命令定义简写形式,如 alias ll='ls -l'。
更进阶的特性包括扩展脚本语言能力,如变量定义、条件语句、循环结构等,甚至可以支持函数定义和局部变量作用域。
高级管道和进程间通信机制也是 Shell 脚本能力的重要组成部分,你可以探索如何支持多重管道、管道与子 Shell 的结合使用等。
真正实用的 Shell 需要与操作系统深度集成。PATH 管理与命令查找优化是一个很好的切入点,你可以支持在运行时动态添加、删除PATH中的条目,并将其持久化到配置文件。还可以使用缓存或哈希表对 PATH 下的可执行文件进行索引,加速命令查找。
配置文件与插件系统可以大大增强 Shell 的可定制性和扩展性。你可以设计读取 ~/.tshrc 这样的配置文件,在启动时加载用户的环境变量、自定义函数、别名等设置。更进一步,你可以定义插件接口,允许用户通过动态库扩展 Shell 的能力。
对于实际使用的 Shell,性能和安全同样重要。优化 Shell 的启动速度和内存占用是一个值得探索的方向,可以考虑懒加载部分功能,避免在初始化时进行大量预加载。同时,良好的内存管理可以防止在执行复杂命令或脚本时出现内存泄漏。
在安全方面,你可以实现命令执行的权限控制和安全检查,如白名单/黑名单机制,防止执行某些高危操作。对命令执行路径和权限的严格检查则可以预防路径注入攻击。
- POSIX Shell 标准 有助于理解 Shell 语法的最小通用规范和关键特性。
- GNU Bash 参考手册 Bash 包含了大量进阶功能,如命令替换、算术表达式、扩展模式匹配等,能为自定义 Shell 提供借鉴。
- Zsh 官方文档 Zsh 在交互性、可扩展性方面非常灵活,很多功能或设计理念值得学习。
- 高级 Bash 脚本指南 其中对 Shell 脚本的高级语法、技巧、陷阱均有说明,可以作为实现高级功能时的参考。