从零写 OS 内核-第十一篇:块设备驱动 —— 让 OS 真正访问硬盘!
"文件系统再精巧,若无法与真实硬件对话,也只是纸上谈兵。
今天,我们为内核添加 IDE/ATA 驱动,让 ext2 文件系统跑在真正的磁盘上!"
在上一篇中,我们实现了 ext2 文件系统,但测试时依赖 QEMU 模拟的 -hda。
而 QEMU 背后,是它替我们完成了磁盘 I/O——你的内核实际上并未直接操作硬件。
真正的操作系统,必须能通过块设备驱动(Block Device Driver),
直接与 IDE/PATA、AHCI/SATA 或 NVMe 控制器通信,读写磁盘扇区。
今天,我们就以 经典 IDE/ATA 接口 为例,实现一个极简但可用的块设备驱动,
让你的 OS 能从物理硬盘(或 QEMU 模拟的 IDE 磁盘)加载程序!
💾 一、为什么需要块设备驱动?
文件系统(如 ext2)只关心"逻辑块"(例如"读第 100 个块"),
但如何将逻辑块转换为物理磁盘操作,是块设备驱动的职责。
块设备驱动的核心任务:
- 探测硬件:检测 IDE 控制器和连接的磁盘
- 发送命令:如 "READ SECTOR"、"WRITE SECTOR"
- 传输数据:通过 PIO(Programmed I/O)或 DMA 读写扇区
- 错误处理:超时、CRC 错误等
💡 初期我们使用 PIO 模式(简单,无需 DMA 设置),适合学习。
🔌 二、IDE/ATA 硬件接口详解
经典的 IDE 接口通过 I/O 端口 通信,主通道端口如下:
| 端口(主通道) | 名称 | 作用 |
|---|---|---|
0x1F0 | Data | 数据寄存器(16 位) |
0x1F1 | Error | 错误寄存器 |
0x1F2 | Sector Count | 要读写的扇区数 |
0x1F3 | LBA Low | LBA 地址低 8 位 |
0x1F4 | LBA Mid | LBA 地址中 8 位 |
0x1F5 | LBA High | LBA 地址高 8 位 |
0x1F6 | Drive/Head | 驱动器选择 + LBA 高位 |
0x1F7 | Status | 状态寄存器(只读) |
0x1F7 | Command | 命令寄存器(只写) |
关键状态位(Status 寄存器):
- BSY (bit 7):1 = 控制器忙
- DRQ (bit 3):1 = 数据就绪(可读写 Data 端口)
- ERR (bit 0):1 = 上次操作出错
⚠️ 所有操作前必须检查 BSY=0 且 DRQ=1(读时)!
⚙️ 三、实现 IDE PIO 读扇区
步骤(READ SECTOR 命令):
- 等待控制器空闲(BSY=0)
- 写入参数:扇区数、LBA 地址、驱动器号
- 发送 READ SECTOR 命令(0x20)
- 等待 DRQ=1(数据就绪)
- 从 Data 端口读取 256 次(每次 2 字节),共 512 字节
代码实现:
#define IDE_PRIMARY_IO 0x1F0
#define IDE_REG_STATUS(ide) (ide + 7)
#define IDE_REG_COMMAND(ide) (ide + 7)
#define IDE_REG_DATA(ide) (ide)
#define IDE_CMD_READ_SECTOR 0x20
// 从 IDE 磁盘读取一个扇区(512 字节)
bool ide_read_sector(uint8_t drive, uint32_t lba, void *buffer) {
uint16_t io_base = IDE_PRIMARY_IO;
uint8_t status;
// 1. 等待控制器空闲
do {
status = inb(IDE_REG_STATUS(io_base));
} while (status & (1 << 7)); // BSY
// 2. 设置参数
outb(IDE_REG_DATA(io_base) + 1, 0x00); // error
outb(IDE_REG_DATA(io_base) + 2, 0x01); // 扇区数 = 1
outb(IDE_REG_DATA(io_base) + 3, (lba >> 0) & 0xFF); // LBA Low
outb(IDE_REG_DATA(io_base) + 4, (lba >> 8) & 0xFF); // LBA Mid
outb(IDE_REG_DATA(io_base) + 5, (lba >> 16) & 0xFF); // LBA High
outb(IDE_REG_DATA(io_base) + 6, 0xE0 | (drive << 4) | ((lba >> 24) & 0x0F));
// 3. 发送命令
outb(IDE_REG_COMMAND(io_base), IDE_CMD_READ_SECTOR);
// 4. 等待数据就绪
do {
status = inb(IDE_REG_STATUS(io_base));
if (status & (1 << 0)) return false; // ERR
} while (!(status & (1 << 3))); // 等待 DRQ
// 5. 读取数据(256 次 word)
uint16_t *buf = (uint16_t*)buffer;
for (int i = 0; i < 256; i++) {
buf[i] = inw(IDE_REG_DATA(io_base));
}
return true;
}
🔑
inb/outb/inw:内联汇编实现的端口 I/O 指令。
🧱 四、块设备抽象层
为让文件系统不依赖具体硬件,我们定义块设备接口:
struct block_device {
const char *name;
uint64_t size; // 总扇区数
int (*read_block)(struct block_device *bdev, uint64_t block, void *buffer);
int (*write_block)(struct block_device *bdev, uint64_t block, const void *buffer);
void *private_data; // 指向 ide_device
};
// IDE 设备注册
static struct block_device ide_bdev = {
.name = "hda",
.read_block = ide_bdev_read,
.write_block = ide_bdev_write,
.private_data = &ide_primary_master,
};
文件系统如何使用?
// ext2_read_block 中调用
bdev->read_block(bdev, block_num, buffer);
✅ ext2 无需知道底层是 IDE、AHCI 还是 SD 卡!
🔍 五、磁盘探测与初始化
1. 探测 IDE 控制器
- 检查 PCI 设备(若支持 PCI),或直接假设端口存在
- 尝试 IDENTIFY DEVICE 命令获取磁盘信息
2. IDENTIFY DEVICE 命令
bool ide_identify(uint8_t drive, void *buffer) {
// 类似 read_sector,但命令为 0xEC
// 返回 512 字节的 IDENTIFY 数据
// 包含:磁盘型号、总扇区数、是否支持 LBA48 等
}
3. 初始化块设备
void ide_init() {
if (ide_identify(0, &identify_data)) {
ide_bdev.size = *(uint64_t*)&identify_data[120]; // 总扇区数(LBA28)
block_device_register(&ide_bdev);
}
}
🧪 六、整合:从 IDE 磁盘启动 ext2
启动流程:
- 内核初始化 →
ide_init() - 挂载根文件系统:
struct super_block *sb = ext2_mount(&ide_bdev); vfs_set_root(sb->s_root); - 执行 init 进程:
exec("/init", (char*[]){"init", NULL});
QEMU 测试命令:
# 创建磁盘镜像并写入 ext2
dd if=/dev/zero of=disk.img bs=1M count=32
mkfs.ext2 -F disk.img
# 挂载并复制 init
sudo mount -o loop disk.img /mnt
sudo cp user_init /mnt/init
sudo umount /mnt
# 启动(QEMU 模拟 IDE 磁盘)
qemu-system-i386 -kernel kernel.bin -hda disk.img -serial stdio
运行效果:
Welcome to MyOS!
#
✅ 你的 OS 现在通过自己的 IDE 驱动,从磁盘加载并运行了 init 程序!
⚠️ 七、局限与未来方向
当前 IDE 驱动仍很基础,但为后续打下基础:
| 问题 | 未来方案 |
|---|---|
| 仅 PIO 模式 | 实现 DMA 提升性能 |
| 仅主通道主盘 | 支持从盘、次通道 |
| 无中断 | 使用中断避免轮询(提升 CPU 效率) |
| 仅 LBA28 | 支持 LBA48(>137GB 磁盘) |
| 无写支持 | 实现 WRITE SECTOR 命令 |
🌱 下一步:实现 AHCI 驱动(现代 SATA 标准),或 SD 卡驱动(树莓派)!
💬 写在最后
块设备驱动是操作系统与物理世界的第一道桥梁。
它枯燥、琐碎,充满硬件细节,
但正是这些代码,让"0 和 1"真正落到了磁盘的磁畴上。
今天你写的 inb/outb,
正是 Linux 内核 drivers/ata/ 目录下千行代码的起点。
🌟 没有驱动,内核只是空中楼阁;有了驱动,它才脚踏实地。
📬 动手挑战:
修改 IDE 驱动,支持读取第二个扇区(LBA=1),并在启动时打印其内容。
欢迎在评论区分享你的磁盘十六进制转储!
👇 下一篇你想看:AHCI (SATA) 驱动,还是 SD 卡驱动(树莓派)?
#操作系统 #内核开发 #块设备驱动 #IDE #ATA #硬盘 #磁盘I/O #从零开始
📢 彩蛋:关注后回复关键词 "ide",获取:
- 完整 IDE PIO 驱动代码(含 inb/outb 实现)
- IDENTIFY DEVICE 数据结构解析
- QEMU IDE 调试技巧(寄存器监控)