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