从零写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(三十六):ext2 写操作 —— 文件创建、引用计数与 tty 字符设备

前几章文件系统都是只读的。这一章实现 ext2 的写路径,让 echo hello > /tmp/a.txt && cat /tmp/a.txt 能正常工作,且重启后文件仍然存在。过程中还修了 fd 引用计数和 tty 字符设备两个问题。 ext2 磁盘结构回顾 块组 0: [超级块][组描述符][block bitmap][inode bitmap][inode table][数据块...] 关键字段: sb->s_free_inodes_count / gd->bg_free_inodes_count:空闲 inode 计数 sb->s_free_blocks_count / gd->bg_free_blocks_count:空闲 block 计数 gd->bg_inode_bitmap:inode bitmap 所在块号 gd->bg_block_bitmap:block bitmap 所在块号 gd->bg_inode_table:inode table 起始块号 inode 号从 1 开始;前 10 个是系统保留的(root=2,lost+found=11)。 alloc_inode / alloc_block alloc_inode uint32_t alloc_inode(void) { // 1. 读 inode bitmap 块 // 2. 找第一个为 0 的位(bit i → inode i+1) // 3. 置位,写回 bitmap // 4. 更新超级块和组描述符的 free_inodes_count // 5. 返回 inode 号(从 1 开始) } alloc_block uint32_t alloc_block(void) { // 1. 读 block bitmap 块 // 2. 找第一个为 0 的位,跳过 block 0(保留) // 3. 置位,写回 bitmap // 4. 更新超级块和组描述符的 free_blocks_count // 5. 清零新分配的块(避免垃圾数据) // 6. 返回块号 } 注意:block bitmap 的 bit 0 对应 block 0,必须跳过(block 0 不存在)。实际第一个可分配的块由文件系统布局决定(本项目中是 610)。 ...

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(十五):挂载 ext2,读真实文件系统

前几章的文件系统是存在内存里的——重启数据就没了,文件名也是硬编码的。这一章做真实的:挂载一个 ext2 磁盘镜像,让 Shell 能读取里面的文件。 上一章已经有了 ATA 驱动,能按扇区号读磁盘。现在的问题是:磁盘上的数据是怎么组织的? ext2 的结构 ext2 是 Linux 最经典的文件系统,ext3/ext4 都是在它基础上演化来的。理解 ext2,基本上就理解了现代文件系统的核心思路。 磁盘从偏移 1024 字节开始是 Superblock,存整个文件系统的基本参数:block 大小是多少、有多少 inode、magic number 是 0xEF53(用来确认这确实是 ext2)。 接下来是 Group Descriptor,告诉你 inode table 在哪个 block。 然后才是真正的数据区:inode table 和数据块。 Inode 是文件的"身份证"。每个文件有一个唯一的 inode 号,inode 里存着文件大小、权限,以及最重要的——i_block[0..11],12 个直接指向数据块的指针。想读文件内容,就顺着这些指针去读对应的数据块。 目录也是文件,它的数据块里存的是一条条 ext2_dir_entry:每条记录包含 inode 号、文件名长度、文件名。ls 就是读根目录的数据块,遍历这些记录。 读文件的完整路径 Superblock → 拿到 block_size、inodes_per_group GroupDesc → 拿到 inode table 的起始 block 号 Inode[ino] → 拿到文件大小和 i_block[] DataBlock → 真正的文件内容 读目录(ls)就在最后一步多做一件事:把数据块里的 ext2_dir_entry 链遍历一遍。 每一步都需要从磁盘读若干个扇区——这正是上一章 ATA 驱动的用武之地。 ...

May 6, 2026 · 1 min · 大飞
京ICP备14031575号-3