从零写 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 命令):

  1. 等待控制器空闲(BSY=0)
  2. 写入参数:扇区数、LBA 地址、驱动器号
  3. 发送 READ SECTOR 命令(0x20)
  4. 等待 DRQ=1(数据就绪)
  5. 从 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

启动流程:

  1. 内核初始化ide_init()
  2. 挂载根文件系统
    struct super_block *sb = ext2_mount(&ide_bdev);
    vfs_set_root(sb->s_root);
    
  3. 执行 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 调试技巧(寄存器监控)