前面的文件系统都是存在内存里的——重启数据就没了。要读真实磁盘,得先搞清楚操作系统怎么和磁盘"说话"。这一章做 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 命令(写)/ 状态(读)

状态寄存器的两个关键位:

  • BSY(bit7):磁盘忙,必须等它清零再发命令
  • DRQ(bit3):数据就绪,可以开始读

读一个扇区

void ata_read_sector(uint32_t lba, void *buf) {
    // 等磁盘空闲
    while (inb(0x1F7) & 0x80);

    // 选主盘,写 LBA[27:24]
    outb(0x1F6, 0xE0 | ((lba >> 24) & 0x0F));

    // 读 1 个扇区
    outb(0x1F2, 1);

    // LBA 地址
    outb(0x1F3, lba & 0xFF);
    outb(0x1F4, (lba >> 8) & 0xFF);
    outb(0x1F5, (lba >> 16) & 0xFF);

    // 发 READ 命令
    outb(0x1F7, 0x20);

    // 等数据就绪
    while (!(inb(0x1F7) & 0x08));

    // 读 256 个 word = 512 字节
    uint16_t *p = (uint16_t *)buf;
    for (int i = 0; i < 256; i++)
        p[i] = inw(0x1F0);
}

就这些。七步,几十行,内核就能读磁盘了。


主盘和从盘

0x1F6 的 bit4 决定读哪块盘:

  • 0xE0 → 第一块盘(master,hda)
  • 0xF0 → 第二块盘(slave,hdb)

QEMU 里 -drive file=myos.img 是 master,-drive file=ext2.img 是 slave。搞错这一位,读出来的是另一块盘的内容,必然崩溃。


这一章的位置

ATA 驱动是文件系统的地基:

Shell
 └── VFS
      └── ext2 驱动
           └── read_bytes()      ← 字节级封装
                └── ata_read_sector()   ← 本章

下一章用这个驱动去读真实的 ext2 文件系统。

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