从零写OS(四十二):让 busybox 跑起来 —— 符号链接、fork/exec、调度器 cpu_pin bug

上一章实现了 TCP/IP 栈,用户程序能发 HTTP 请求了。但验证时发现 /bin/ls 完全没反应,wget_test 也跑不起来。这一章记录排查过程——一共修了 6 个 bug,涉及 ext2 fast symlink、VFS 路径解析、内核栈溢出、缺失 syscall,以及两个调度器 cpu_pin 问题。 最终效果: / # ls bin etc lost+found / # cd bin /bin # ls busybox cp kill mv pwd sh wget_test cat echo ls ps rm umount /bin # wget_test wget_test start connecting... connected! request sent HTTP/1.0 200 OK ... DONE /bin # 背景:busybox 的目录结构 Makefile 用这种方式制作 ext2 镜像: sudo cp busybox-x86_64 /tmp/ext2mnt/bin/busybox sudo cp busybox-x86_64 /tmp/ext2mnt/bin/sh # sh 是真实复制 for cmd in ls cat echo pwd ...; do sudo ln -sf /bin/busybox /tmp/ext2mnt/bin/$cmd # 其他命令是符号链接 done /bin/sh 是真实文件,/bin/ls 等是指向 /bin/busybox 的符号链接。 ...

June 1, 2026 · 4 min · 大飞

从零写OS(三十四):阻塞式管道与调度器的两个 Bug

上一章实现了阻塞式 TTY,这一章在同样的思路上让 pipe 的 read 也变成阻塞式,同时修复了调度器里隐藏的两个 Bug,让 fork + exec + wait4 的完整流程跑通。 问题:非阻塞 pipe read 的后果 ch33 之前,vfs_read 对 pipe 的实现是: if (pipe_buf_empty(p)) return 0; // 管道空 → 直接返回 0 对于 shell 管道命令,busybox sh 会: fork 出子进程执行左侧命令,写 pipe 父进程(或另一子进程)读 pipe,处理右侧命令 如果读端在写端还没写入时就读到 0,shell 认为 EOF,管道提前关闭,命令输出丢失。 解决方案:sti/hlt 等待 与 ch33 阻塞式 TTY read 相同的思路:在内核态用 sti; hlt 轮询等待。 // pipe read:等待数据或写端关闭 while (pipe_buf_empty(p) && pipe_has_writer(p)) { __asm__ volatile("sti" ::: "memory"); __asm__ volatile("hlt" ::: "memory"); __asm__ volatile("cli" ::: "memory"); } if (pipe_buf_empty(p)) return 0; // 写端已关闭且无数据 → EOF 关键点: ...

May 22, 2026 · 2 min · 大飞

从零写OS(三十三):阻塞式 TTY —— read 不再忙等

上一章 busybox sh 成功显示了 / # 提示符,但 shell 拿不到任何输入——因为 tty_read 是忙轮询的,没有字符就直接返回 0,shell 以为收到 EOF,立刻退出。这一章实现真正的阻塞式 TTY,让 shell 能等待用户输入。 之前的问题 之前的 tty_read 大概是这样: int tty_read(char *buf, int len) { // 轮询串口状态寄存器 if (!(inb(0x3F8 + 5) & 1)) return 0; // 没数据就返回 0 *buf = inb(0x3F8); return 1; } 这有两个问题: 没有输入时返回 0,上层程序(shell)以为是 EOF 如果上层在循环里调这个,CPU 100% 占用 正确做法:没有输入时挂起进程,等串口中断来了再唤醒。 串口中断(IRQ4) COM1 对应 IRQ4,接在 PIC 主片的 IR4 引脚。要用串口中断,需要两步: 1. 在 PIC 上 unmask IRQ4: // PIC 主片 IMR 寄存器:0 表示开放,1 表示屏蔽 uint8_t mask = inb(0x21); mask &= ~(1 << 4); // 清除 bit4,开放 IRQ4 outb(0x21, mask); 2. 开启串口的接收中断: ...

May 22, 2026 · 2 min · 大飞

从零写OS(三十一):运行 musl libc 程序 —— 两个隐藏的寄存器 Bug

上一章对齐了 Linux syscall ABI,现在试着跑一个真正用 musl libc 静态编译的 C 程序。目标很简单: #include <stdio.h> int main(int argc, char **argv) { printf("hello from musl libc!\n"); printf("argc=%d\n", argc); return 0; } 编译:musl-gcc -static -O2 -o hello.elf hello.c 结果踩了两个隐藏很深的寄存器 bug,花了不少时间才找出来。 新增的 syscall musl libc 启动时会调用一批 syscall,其中有三个是绕不过的: syscall 编号 说明 arch_prctl(ARCH_SET_FS, addr) 158 设置 TLS base(FS 寄存器) set_tid_address(tidptr) 218 设置线程 ID 地址,返回 pid writev(fd, iov, iovcnt) 20 向量写,musl 的 stdio 用它输出 arch_prctl 需要写 MSR 0xC0000100(FS.base),这是 TLS 的基地址,musl 用来存放线程局部变量: case SYS_ARCH_PRCTL: if (a1 == ARCH_SET_FS) { wrmsr(0xC0000100, a2); // FS.base MSR return 0; } return (uint64_t)(-EINVAL); writev 需要遍历 iov 数组,把多段数据拼起来写到 tty: ...

May 22, 2026 · 3 min · 大飞

从零写OS(二十九):block cache —— 给磁盘加一层缓存

Linux 内核里有个叫 page cache(以前叫 buffer cache)的东西,所有磁盘 IO 都要经过它。为什么?因为磁盘太慢了——ATA PIO 读一个扇区要等几毫秒,而内存访问只要几纳秒。把最近访问过的扇区留在内存里,下次再访问直接从内存读,速度提升几千倍。 这一章给我们的 ext2 文件系统加上这层缓存,同时顺手修了一个隐藏很深的调度器 bug。 设计:LRU write-back 缓存 最简单够用的设计:固定 64 个 slot,每个 slot 缓存一个 512 字节扇区,LRU 淘汰,write-back 写回。 typedef struct { uint32_t lba; uint8_t data[512]; uint8_t valid; uint8_t dirty; uint32_t lru_time; } bcache_slot_t; static bcache_slot_t slots[64]; static uint32_t clock = 0; lru_time 用一个全局 clock 计数器实现——每次访问 clock++,命中的 slot 拿到最新值,淘汰时找 lru_time 最小的那个。 bcache_get(lba) uint8_t *bcache_get(uint32_t lba) { clock++; // 命中? for (int i = 0; i < 64; i++) { if (slots[i].valid && slots[i].lba == lba) { slots[i].lru_time = clock; return slots[i].data; } } // 找 LRU victim int victim = 0; for (int i = 1; i < 64; i++) { if (!slots[i].valid) { victim = i; break; } if (slots[i].lru_time < slots[victim].lru_time) victim = i; } // victim 是 dirty 的?先写回磁盘 if (slots[victim].valid && slots[victim].dirty) ata_write_sector(slots[victim].lba, slots[victim].data); // 读新扇区 ata_read_sector(lba, slots[victim].data); slots[victim] = (bcache_slot_t){ lba, ..., valid=1, dirty=0, lru_time=clock }; return slots[victim].data; } 返回的是指向缓存数据的指针,调用方可以直接读写这块内存。写完后调 bcache_dirty(lba) 标记为脏。 ...

May 15, 2026 · 4 min · 大飞

从零写OS(二十八):timer —— 让进程睡一会儿

Linux 里的 sleep(2) 背后是什么? 进程调用 sleep,内核把它标记为"阻塞",让出 CPU 给其他进程,等时间到了再唤醒它。实现这个功能需要两个组件配合:定时器(知道时间到了)+ 调度器(切换进程)。 好消息是这两个我们都有了:PIT 100Hz 时钟 + round-robin 调度器。这一章只需要把它们接起来。 核心思路 SYS_SLEEP(ms) → sleep_until = pit_get_ticks() + ms/10 → state = PROC_BLOCKED IRQ0 handler(每 10ms) → pit_tick() // ticks++ → timer_tick() // 扫描所有进程,到期则唤醒 → sched_tick() // 调度下一个 READY 进程 进程睡觉时设好"闹钟"(sleep_until),然后自己进入 PROC_BLOCKED。 每次时钟中断,timer_tick 扫一遍所有进程,到期的改回 PROC_READY,下次调度就能运行了。 实现 1. process_t 加一个字段 typedef struct { // ... 原有字段 uint64_t sleep_until; // 睡到哪个 tick,0 表示没在睡 } process_t; 2. timer.c void timer_tick(void) { uint64_t now = pit_get_ticks(); for (int i = 0; i < MAX_PROCS; i++) { if (procs[i].state == PROC_BLOCKED && procs[i].sleep_until > 0) { if (now >= procs[i].sleep_until) { procs[i].sleep_until = 0; procs[i].state = PROC_READY; } } } } 逻辑极简:遍历,到期,唤醒。 ...

May 15, 2026 · 2 min · 大飞
京ICP备14031575号-3