前十二章,内核从一个 512 字节的 Bootloader 长成了有进程调度、文件系统、VFS 的微型系统。但用户还没有办法和它交互。

这一章做 Shell——一个可以敲命令操作文件的命令行。


先把概念搞清楚

Shell 是普通进程

Shell 不是内核的一部分。它是一个运行在用户态的普通进程,通过系统调用和内核打交道,和 lscat 这些程序的地位完全一样。

真实系统里,Shell 用 fork + exec 启动外部命令。我们这里简化:四条命令直接内嵌在 Shell 进程里,不做 fork/exec。

tokenize:命令行的第一步

用户输入一行字符串,Shell 要把它拆成命令名和参数:

"write hello.txt world"
        ↓ tokenize
argv[0] = "write"
argv[1] = "hello.txt"
argv[2] = "world"

做法是遍历字符串,遇到空格就写入 \0 截断,记录每段的起始地址。不需要任何库函数,20 行搞定。

串口 I/O

键盘输入通过串口读取(x86 端口 0x3F8)。QEMU 的 -serial mon:stdio 把宿主机终端直接映射到串口,你在终端敲的每个字符都会被 in 指令读到。


实现了什么

四条命令:

命令 功能
write <文件> <内容> 创建文件并写入内容
read <文件> 读取文件内容并打印
ls 列出根目录所有文件
help 打印帮助

关键代码

readline:读一行,支持 backspace

static int readline(char *buf, int maxlen) {
    int i = 0;
    while (i < maxlen - 1) {
        char c = serial_getchar();
        if (c == '\r' || c == '\n') {
            serial_print("\r\n");
            break;
        }
        if (c == 127 || c == '\b') {
            if (i > 0) { i--; serial_print("\b \b"); }
            continue;
        }
        buf[i++] = c;
        char echo[2] = {c, 0};
        serial_print(echo);   // 回显给用户
    }
    buf[i] = '\0';
    return i;
}

\b \b 是终端删除字符的标准做法:退格、打空格覆盖、再退格。

主循环

void shell_run(void) {
    serial_print("\r\nSimpleOS Shell\r\n");
    char line[256];
    char *argv[8];
    while (1) {
        serial_print("\r\n> ");
        readline(line, sizeof(line));
        int argc = tokenize(line, argv, 8);
        if (!argc) continue;
        if      (streq(argv[0], "help"))  cmd_help();
        else if (streq(argv[0], "ls"))    cmd_ls();
        else if (streq(argv[0], "read"))  cmd_read(argc, argv);
        else if (streq(argv[0], "write")) cmd_write(argc, argv);
        else { serial_print("unknown: "); serial_print(argv[0]); serial_print("\r\n"); }
    }
}

cmd_writecmd_read 内部直接调 vfs_open / vfs_write / vfs_read / vfs_close,完全不知道底层是什么文件系统。


跑起来

Kernel loaded!
SimpleOS Shell
commands:
  write <file> <content>
  read  <file>
  ls
  help

> write hello.txt world
written.

> read hello.txt
world

> ls
hello.txt

从 bootloader 到能敲命令操作文件,13 章。


一个真实踩坑

加完 Shell 之后,输入 write 命令内核直接崩溃:

[EXCEPTION] Invalid Opcode (rip=0x0000000000000003)

rip=3 说明跳到了一个垃圾地址。排查了半天函数指针、初始化顺序,最后发现原因很蠢:

kernel.bin 编译出来是 10304 字节,但 boot.asm 里硬编码 mov al, 20,只从磁盘加载 20 个扇区(10240 字节)——差了 64 字节,Shell 的代码被截掉了一截。

修法:mov al, 30

教训:每次加新模块,检查 kernel.bin 大小,确保加载扇区数够用。


下一章

十三章走完,从 512 字节的 Bootloader 到能交互的 Shell,整个系统的骨架已经成型。下一章做一个完整回顾,看看我们建了什么、和真实 Linux 的差距在哪里。

源码:github.com/tongpengfei/learn-with-ai