前十二章,内核从一个 512 字节的 Bootloader 长成了有进程调度、文件系统、VFS 的微型系统。但用户还没有办法和它交互。
这一章做 Shell——一个可以敲命令操作文件的命令行。
先把概念搞清楚
Shell 是普通进程
Shell 不是内核的一部分。它是一个运行在用户态的普通进程,通过系统调用和内核打交道,和 ls、cat 这些程序的地位完全一样。
真实系统里,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_write 和 cmd_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 的差距在哪里。