Shell 是操作系统最古老的用户界面。在图形界面出现之前,Shell 是人与计算机交互的唯一方式。内核有了文件系统、进程管理和系统调用,但用户无法直接调用这些功能——需要一个交互式的命令解释器。本章将实现一个简化的 Shell,能解析命令、执行内建命令、记录命令历史。
重要说明:本章实现的 Shell 是内核线程(kernel task),不是用户态程序。它直接调用内核文件系统 API,没有管道、重定向、fork/exec 等功能。这种设计简化了实现,同时让读者理解 Shell 的核心工作原理。
Shell 的角色
Shell(命令行解释器)是用户与操作系统交互的核心界面:
- 用户接口:提供友好的命令行界面,让用户可以通过输入命令与系统交互
- 命令执行:解析用户输入的命令,调用相应的系统功能
- 系统管理:提供文件管理、进程管理、系统信息查询等功能
- 学习价值:理解 Shell 的工作原理是掌握操作系统的重要一步
内核态 vs 用户态 Shell
在传统的操作系统(如 Linux)中,Shell 通常作为用户进程运行:
用户态 Shell:用户进程 → fork → exec → 命令程序内核态 Shell:内核线程 → 直接调用内核 API本章采用内核态设计的原因:
- 简化实现:无需实现复杂的 fork/exec 机制
- 教学价值:专注于命令解析和执行的核心逻辑
- 直接访问:可以直接调用内核文件系统 API,减少系统调用开销
Shell 的核心设计
Shell 作为内核任务
Shell 需要持续运行,等待用户输入并响应命令。将其实现为内核任务可以:
- 持续运行而不退出
- 直接访问内核数据结构和 API
- 简化实现,专注于命令处理逻辑
Shell 被创建为一个内核任务,拥有自己的执行上下文和内核栈:
// 在 main() 函数中创建 Shell 任务task_create_kernel("shell", shell_task, NULL, 5);
// Shell 任务的主函数static void shell_task(void *arg){ while (1) { // 显示提示符 vga_printf("%s> ", cwd);
// 读取用户输入 // 解析命令 // 执行命令 }}Shell 任务在调度器中与其他任务交替运行,每当轮到它执行时,就会处理用户的输入。
命令解析
用户输入的命令是字符串,需要将其分解为可执行的结构化数据:
- 分离命令名和参数
- 处理空格和制表符
- 为命令执行准备参数数组
本章实现简单的空白分割解析器:
static int parse_command(char *line, char *argv[]){ int argc = 0; char *p = line;
// 跳过前导空白 while (*p && argc < MAX_ARGS - 1) { while (*p == ' ' || *p == '\t') p++; if (!*p) break;
// 记录参数起始位置 argv[argc++] = p;
// 找到参数结束位置 while (*p && *p != ' ' && *p != '\t') p++;
// 用空字符分隔参数 if (*p) *p++ = '\0'; } argv[argc] = NULL; return argc;}这个解析器的特点:
- 原地修改:直接在输入缓冲区中插入空字符
- 简单高效:只处理空格和制表符
- 无特殊语法:不支持引号、转义等复杂语法
内建命令
内建命令是 Shell 直接执行的命令,无需创建新进程:
- 效率高:直接调用函数,无进程创建开销
- 访问特权:可以直接操作 Shell 的内部状态(如当前目录)
- 核心功能:实现 Shell 必需的基本操作
使用命令表(command table)将命令名映射到执行函数:
typedef struct { const char *name; void (*func)(int argc, char *argv[]);} builtin_t;
static builtin_t builtins[] = { {"help", cmd_help}, {"echo", cmd_echo}, {"ls", cmd_ls}, {"cat", cmd_cat}, {"mkdir", cmd_mkdir}, {"touch", cmd_touch}, {"write", cmd_write_file}, {"pwd", cmd_pwd}, {"cd", cmd_cd}, {"ps", cmd_ps}, {"mem", cmd_mem}, {"uptime", cmd_uptime}, {"history", cmd_history}, {"version", cmd_version}, {NULL, NULL}};命令执行流程:
- 解析命令字符串,得到
argc和argv - 遍历命令表,查找匹配的命令名
- 调用对应的命令处理函数
- 如果找不到,显示”command not found”错误
命令历史
命令历史提供便利的用户体验:
- 快速重复执行之前的命令
- 方便修改和调试命令
- 提升交互效率
使用循环缓冲区存储命令历史:
#define HISTORY_SIZE 8
static char *history[HISTORY_SIZE];static int hist_count = 0;
static void execute_command(char *line){ // 保存到历史 if (line[0] && hist_count < HISTORY_SIZE) { history[hist_count] = (char *)kmalloc(strlen(line) + 1); if (history[hist_count]) { strcpy(history[hist_count], line); hist_count++; } } // 执行命令...}查看历史的命令:
static void cmd_history(int argc, char *argv[]){ (void)argc; (void)argv; for (int i = 0; i < hist_count; i++) { vga_printf(" %d: %s\n", i + 1, history[i]); } if (hist_count == 0) vga_printf(" (empty)\n");}注意:完整的箭头键历史导航功能需要更复杂的键盘输入处理,本章只实现了基本的历史存储和显示功能。
代码实现
文件结构
17.shell/├── boot/│ ├── mbr.S # MBR 引导程序│ └── loader.S # 加载器├── kernel/│ ├── include/ # 头文件│ ├── kernel.c # Shell 实现文件│ ├── fs/ # 文件系统│ ├── drivers/ # 设备驱动(键盘驱动)│ └── ... # 其他内核模块└── Makefile # 构建配置关键点:Shell 代码直接位于 kernel/kernel.c,而不是独立的 shell.c 文件。这是因为 Shell 是内核的一部分,不是独立的可执行文件。
// 命令缓冲区#define CMD_BUF_SIZE 256#define MAX_ARGS 16#define HISTORY_SIZE 8
// Shell 状态static char cmd_buffer[CMD_BUF_SIZE]; // 命令输入缓冲区static int cmd_pos = 0; // 当前输入位置static char *history[HISTORY_SIZE]; // 命令历史static int hist_count = 0; // 历史条目数量static char cwd[64] = "/"; // 当前工作目录Shell 主循环
static void shell_task(void *arg){ (void)arg;
vga_printf("\nMyOS Shell v1.0\n"); vga_printf("Type 'help' for available commands.\n\n"); vga_printf("%s> ", cwd);
while (1) { char c = keyboard_getchar(); // 获取键盘输入
if (c == '\n' || c == '\r') { // 用户按下回车,执行命令 vga_printf("\n"); cmd_buffer[cmd_pos] = '\0'; if (cmd_pos > 0) execute_command(cmd_buffer); cmd_pos = 0; vga_printf("%s> ", cwd); } else if (c == '\b' || c == 127) { // 退格键删除 if (cmd_pos > 0) { cmd_pos--; vga_printf("\b \b"); } } else if (c >= 32 && cmd_pos < CMD_BUF_SIZE - 1) { // 可打印字符 cmd_buffer[cmd_pos++] = c; vga_printf("%c", c); } }}解析:这是 Shell 的主循环,不断等待用户输入:
keyboard_getchar()从键盘驱动获取字符- 回车键触发命令执行
- 退格键实现删除功能
- 可打印字符(ASCII 32以上)存入缓冲区并回显
命令执行和分发
static void execute_command(char *line){ // 保存到历史 if (line[0] && hist_count < HISTORY_SIZE) { history[hist_count] = (char *)kmalloc(strlen(line) + 1); if (history[hist_count]) { strcpy(history[hist_count], line); hist_count++; } }
char *argv[MAX_ARGS]; int argc = parse_command(line, argv); // 解析命令 if (argc == 0) return;
// 特殊命令处理 if (strcmp(argv[0], "clear") == 0) { vga_clear(); return; } if (strcmp(argv[0], "exit") == 0) { vga_printf("Shutting down...\n"); while (1) __asm__ volatile("hlt"); }
// 查找并执行内建命令 for (int i = 0; builtins[i].name; i++) { if (strcmp(argv[0], builtins[i].name) == 0 && builtins[i].func) { builtins[i].func(argc, argv); return; } }
vga_printf("%s: command not found\n", argv[0]);}解析:命令执行的核心逻辑:
- 将命令保存到历史(使用
kmalloc动态分配) - 调用
parse_command解析命令字符串 - 特殊命令(
clear,exit)直接处理 - 遍历内建命令表,查找并执行匹配的命令
- 未找到则显示错误信息
内建命令示例 - ls
static void cmd_ls(int argc, char *argv[]){ // 确定要列出的目录路径 const char *path = (argc > 1) ? argv[1] : cwd;
// 打开目录 int dir = fs_opendir(path); if (dir < 0) { vga_printf("ls: cannot open '%s'\n", path); return; }
// 读取并显示目录项 dirent_t *de; while ((de = fs_readdir(dir)) != NULL) { if (de->type == FILE_TYPE_DIRECTORY) vga_printf(" %s/\n", de->name); // 目录显示斜杠 else vga_printf(" %s\n", de->name); // 文件不显示斜杠 } fs_closedir(dir);}解析:ls 命令的实现:
- 如果未指定路径,使用当前工作目录
cwd - 调用文件系统 API
fs_opendir和fs_readdir - 根据文件类型(目录或文件)显示不同的格式
- 注意:这是内核态调用,不是通过系统调用
内建命令示例 - cd
static void cmd_cd(int argc, char *argv[]){ if (argc < 2) { // 无参数,回到根目录 strcpy(cwd, "/"); return; } if (argv[1][0] == '/') { // 绝对路径 strncpy(cwd, argv[1], sizeof(cwd) - 1); } else if (strcmp(argv[1], "..") == 0) { // 父目录(简化版:回到根目录) strcpy(cwd, "/"); } else { // 相对路径 char newpath[64]; sprintf(newpath, "%s/%s", strcmp(cwd, "/") == 0 ? "" : cwd, argv[1]); strncpy(cwd, newpath, sizeof(cwd) - 1); }}解析:cd 命令直接修改 Shell 的 cwd 状态:
- 无参数或
..返回根目录(简化实现) - 绝对路径(以
/开头)直接使用 - 相对路径拼接当前目录
- 这是一个典型的修改 Shell 内部状态的命令,必须在内核态实现
运行与验证
编译运行
cd 17.shellmake clean && make all && make run预期输出
=== Chapter 17: Shell ===
MyOS Shell v1.0Type 'help' for available commands.
/> helpAvailable commands: help - Show this help clear - Clear screen echo - Print arguments ls [dir] - List directory cat file - Show file contents mkdir d - Create directory touch f - Create empty file write f - Write text to file pwd - Print working directory cd dir - Change directory ps - List tasks mem - Memory info uptime - System uptime history - Command history version - System version exit - Halt system
/> ls / home/ bin/ readme.txt
/> cat /readme.txtWelcome to MyOS!This is a teaching operating system.Type 'help' in the shell for commands.
/> mkdir /home/user/> touch /home/user/test.txt/> write /home/user/test.txt Hello World/> ls /home/user test.txt
/> cat /home/user/test.txtHello World
/> cd /home/> pwd/home
/> history 1: help 2: ls / 3: cat /readme.txt 4: mkdir /home/user 5: touch /home/user/test.txt 6: write /home/user/test.txt Hello World 7: ls /home/user 8: cat /home/user/test.txt 9: cd /home 10: pwd 11: history
/> uptimeUptime: 12 seconds (1200 ticks)
/> versionMyOS v1.0 - Teaching Operating SystemChapter 17: Shell Implementation
/> exitShutting down...功能验证
通过以下步骤验证 Shell 的各项功能:
-
命令解析:测试带多个参数的命令
echo Hello MyOS Shell -
文件操作:创建、写入、读取文件
mkdir /tmptouch /tmp/test.txtwrite /tmp/test.txt This is a testcat /tmp/test.txt -
目录导航:切换目录和显示路径
cd /cd tmppwdcd ..pwd -
命令历史:重复执行之前的命令
history -
系统信息:查看进程和内存信息
psmemuptime
踩坑记录
1. 为什么不支持管道和重定向?
原因:管道和重定向需要复杂的进程管理和 I/O 重定向机制:
- 管道需要创建子进程并设置进程间通信
- 重定向需要修改文件描述符表
- 这些功能需要完整的 fork/exec、文件描述符管理等支持
当前设计:专注于命令解析和执行的核心逻辑,避免过早引入复杂机制。用户态工具(第18章)可以实现这些功能。
2. 命令历史为什么只能存储 8 条?
原因:简化实现,使用固定大小的数组:
#define HISTORY_SIZE 8改进方向:
- 使用循环缓冲区(ring buffer)可以存储更多历史
- 实现持久化存储(保存到磁盘)
- 添加历史搜索功能(Ctrl+R)
3. 为什么 cd 命令的 .. 只能回到根目录?
原因:简化了路径解析逻辑。完整的 .. 实现需要:
- 解析路径字符串
- 找到最后一个
/并截断 - 处理多级
..(如../../..)
当前实现:所有 .. 都返回根目录 /,避免了复杂的路径处理。
4. 错误提示为什么是英文?
原因:为了保持代码简洁,避免中文字符编码问题。在裸机环境中处理 UTF-8 编码需要额外的工作。
5. 为什么没有 Tab 自动补全?
原因:Tab 自动补全需要:
- 文件系统遍历功能
- 字符串匹配算法
- 更复杂的键盘输入处理(需要检测 Tab 键)
当前限制:键盘驱动只提供简单的字符输入,无法区分特殊功能键。
小结
Shell 作为内核线程运行,通过空白分割解析用户输入,16 个内建命令覆盖了文件操作(ls/cat/cp)、目录导航(cd)、系统控制(clear/reboot)等基本功能,命令历史记录提供了简单的回溯能力。这个 Shell 是简化版——不支持管道、重定向和外部程序加载,但它已经让用户可以通过命令与系统交互。下一章将实现更多用户工具,让系统具备完整的文件操作能力。
理解:
- Shell 的基本工作原理(解析-分发-执行)
- 命令解析的实现方法
- 内建命令的设计和实现
- 内核态 Shell 的优势和局限
参考
- 第16章:系统调用完善 - 系统调用框架
- 第14章:文件系统 - 文件系统实现
- POSIX Shell 规范 - 标准 Shell 的规范
- Bash Reference Manual - Bash Shell 的完整文档
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






