从零写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 · 大飞

从零写OS(十四):ATA 驱动,让内核能读磁盘

前面的文件系统都是存在内存里的——重启数据就没了。要读真实磁盘,得先搞清楚操作系统怎么和磁盘"说话"。这一章做 ATA 磁盘驱动。 磁盘和内核怎么通信 硬盘插在主板上,操作系统通过 ATA 协议和它通信。ATA 协议规定了一组固定的 x86 I/O 端口,内核用 in/out 指令直接操作这些端口,就能控制磁盘。 这叫 PIO 模式(Programmed I/O)——CPU 亲自一个字搬一个字地读数据。慢,但实现只需要几十行代码,是学习的最佳起点。 真实生产内核用 DMA(磁盘直接写内存,CPU 不搬数据),那是后话。 磁盘的最小单位:扇区 磁盘被切成 512 字节的扇区,每个扇区有一个编号,叫 LBA(Logical Block Address),从 0 开始数。 LBA=0 → 前 512 字节(Boot Sector) LBA=1 → 512~1023 字节 LBA=2 → 1024~1535 字节(ext2 Superblock 就在这里) ... 要读文件系统里偏移 1024 字节的内容,就是读 LBA = 1024 / 512 = 2。 ATA 端口 ATA 协议设计于 1980 年代,把控制磁盘的所有操作映射到一组固定的 I/O 端口号上——这是当时 PC 硬件的惯例,如今这些端口号已经写死在无数设备里,成了不能改的"历史遗产"。 Primary ATA 控制器的端口: 端口 用途 0x1F0 数据(读写 16-bit) 0x1F2 要读几个扇区 0x1F3 LBA[7:0] 0x1F4 LBA[15:8] 0x1F5 LBA[23:16] 0x1F6 选盘 + LBA[27:24] 0x1F7 命令(写)/ 状态(读) 状态寄存器的两个关键位: ...

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