mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2186 字
6 分钟
Shell:命令解析与执行
2021-10-28

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 任务在调度器中与其他任务交替运行,每当轮到它执行时,就会处理用户的输入。

graph TD A[系统启动] --> B[main函数] B --> C[初始化子系统] C --> D[创建Shell任务] D --> E[启动调度器] E --> F[Shell任务被调度] F --> G[显示提示符] G --> H[等待用户输入] H --> I[解析命令] I --> J{命令类型} J -->|内建命令| K[直接执行] J -->|未知命令| L[报错] K --> G L --> G

命令解析#

用户输入的命令是字符串,需要将其分解为可执行的结构化数据:

  • 分离命令名和参数
  • 处理空格和制表符
  • 为命令执行准备参数数组

本章实现简单的空白分割解析器:

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;
}

这个解析器的特点:

  • 原地修改:直接在输入缓冲区中插入空字符
  • 简单高效:只处理空格和制表符
  • 无特殊语法:不支持引号、转义等复杂语法
flowchart LR A[输入: ls -l /home] --> B[跳过前导空白] B --> C[找到第一个token] C --> D[argv[0]='ls'] D --> E[插入分隔符] E --> F[找到第二个token] F --> G[argv[1]='-l'] G --> H[找到第三个token] H --> I[argv[2]='/home'] I --> J[返回argc=3]

内建命令#

内建命令是 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}
};

命令执行流程:

  1. 解析命令字符串,得到 argcargv
  2. 遍历命令表,查找匹配的命令名
  3. 调用对应的命令处理函数
  4. 如果找不到,显示”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] = "/"; // 当前工作目录
flowchart TD A[Shell任务启动] --> B[显示提示符] B --> C[等待键盘输入] C --> D{输入字符类型} D -->|回车| E[结束命令输入] D -->|退格| F[删除上一个字符] D -->|普通字符| G[添加到缓冲区] F --> B G --> B E --> H[解析命令] H --> I[保存到历史] I --> J[查找内建命令] J --> K{是否找到} K -->|是| L[执行命令] K -->|否| M[显示错误] L --> B M --> B

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]);
}

解析:命令执行的核心逻辑:

  1. 将命令保存到历史(使用 kmalloc 动态分配)
  2. 调用 parse_command 解析命令字符串
  3. 特殊命令(clear, exit)直接处理
  4. 遍历内建命令表,查找并执行匹配的命令
  5. 未找到则显示错误信息

内建命令示例 - 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_opendirfs_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.shell
make clean && make all && make run

预期输出#

=== Chapter 17: Shell ===
MyOS Shell v1.0
Type 'help' for available commands.
/> help
Available 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.txt
Welcome 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.txt
Hello 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
/> uptime
Uptime: 12 seconds (1200 ticks)
/> version
MyOS v1.0 - Teaching Operating System
Chapter 17: Shell Implementation
/> exit
Shutting down...

功能验证#

通过以下步骤验证 Shell 的各项功能:

  1. 命令解析:测试带多个参数的命令

    echo Hello MyOS Shell
  2. 文件操作:创建、写入、读取文件

    mkdir /tmp
    touch /tmp/test.txt
    write /tmp/test.txt This is a test
    cat /tmp/test.txt
  3. 目录导航:切换目录和显示路径

    cd /
    cd tmp
    pwd
    cd ..
    pwd
  4. 命令历史:重复执行之前的命令

    history
  5. 系统信息:查看进程和内存信息

    ps
    mem
    uptime

踩坑记录#

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 是简化版——不支持管道、重定向和外部程序加载,但它已经让用户可以通过命令与系统交互。下一章将实现更多用户工具,让系统具备完整的文件操作能力。

理解:

  1. Shell 的基本工作原理(解析-分发-执行)
  2. 命令解析的实现方法
  3. 内建命令的设计和实现
  4. 内核态 Shell 的优势和局限

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Shell:命令解析与执行
https://blog.souloss.com/posts/os/shell-command-parsing/
作者
Souloss
发布于
2021-10-28
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时