《操作系统真象还原》第十三章——编写硬盘驱动程序

前言

上章完成了系统调用,博客链接如下。

系统调用框架:《操作系统真象还原》第十二章(1)——进一步完善内核-CSDN博客

用户打印函数:《操作系统真象还原》第十二章(2)——进一步完善内核-CSDN博客

堆内存管理:《操作系统真象还原》第十二章(3)——进一步完善内核-CSDN博客

我们在加载loader.bin 的时候,已经接触了硬盘基本操作。但那时的操作方法还不成体系,咱们为了实现文件系统,必须建立好一套完整的方法,这套方法就是磁盘驱动程序,本章咱们要完成它。


硬盘及分区表

创建从盘及获取安装的磁盘数

原有硬盘c.img我们作为系统盘,现在我们创建一个新的文件盘,给它分配80mb空间。我打算顺带调整一下原有硬盘的名字和路径,这样更清晰一些。

先创建新硬盘吧。路径在/bin/下,用bximage命令,按照要求输入即可,硬盘名我设置为file_hd_80M。创建好的目录截图如下。

然后修改配置文件,代码如下

######################################
#### Configuration file for Bochs  ###
######################################
megs: 32

romimage: file=/home/hongbai/bochs/share/bochs/BIOS-bochs-latest
vgaromimage: file=/home/hongbai/bochs/share/bochs/VGABIOS-lgpl-latest

boot: disk

log: bochsout.txt

mouse: enabled=0
keyboard: keymap=/home/hongbai/bochs/share/bochs/keymaps/x11-pc-us.map

ata0:enabled=1,ioaddr1=0x1f0,ioaddr2=0x3f0,irq=14
ata0-master: type=disk, path="./bin/c.img",mode=flat,cylinders=121,heads=16,spt=63
ata0-slave: type=disk, path="./bin/file_hd_80M.img", mode=flat,cylinders=162,heads=16,spt=63
############### end ###############

用xp/b 0x475查看两块硬盘是否都已经载入,结果如图

目前我们这部分的任务已经完成了,但是我想顺带修改一下系统盘名称。我计划是重新再创建一个60mb的硬盘,修改好名字,然后把之前的文件(mbr、loader、kernel)重新写入即可。

正好补一张创建硬盘过程的截图,大部分直接回车即可。

然后修改我们写入代码时的路径,顺带贴出来我目前写入时的指令

以下是写入mbr和loader的,放在我创建的一个文本文档build.txt里
nasm -I include/ -o root/mbr.bin root/mbr.S
dd if=/home/hongbai/bochs/root/mbr.bin of=/home/hongbai/bochs/bin/os_hd_60M.img bs=512 count=1 conv=notrunc

nasm -I include/ -o root/loader.bin root/loader.S
dd if=/home/hongbai/bochs/root/loader.bin of=/home/hongbai/bochs/bin/os_hd_60M.img bs=512 count=4 seek=2 conv=notrunc

make all
./bochs -f ./bin/bochsrc

这是修改后的makefile写入路径,是makefile文件的一部分
hd:
	dd if=$(BUILD_DIR)/kernel.bin \
           of=/home/hongbai/bochs/bin/os_hd_60M.img \
           bs=512 count=200 seek=10 conv=notrunc

使用了新的系统盘后的运行截图

旧的系统盘c.img以后就不在使用了,我们目前的系统盘是os_hd_60M.img,文件盘是file_hd_80M.img。

创建磁盘分区表

文件系统是运行在操作系统中的软件模块,是操作系统提供的一套管理磁盘文件读写的方法和数据组织、存储形式,因此,文件系统=数据结构+算法,所以它是程序。它的管理对象是文件,管辖范围是分区,因此它建立在分区的基础上,每个分区都可以有不同的文件系统。但咱们刚创建了磁盘而已,磁盘还是裸盘,即raw disk,本节的任务是把刚创建的磁盘分区。

我们先快速回顾一下(机械)硬盘的基础知识:

扇区:硬盘读写的基本单位,通常是512字节大小。

磁道:扇区的载体,是一个个同心圆,一个磁道上有一圈扇区。

对于周长不同的磁道,扇区数是否一致?老硬盘是一样的, 新式硬盘中已经改进了,外圈磁道会容纳更多的扇区,在新硬盘中有个地址转换器来兼容老硬盘的扇区寻址, 因此依然可以认为硬盘中每个磁道上的扇区数一样多。

磁头:读取数据的装置。一个盘片有正反两个磁头,目前硬盘有多个盘片,就有2x盘片个磁头。磁头有编号,磁头号等于盘面号。

柱面:为了提高读写速度,把若干盘片摞起来形成一个圆柱,让若干个磁头并行工作。圆柱的工作面称为柱面,一个圆柱上下盘的磁道号是一致的,因而柱面号=磁道号。

分区:是由多个编号连续的柱面组成的,因此分区在物理上的表现是由某段范围内的所有柱面组成的 通心环,并不是像“饼图”那种逻辑表示,当然若整个硬盘只有1个分区,那这个分区就是个所有柱面组 成的圆柱体。分区不能跨柱面,也就是同一个柱面不能包含两个分区,一个柱面只属于一个分区,分区的 起始和终止都落在完整的柱面上,并不会出现多个分区共享同一柱面的情况,这就是所谓的“分区粒度”。 因此,分区大小等于“每柱面上的扇区数”乘以“柱面数”,这就是我们实际分区时,键入的大小往往与 实际大小不同的原因,分区大小总会是“每柱面上的扇区数”的整数倍,也就是会以柱面向上取整。假如 硬盘上有n个柱面,1~10柱面是a分区,11~23是b分区,这两个分区不共享11号柱面。

现在得出公式。

(1)硬盘容量=单片容量×磁头数。

(2)单片容量=每磁道扇区数×磁道数×512字节。

磁道数又等于柱面数,因此将公式2代入公式1后: 硬盘容量=每磁道扇区数×柱面数×512字节×磁头数

这些知识有助于我们更好的使用fdisk命令。

关于分区,总结一下知识点:

硬盘的mbr中有一个64字节的分区表结构,分区表的表项16字节,一个表项指示一个分区,因而原生只支持4个分区。后续为了实现更多的分区,专门增加一种id属性值(id为5),用来表示该分区可被再次划分出更多的子分区。反正分区本质上也只是一种逻辑过程,就无所谓了。

原生的四个分区,一个作为拓展分区,可以划分为若干个子分区,剩下三个就称为主分区。第一个逻辑子分区编号为5。

开始实践部分,这部分完整过程书里很详细,但是和我的fdisk软件版本不同,所以下面给出我的步骤,供大家参考

查看属性指令:
fdisk -l ./bin/file_hd_80M.img
开始分区
fdisk ./bin/file_hd_80M.img

开始设置部分
输入m得到命令菜单
输入x得到专家命令菜单
输入c设置柱面 设置为163
输入h设置磁头 输入16
输入r返回主菜单

开始硬盘创建
输入n,选择主分区,分区号1,起始扇区终止扇区见下图
输入n,选择扩展分区,分区号4
然后继续输入n,提示开始逻辑分区,创建5-9逻辑分区
然后输入t修改5-9的分区id,设置为0x66
最后输入w保存

再次查看属性:
fdisk -l ./bin/file_hd_80M.img

5.6上午就这样,吃饭去了,下午还要复习信号与系统。

硬盘分区表浅析

这部分介绍硬盘分析表细节。其实没太看懂,可能因为我用的fdisk和郑钢老师的版本不同,分区后呈现出的信息也不同,做不到对应,理解起来就有点困难。把书上这部分文字梳理一遍吧:

我们之前已经引入了硬盘分区表DPT,64字节,包括4个项。每个分区都有一个分区表。

第一个分区表在file_hd_80m.img的mbr扇区中。关于这个我想说一下我的理解:对于系统盘,我们编写过mbr引导程序,让bios运行完之后运行。这个mbr程序所在的0.0.1扇区就是mbr扇区。对于系统盘,我们写了加载loader的程序,对于文件盘,上面什么都没有。

现在告诉我们mbr扇区可以分为三部分(1)主引导记录MBR。 (2)磁盘分区表DPT。 (3)结束魔数55AA,表示此扇区为主引导扇区,里面包含控制程序。对于系统盘,我们没有第二部分,系统盘也没分过区。文件盘肯定是三部分都要有。

下一部分,分区的起始地址必须是柱面的整数倍,因而mbr那个柱面我们就不能划入分区。

最早硬盘很小,一个硬盘1个分区表4个分区足够使用。随着硬盘容量变大,4个分区1个分区表不够用,我们引入了扩展分区概念,那么对应的,每个子扩展分区在逻辑上被视作一个硬盘,它也有自己的分区表,这个分区表同样是4个表项。我们用链式结构把所有的分区表串在一起。

对于每个子扩展分区(逻辑分区),它们有一个类似于mbr扇区的扇区,称为EBR引导扇区,结构和mbr扇区一模一样。只不过,mbr的分区表1个表项,16字节对应一个分区。到了ebr就比较奢侈,一个分区表只对应1个分区,因而EBR中分区表的第一分区 表项用来描述所包含的逻辑分区的元信息,第二分区表项用来描述下一个子扩展分区的地址,第三、四表 项未用到。

这是分区表表项的截图,来自真象还原。重点关注两个4字节数据,起始地址和扇区数。

后面比较简单了,先放一张截图。

这是郑钢老师的分区图,和我的有很大不同。

我查了一下。**现代硬盘为了追求读写效率,要求分区按4kb/1mb对齐分区,而不是传统的512b。1mb=2048*512b,这就是为什么我们每个分区的起始扇区号总是2048的整数倍。**一会编写硬盘驱动的时候,肯定按我们的分区进行组织。


编写硬盘驱动程序

整体流程我们在写完代码后补充。。。

硬盘初始化

简单来说就是把物理上的硬件抽象成逻辑上的数据结构,用代码代表硬件。

我们知道操作系统是由中断驱动的,想要让cpu和硬盘建立关联,一定是硬盘发出中断信号,cpu进行处理。具体而言,一块主板(书上错误的写成了硬盘)上有两个ATA通道,称为主通道和从通道,对于每个通道,又支持一主一从两个设备(2个硬盘)。第一个通道在中断处理芯片8259A从片的IRQ14引脚上,第二个通道在IRQ15引脚上。而8259A从片又级联在8259A主片的IRQ2接口。

修改interrupt.c
...
/* 初始化可编程中断控制器8259A */
static void pic_init(void)
{

    /* 初始化主片 */
    outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
    outb(PIC_M_DATA, 0x04); // ICW3: IR2接从片.
    outb(PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 初始化从片 */
    outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
    outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
    outb(PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
    //主片打开时钟中断IRQ0、键盘中断IRQ1、级联从片的IRQ3
    outb(PIC_M_DATA, 0xf8);
    //从盘打开IRQ14,接受硬盘中断
    outb(PIC_S_DATA, 0xbf);

    put_str("  pic_init done\n");
}
...
编写ide.h

路径在device/ide.h


#include "../lib/kernel/stdint.h"
#include "../lib/kernel/list.h"
#include "../thread/sync.h"
/*ata通道结构*/
struct ide_channel
{
    char name[8];               // ata通道名称
    uint16_t port_base;         // 本通道起始端口号
    uint8_t irq_no;             // 本通道使用的中断号
    struct lock lock;           // 通道锁
    bool expecting_intr;        // 用来表示是否等待硬盘中断
    struct semaphore disk_done; // 用来阻塞、唤醒驱动程序
    struct disk devices[2];     // 一个通道上可以连接两个硬盘
};
/*硬盘结构*/
struct disk
{
    char name[8];                    // 硬盘名称
    struct ide_channel *my_channel;  // 硬盘使用的ide通道
    uint8_t dev_no;                  // 本硬盘是主盘还是从盘,主0从1
    struct partition prim_parts[4];  // 主分区,上限为4
    struct partition logic_parts[8]; // 逻辑分区,理论上无上限,我设置为只支持8个
};
/*分区结构*/
struct partition
{
    uint32_t start_lba;         // 起始扇区
    uint32_t sec_cnt;           // 扇区数
    struct disk *my_disk;       // 分区所属的硬盘
    struct list_elem part_tag;  // 对列标记
    char name[8];               // 分区名称
    struct super_block *sb;     // 超级块
    struct bitmap block_bitmap; // 块的位图
    struct bitmap inode_bitmap; // i节点位图
    struct list open_inodes;    // i节点队列
};
void ide_init(void);
编写ide.c

#include "ide.h"
#include "../lib/stdio.h"
#include "../kernel/debug.h"
#include "../kernel/global.h"
#include "../thread/sync.h"
// 主IDE通道寄存器端口的基址是0x1F0,从通道基址是0x170
#define reg_data(channel) (channel->port_base + 0)     // 读写数据,每次传输两字节
#define reg_error(channel) (channel->port_base + 1)    // 读取硬盘错误信息
#define reg_sect_cnt(channel) (channel->port_base + 2) // 扇区计数
#define reg_lba_l(channel) (channel->port_base + 3)    // 起始扇区LBA低八位
#define reg_lba_m(channel) (channel->port_base + 4)    // 中八位
#define reg_lba_h(channel) (channel->port_base + 5)    // 高八位
#define reg_dev(channel) (channel->port_base + 6)      // 驱动器/磁头寄存器
#define reg_status(channel) (channel->port_base + 7)   // 状态寄存器
#define reg_cmd(channel) (reg_status(channel))         // 命令寄存器,和状态寄存器共用0x1F7端口
#define reg_alt_status(channel->port_base + 0x206)     // 备用状态寄存器
#define reg_ctl(channel) (reg_alt_status(channel))     // 设备控制寄存器,和前者共用0x3F6端口
// 记录reg_alt_status备用状态寄存器的一些关键位
#define BIT_ALT_STAT_BSY 0x80  // 硬盘忙
#define BIT_ALT_STAT_DRDY 0x40 // 驱动器准备好了
#define BIT_ALT_STAT_DRQ 0x8   // 数据传输准备好了
// 记录device驱动器/磁头寄存器的一些关键位
#define BIT_DEV_MBS 0xa0
#define BIT_DEV_LBA 0x40
#define BIT_DEV_DEV 0x10
// 一些硬盘操作的指令
#define CMD_IDENTIFY 0xec     // identify识别硬盘指令
#define CMD_READ_SECTOR 0x20  // 读扇区指令
#define CMD_WRITE_SECTOR 0x30 // 写扇区指令
// 定义可读写的最大扇区数,用于调试
#define max_lba ((80 * 1024 * 1024 / 512) - 1) // 只支持80MB硬盘

uint8_t channel_cnt;            // 通道数
struct ide_channel channels[2]; // 一个主板最多有两个通道

/*硬盘数据结构初始化*/
void ide_init()
{
    printk("ide_init start\n");
    // BIOS扫描获取的硬盘数保存在物理地址0x475上,低1MB虚拟地址等于物理地址
    uint8_t hd_cnt = *((uint8_t)(0x475));
    ASSERT(hd_cnt > 0);
    channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 计算通道数,一个通道对应两个硬盘
    struct ide_channel *channel;
    uint8_t channel_no = 0;
    // 处理每个通道上的硬盘
    while (channel_no < channel_cnt)
    {
        channel = &channels[channel_no];
        sprintf(channel->name, "ide%d", channel_no); // 设置通道名
        if (channel_no == 0)
        { // 主通道
            channel->port_base = 0x1f0;
            channel->irq_no = 0x20 + 14; // 对应从片IRQ14引脚
        }
        else if (channel_no == 1)
        { // 从通道
            channel->port_base = 0x170;
            channel->irq_no = 0x20 + 15;
        }
        // 默认不需要等待硬盘中断
        channel->expecting_intr = false;
        lock_init(&channel->lock);
        sema_init(&channel->disk_done, 0);
        channel_no++;
    }
    printk("ide_init done\n");
}

这部分就算完成了。

实现thread_yield和idle线程

先说明一下接下来两部分在干什么。

硬盘和CPU 是相互独立的个体,它们各自并行执行,但由于硬盘是低速设备,其在处理请求时往往 消耗很长的时间,为避免浪费CPU资源,在等待硬盘操 作的过程中最好把CPU主动让出来,让CPU去执行其他任务。

thread_yield函数由当前线程执行,让当前线程主动把cpu使用权交出来。和block不同,交出cpu后,当前线程依旧是ready状态,会加入就绪队列。

idle线程是一个特殊的线程。初始状态为阻塞,当就绪队列为空时,它被唤醒,执行它内部的hlt指令,让cpu停止执行指令,休息。

执行hlt指令后,只有外部中断能再次唤醒cpu,所以在执行hlt之前一定要先sti开中断,保证cpu还能被唤醒。

修改thread.c
...
struct task_struct *idle_thread; // idle线程

/*设置系统空闲时运行的线程*/
static void idle(void)
{
    while (1)
    {
        thread_block(TASK_BLOCKED); // 初始为阻塞状态
        // 被唤醒后执行下面的代码
        asm volatile("sti; hlt" : : : "memory");
    }
}
...
/*主动让出CPU,切换其他线程运行*/
void thread_yield(void){
    struct task_struct *cur = running_thread();
    enum intr_status old_status = intr_disable();
    ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
    cur->status = TASK_READY;
    schedule();
    intr_set_status(old_status);
}

/* 初始化线程环境 */
void thread_init(void)
{
    put_str("thread_init start\n");
    list_init(&thread_ready_list); // 初始化就绪线程队列
    list_init(&thread_all_list);   // 初始化所有线程队列
    lock_init(&pid_lock);          // 初始化pid锁
    make_main_thread();            // 创建主线程
    //创建idle线程
    idle_thread = thread_start("idle", 10, idle, NULL);
    put_str("thread_init done\n");
}
...

记得把void thread_yield(void)声明写进头文件里。

实现简单的休眠函数

修改timer.c

这个文件不长,我就贴出全文吧。

// 时钟中断相关
#include "./timer.h"
#include "../kernel/io.h"
#include "../lib/kernel/print.h"
#include "../kernel/interrupt.h"
#include "../thread/thread.h"
#include "../kernel/debug.h"

#define IRQ0_FREQUENCY 100      // 时钟中断频率
#define INPUT_FREQUENCY 1193180 // 8254PIT的输入时钟频率
// 计数器初始值
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define COUNTER0_PORT 0X40     // Counter0的数据端口,通过该端口写入计数器初始值
#define COUNTER0_NO 0          // 选择Counter0
#define COUNTER_MODE 2         // 选择模式2,进行周期中断
#define READ_WRITE_LATCH 3     // 表示先写低字节,再写高字节
#define PIT_COUNTROL_PORT 0x43 // 控制字寄存器端口
// 计算每多少毫秒发生1次中断
#define mil_second_per_intr (1000 / IRQ0_FREQUENCY)

uint32_t ticks; // ticks是内核自中断开启以来的滴答数,一个tick就是一次时钟中断

/* 配置8524PIT */
void frequency_set(uint8_t counter_port, uint8_t counter_no, uint8_t rwl, uint8_t counter_mode, uint16_t counter_value)
{
    outb(PIT_COUNTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
    outb(counter_port, (uint8_t)counter_value);
    outb(counter_port, (uint8_t)counter_value >> 8);
    return;
}

/* 时钟的中断处理函数 */
void intr_timer_handler(void)
{
    struct task_struct *cur_thread = running_thread();
    ASSERT(cur_thread->stack_magic == 0x19870916); // 检测栈溢出
    cur_thread->elapsed_ticks++;                   // 线程运行的时间加1
    // 每次时钟中断,ticks加1
    ticks++;
    if (cur_thread->ticks == 0)
    {               // 如果当前线程的时间片用完
        schedule(); // 调度其他线程
    }
    else
    {
        cur_thread->ticks--; // 否则,当前线程的时间片减1
    }
    return;
}

/* 初始化PIT8253 */
void timer_init(void)
{
    put_str("timer_init start\n");
    frequency_set(COUNTER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
    register_handler(0x20, intr_timer_handler); // 注册时钟中断处理函数
    put_str("timer_init done\n");
    return;
}

/*以tick为单位的sleep,让当前线程休眠sleep_ticks次中断
 *每次中断全局变量ticks++,计算某次中断后ticks和开始休眠时的start_tick的差值,
 *决定是继续休眠还是结束休眠*/
static void ticks_to_sleep(uint32_t sleep_ticks)
{
    uint32_t start_tick = ticks;
    while (ticks - start_tick < sleep_ticks) // 如果差值没达到目标,就一直休眠
    {
        thread_yield();
    }
}

/*以毫秒ms为单位的休眠*/
void mtime_sleep(uint32_t m_second)
{
    uint32_t sleep_ticks = DIV_ROUND_UP(m_second, mil_second_per_intr);
    ASSERT(sleep_ticks > 0);
    ticks_to_sleep(sleep_ticks);
}

记得把新写的函数写到头文件里。

完善硬盘驱动程序

继续编写ide.c

这部分写完是200多行代码。


#include "ide.h"
#include "../lib/stdio.h"
#include "../kernel/debug.h"
#include "../kernel/global.h"
#include "../thread/sync.h"
#include "../kernel/io.h"
#include "timer.h"
#include "../kernel/interrupt.h"

/*主IDE通道寄存器端口的基址是0x1F0,从通道基址是0x170*/
#define reg_data(channel) (channel->port_base + 0)     // 读写数据,每次传输两字节
#define reg_error(channel) (channel->port_base + 1)    // 读取硬盘错误信息
#define reg_sect_cnt(channel) (channel->port_base + 2) // 扇区计数
#define reg_lba_l(channel) (channel->port_base + 3)    // 起始扇区LBA低八位
#define reg_lba_m(channel) (channel->port_base + 4)    // 中八位
#define reg_lba_h(channel) (channel->port_base + 5)    // 高八位
#define reg_dev(channel) (channel->port_base + 6)      // 驱动器/磁头寄存器
#define reg_status(channel) (channel->port_base + 7)   // 状态寄存器
#define reg_cmd(channel) (reg_status(channel))         // 命令寄存器,和状态寄存器共用0x1F7端口
#define reg_alt_status(channel->port_base + 0x206)     // 备用状态寄存器
#define reg_ctl(channel) (reg_alt_status(channel))     // 设备控制寄存器,和前者共用0x3F6端口

/*记录reg_alt_status备用状态寄存器的一些关键位*/
#define BIT_ALT_STAT_BSY 0x80  // 1000 0000,表示硬盘忙
#define BIT_ALT_STAT_DRDY 0x40 // 0100 0000,表示驱动器准备好了
#define BIT_ALT_STAT_DRQ 0x8   // 0000 1000,表示数据传输准备好了

/*记录device驱动器/磁头寄存器的一些关键位*/
#define BIT_DEV_MBS 0xa0 // 1010 0000,设置必须的保留位
#define BIT_DEV_LBA 0x40 // 0100 0000,启用LBA模式
#define BIT_DEV_DEV 0x10 // 0001 0000,如果使用此位,则选择了从盘

/*一些硬盘操作指令的指令码*/
#define CMD_IDENTIFY 0xec     // identify识别硬盘指令
#define CMD_READ_SECTOR 0x20  // 读扇区指令
#define CMD_WRITE_SECTOR 0x30 // 写扇区指令

// 定义可读写的最大扇区数,用于调试
#define max_lba ((80 * 1024 * 1024 / 512) - 1) // 只支持80MB硬盘

uint8_t channel_cnt;            // 通道数
struct ide_channel channels[2]; // 一个主板最多有两个通道

/*选择要读写的硬盘*/
static void select_disk(struct disk *hd)
{
    uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;
    if (hd->dev_no == 1) // 如果是从盘
    {
        reg_device |= BIT_DEV_DEV;
    }
    // 通过汇编写入通道寄存器
    outb(reg_dev(hd->my_channel), reg_device);
}

/*向硬盘控制器写入起始扇区地址和要读写的扇区数*/
static void select_sector(struct disk *hd, uint32_t lba, uint8_t sec_cnt)
{
    ASSERT(lba <= max_lba);
    struct ide_channel *channel = hd->my_channel;
    // 写入扇区数
    outb(reg_sect_cnt(channel), sec_cnt);
    // 写入0-23位LBA
    outb(reg_lba_l(channel), lba);
    outb(reg_lba_m(channel), lba >> 8);
    outb(reg_lba_h(channel), lba >> 16);
    // 还有24-27四位LBA要写在dev寄存器里
    outb(reg_dev(channel), BIT_DEV_MBS | BIT_DEV_LBA | (hd->dev_no == 1 ? BIT_DEV_DEV : 0) | lba >> 24);
}

/*向通道channel发命令cmd*/
static void cmd_out(struct ide_channel *channel, uint8_t cmd)
{
    // 因为我们已经向通道发出了命令,所以将此位置为true,供硬盘中断处理程序使用
    channel->expecting_intr = true;
    outb(reg_cmd(channel), cmd);
}

/*从硬盘读出sec_cnt个扇区的数据到内存buf*/
static void read_from_sector(struct disk *hd, void *buf, uint8_t sec_cnt)
{
    // 先计算待读取字节数
    uint32_t byte = 0;
    if (sec_cnt == 0) // sec_cnt范围是0-255,如果传入256会变为0
    {
        byte = 256 * 512;
    }
    else
    {
        byte = sec_cnt * 512;
    }
    // 按字读取,1字=2字节
    insw(reg_data(hd->my_channel), buf, byte / 2);
}

/*从内存buf向硬盘写入sec_cnt个扇区的数据*/
static void write_to_sector(struct disk *hd, void *buf, uint8_t sec_cnt)
{
    uint32_t byte = 0;
    if (sec_cnt == 0) // sec_cnt范围是0-255,如果传入256会变为0
    {
        byte = 256 * 512;
    }
    else
    {
        byte = sec_cnt * 512;
    }
    outsw(reg_data(hd->my_channel), buf, byte / 2);
}

/*等待30秒,本质是优化了的自选锁*/
static bool busy_wait(struct disk *hd)
{
    struct ide_channel *channel = hd->my_channel;
    uint16_t time_limit = 30 * 1000;
    while (time_limit -= 10 >= 0)
    {
        // 如果硬盘不忙
        if (!(inb(reg_status(channel)) & BIT_ALT_STAT_BSY))
        {
            // 已经准备好数据传输
            return (inb(reg_status(channel)) & BIT_ALT_STAT_DRQ);
        }
        else
        {
            mtime_sleep(10); // 睡眠10ms,10ms后再次判断
        }
    }
    return false;
}

/*从硬盘sec_cnt个扇区读取数据到内存buf的全过程*/
void ide_read(struct disk *hd, uint32_t lba, void *buf, uint8_t sec_cnt)
{
    ASSERT(lba <= max_lba);
    ASSERT(sec_cnt > 0);
    lock_acquire(&hd->my_channel->lock); // 加锁表示通道已被占用

    // 1.选择要操作的硬盘
    select_disk(hd);

    uint32_t secs_op = 0;   // 本次要处理的扇区数,最大为256
    uint32_t secs_done = 0; // 已经处理的扇区数
    while (secs_done < sec_cnt)
    {
        if (sec_cnt - secs_done >= 256)
        {
            secs_op = 256;
        }
        else
        {
            secs_op = sec_cnt - secs_done;
        }
        // 2.写入待读取的扇区数和起始扇区号
        select_sector(hd, lba + secs_done, secs_op);
        // 3.写入读取命令到cmd寄存器
        cmd_out(hd->my_channel, CMD_READ_SECTOR);
        /*硬盘IO最慢的环节是硬盘内部处理环节,机械硬盘涉及到磁头移动等物理过程
         *此时,硬盘收到信号,开始在内部进行数据处理,我们可以让读取线程先阻塞自己
         *在读取完成后,通过中断处理程序唤醒线程,实现cpu的高效利用*/
        sema_down(&hd->my_channel->disk_done);

        /*此时,硬盘内部处理完成,程序被唤醒,继续接下来的环节*/
        // 4.检查硬盘状态是否可读
        if (!busy_wait(hd)) // 30秒内硬盘一直处于忙状态
        {
            char error[64];
            sprintf(error, "%s read sector %d failed!!!!!!\n", hd->name, lba);
            PANIC(error);
        }
        // 5.把读取出数据放到内存buf中
        read_from_sector(hd, (void *)((uint32_t)buf + secs_done * 512), secs_op);

        secs_done += secs_op;
    }
    lock_release(&hd->my_channel->lock);
}

/*从内存buf写入sec_cnt个扇区数据到硬盘的全过程*/
void ide_write(struct disk *hd, uint32_t lba, void *buf, uint8_t sec_cnt)
{
    // 写入和读取过程基本一致
    ASSERT(lba <= max_lba);
    ASSERT(sec_cnt > 0);
    lock_acquire(hd->my_channel->lock);

    select_disk(hd); // 选取硬盘

    uint32_t secs_op = 0;
    uint32_t secs_done = 0;
    while (secs_done < sec_cnt)
    {
        if (sec_cnt - secs_done >= 256)
        {
            secs_op = 256;
        }
        else
        {
            secs_op = sec_cnt - secs_done;
        }
        select_sector(hd, lba + secs_done, secs_op); // 写入起始扇区号和扇区数
        cmd_out(hd->my_channel, CMD_WRITE_SECTOR);   // 写入写命令
        if (!busy_wait(hd))                          // 检验状态
        {
            char error[64];
            sprintf(error, "%s write sector %d failed!!!!!!\n", hd->name, lba);
            PANIC(error);
        }
        write_to_sector(hd, (void *)((uint32_t)buf + secs_done * 256), secs_op);
        sema_down(&hd->my_channel->disk_done); // 阻塞
        secs_done += secs_op;
    }
    lock_release(&hd->my_channel->lock);
}

/*硬盘中断处理程序*/
void intr_hd_handler(uint8_t irq_no)
{
    ASSERT(irq_no == 0x2e || irq_no == 0x2f);
    // 0x2e对应14引脚,是主通道,0x2f对应从通道
    uint32_t no = irq_no - 0x2e;
    struct ide_channel *channel = channels[no];
    ASSERT(channel->irq_no == irq_no);
    if (channel->expecting_intr == true)
    {
        channel->expecting_intr = false;
        sema_up(&channel->disk_done);
        // 修改状态,让硬盘可以执行新的读写
        inb(reg_status(channel));
    }
}

/*硬盘数据结构初始化*/
void ide_init()
{
    printk("ide_init start\n");
    // BIOS扫描获取的硬盘数保存在物理地址0x475上,低1MB虚拟地址等于物理地址
    uint8_t hd_cnt = *((uint8_t)(0x475));
    ASSERT(hd_cnt > 0);
    channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 计算通道数,一个通道对应两个硬盘
    struct ide_channel *channel;
    uint8_t channel_no = 0;
    // 处理每个通道上的硬盘
    while (channel_no < channel_cnt)
    {
        channel = &channels[channel_no];
        sprintf(channel->name, "ide%d", channel_no); // 设置通道名
        if (channel_no == 0)
        { // 主通道
            channel->port_base = 0x1f0;
            channel->irq_no = 0x20 + 14; // 对应从片IRQ14引脚
        }
        else if (channel_no == 1)
        { // 从通道
            channel->port_base = 0x170;
            channel->irq_no = 0x20 + 15;
        }
        // 默认不需要等待硬盘中断
        channel->expecting_intr = false;
        lock_init(&channel->lock);
        sema_init(&channel->disk_done, 0);
        // 注册中断处理程序
        register_handler(channel->irq_no, intr_hd_handler);
        channel_no++;
    }
    printk("ide_init done\n");
}

获取硬盘信息,扫描分区表

我们已经完成了读写命令,下一步完成identify命令和扫描分区表

先介绍identify,用于获取硬盘参数,返回值以字为单位。下图列出了我们需要的三个参数。

再介绍一下扫描分区表,我们需要给每个分区起一个名字,参考linux命名规则,我们的文件硬盘file_hd_80M上主分区名为sdb[1-4],逻辑分区名为sdb[5-n]。

这部分代码依旧写在ide.c文件,我们给出完整的ide.c吧


#include "ide.h"
#include "../lib/stdio.h"
#include "../kernel/debug.h"
#include "../kernel/global.h"
#include "../thread/sync.h"
#include "../kernel/io.h"
#include "timer.h"
#include "../kernel/interrupt.h"
#include "../lib/string.h"

/*主IDE通道寄存器端口的基址是0x1F0,从通道基址是0x170*/
#define reg_data(channel) (channel->port_base + 0)           // 读写数据,每次传输两字节
#define reg_error(channel) (channel->port_base + 1)          // 读取硬盘错误信息
#define reg_sect_cnt(channel) (channel->port_base + 2)       // 扇区计数
#define reg_lba_l(channel) (channel->port_base + 3)          // 起始扇区LBA低八位
#define reg_lba_m(channel) (channel->port_base + 4)          // 中八位
#define reg_lba_h(channel) (channel->port_base + 5)          // 高八位
#define reg_dev(channel) (channel->port_base + 6)            // 驱动器/磁头寄存器
#define reg_status(channel) (channel->port_base + 7)         // 状态寄存器
#define reg_cmd(channel) (reg_status(channel))               // 命令寄存器,和状态寄存器共用0x1F7端口
#define reg_alt_status(channel) (channel->port_base + 0x206) // 备用状态寄存器
#define reg_ctl(channel) (reg_alt_status(channel))           // 设备控制寄存器,和前者共用0x3F6端口

/*记录reg_alt_status备用状态寄存器的一些关键位*/
#define BIT_ALT_STAT_BSY 0x80  // 1000 0000,表示硬盘忙
#define BIT_ALT_STAT_DRDY 0x40 // 0100 0000,表示驱动器准备好了
#define BIT_ALT_STAT_DRQ 0x8   // 0000 1000,表示数据传输准备好了

/*记录device驱动器/磁头寄存器的一些关键位*/
#define BIT_DEV_MBS 0xa0 // 1010 0000,设置必须的保留位
#define BIT_DEV_LBA 0x40 // 0100 0000,启用LBA模式
#define BIT_DEV_DEV 0x10 // 0001 0000,如果使用此位,则选择了从盘

/*一些硬盘操作指令的指令码*/
#define CMD_IDENTIFY 0xec     // identify识别硬盘指令
#define CMD_READ_SECTOR 0x20  // 读扇区指令
#define CMD_WRITE_SECTOR 0x30 // 写扇区指令

// 定义可读写的最大扇区数,用于调试
#define max_lba ((80 * 1024 * 1024 / 512) - 1) // 只支持80MB硬盘

uint8_t channel_cnt;            // 通道数
struct ide_channel channels[2]; // 一个主板最多有两个通道

int32_t ext_lba_base = 0;   // 总扩展分区LBA基址
uint8_t p_no = 0, l_no = 0; // 主分区和逻辑分区的下标
struct list partition_list; // 分区队列

/*分区表项结构体*/
struct partition_table_entry
{
    uint8_t bootable;   // 是否可引导
    uint8_t start_head; // 起始磁头号
    uint8_t start_sec;  // 起始扇区号
    uint8_t start_chs;  // 起始柱面号
    uint8_t fs_type;    // 分区类型
    uint8_t end_head;   // 结束磁头号
    uint8_t end_sec;    // 结束扇区号
    uint8_t end_chs;    // 结束柱面号

    uint32_t start_lba;    // 本分区起始扇区LBA地址
    uint32_t sec_cnt;      // 本分区扇区数
} __attribute__((packed)); // 此关键字保证这个结构是16字节大小

/*引导扇区mbr/ebr结构体*/
struct boot_sector
{
    uint8_t other[446]; // 446字节的引导代码
    struct partition_table_entry partition_table[4];
    uint16_t signature; // 结束标志0x55 0xaa
} __attribute__((packed));

/*选择要读写的硬盘*/
static void select_disk(struct disk *hd)
{
    uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;
    if (hd->dev_no == 1) // 如果是从盘
    {
        reg_device |= BIT_DEV_DEV;
    }
    // 通过汇编写入通道寄存器
    outb(reg_dev(hd->my_channel), reg_device);
}

/*向硬盘控制器写入起始扇区地址和要读写的扇区数*/
static void select_sector(struct disk *hd, uint32_t lba, uint8_t sec_cnt)
{
    ASSERT(lba <= max_lba);
    struct ide_channel *channel = hd->my_channel;
    // 写入扇区数
    outb(reg_sect_cnt(channel), sec_cnt);
    // 写入0-23位LBA
    outb(reg_lba_l(channel), lba);
    outb(reg_lba_m(channel), lba >> 8);
    outb(reg_lba_h(channel), lba >> 16);
    // 还有24-27四位LBA要写在dev寄存器里
    outb(reg_dev(channel), BIT_DEV_MBS | BIT_DEV_LBA | (hd->dev_no == 1 ? BIT_DEV_DEV : 0) | lba >> 24);
}

/*向通道channel发命令cmd*/
static void cmd_out(struct ide_channel *channel, uint8_t cmd)
{
    // 因为我们已经向通道发出了命令,所以将此位置为true,供硬盘中断处理程序使用
    channel->expecting_intr = true;
    outb(reg_cmd(channel), cmd);
}

/*从硬盘读出sec_cnt个扇区的数据到内存buf*/
static void read_from_sector(struct disk *hd, void *buf, uint8_t sec_cnt)
{
    // 先计算待读取字节数
    uint32_t byte = 0;
    if (sec_cnt == 0) // sec_cnt范围是0-255,如果传入256会变为0
    {
        byte = 256 * 512;
    }
    else
    {
        byte = sec_cnt * 512;
    }
    // 按字读取,1字=2字节
    insw(reg_data(hd->my_channel), buf, byte / 2);
}

/*从内存buf向硬盘写入sec_cnt个扇区的数据*/
static void write_to_sector(struct disk *hd, void *buf, uint8_t sec_cnt)
{
    uint32_t byte = 0;
    if (sec_cnt == 0) // sec_cnt范围是0-255,如果传入256会变为0
    {
        byte = 256 * 512;
    }
    else
    {
        byte = sec_cnt * 512;
    }
    outsw(reg_data(hd->my_channel), buf, byte / 2);
}

/*等待30秒,本质是优化了的自选锁*/
static bool busy_wait(struct disk *hd)
{
    struct ide_channel *channel = hd->my_channel;
    uint16_t time_limit = 30 * 1000;
    while (time_limit -= 10 >= 0)
    {
        // 如果硬盘不忙
        if (!(inb(reg_status(channel)) & BIT_ALT_STAT_BSY))
        {
            // 已经准备好数据传输
            return (inb(reg_status(channel)) & BIT_ALT_STAT_DRQ);
        }
        else
        {
            mtime_sleep(10); // 睡眠10ms,10ms后再次判断
        }
    }
    return false;
}

/*从硬盘sec_cnt个扇区读取数据到内存buf的全过程*/
void ide_read(struct disk *hd, uint32_t lba, void *buf, uint8_t sec_cnt)
{
    ASSERT(lba <= max_lba);
    ASSERT(sec_cnt > 0);
    lock_acquire(&hd->my_channel->lock); // 加锁表示通道已被占用

    // 1.选择要操作的硬盘
    select_disk(hd);

    uint32_t secs_op = 0;   // 本次要处理的扇区数,最大为256
    uint32_t secs_done = 0; // 已经处理的扇区数
    while (secs_done < sec_cnt)
    {
        if (sec_cnt - secs_done >= 256)
        {
            secs_op = 256;
        }
        else
        {
            secs_op = sec_cnt - secs_done;
        }
        // 2.写入待读取的扇区数和起始扇区号
        select_sector(hd, lba + secs_done, secs_op);
        // 3.写入读取命令到cmd寄存器
        cmd_out(hd->my_channel, CMD_READ_SECTOR);
        /*硬盘IO最慢的环节是硬盘内部处理环节,机械硬盘涉及到磁头移动等物理过程
         *此时,硬盘收到信号,开始在内部进行数据处理,我们可以让读取线程先阻塞自己
         *在读取完成后,通过中断处理程序唤醒线程,实现cpu的高效利用*/
        sema_down(&hd->my_channel->disk_done);

        /*此时,硬盘内部处理完成,程序被唤醒,继续接下来的环节*/
        // 4.检查硬盘状态是否可读
        if (!busy_wait(hd)) // 30秒内硬盘一直处于忙状态
        {
            char error[64];
            sprintf(error, "%s read sector %d failed!!!!!!\n", hd->name, lba);
            PANIC(error);
        }
        // 5.把读取出数据放到内存buf中
        read_from_sector(hd, (void *)((uint32_t)buf + secs_done * 512), secs_op);

        secs_done += secs_op;
    }
    lock_release(&hd->my_channel->lock);
}

/*从内存buf写入sec_cnt个扇区数据到硬盘的全过程*/
void ide_write(struct disk *hd, uint32_t lba, void *buf, uint8_t sec_cnt)
{
    // 写入和读取过程基本一致
    ASSERT(lba <= max_lba);
    ASSERT(sec_cnt > 0);
    lock_acquire(&hd->my_channel->lock);

    select_disk(hd); // 选取硬盘

    uint32_t secs_op = 0;
    uint32_t secs_done = 0;
    while (secs_done < sec_cnt)
    {
        if (sec_cnt - secs_done >= 256)
        {
            secs_op = 256;
        }
        else
        {
            secs_op = sec_cnt - secs_done;
        }
        select_sector(hd, lba + secs_done, secs_op); // 写入起始扇区号和扇区数
        cmd_out(hd->my_channel, CMD_WRITE_SECTOR);   // 写入写命令
        if (!busy_wait(hd))                          // 检验状态
        {
            char error[64];
            sprintf(error, "%s write sector %d failed!!!!!!\n", hd->name, lba);
            PANIC(error);
        }
        write_to_sector(hd, (void *)((uint32_t)buf + secs_done * 256), secs_op);
        sema_down(&hd->my_channel->disk_done); // 阻塞
        secs_done += secs_op;
    }
    lock_release(&hd->my_channel->lock);
}

/*硬盘中断处理程序*/
void intr_hd_handler(uint8_t irq_no)
{
    ASSERT(irq_no == 0x2e || irq_no == 0x2f);
    // 0x2e对应14引脚,是主通道,0x2f对应从通道
    uint32_t no = irq_no - 0x2e;
    struct ide_channel *channel = &channels[no];
    ASSERT(channel->irq_no == irq_no);
    if (channel->expecting_intr == true)
    {
        channel->expecting_intr = false;
        sema_up(&channel->disk_done);
        // 修改状态,让硬盘可以执行新的读写
        inb(reg_status(channel));
    }
}

/*将dst中len个相邻字节交换位置后存入buf*/
static void swap_pairs_bytes(const char *dst, char *buf, uint32_t len)
{
    uint8_t idx;
    for (idx = 0; idx < len; idx += 2)
    {
        buf[idx + 1] = *dst;
        dst++;
        buf[idx] = *dst;
        dst++;
    }
    buf[idx] = '\0';
}

/*获取硬盘参数信息*/
static void identify_disk(struct disk *hd)
{
    char id_info[512];
    select_disk(hd);
    cmd_out(hd->my_channel, CMD_IDENTIFY);
    sema_down(&hd->my_channel->disk_done);

    if (!busy_wait(hd))
    {
        char error[64];
        sprintf(error, "%s identify failed!!!!!!\n", hd->name);
        PANIC(error);
    }
    read_from_sector(hd, id_info, 1);

    char buf[64];
    // 硬盘序列号起始偏移量10个字,是长度20字节的的字符串
    uint8_t sn_start = 10 * 2, sn_len = 20;
    // 硬盘型号起始偏移量27个字,长度40字节
    uint8_t md_start = 27 * 2, md_len = 40;
    swap_pairs_bytes(&id_info[sn_start], buf, sn_len);
    printk("  disk %s info:\n", hd->name);
    printk("    SN: %s\n", buf); // 打印序列号
    memset(buf, 0, sizeof(buf));
    swap_pairs_bytes(&id_info[md_start], buf, md_len);
    printk("    MODULE: %s\n", buf); // 打印硬盘型号
    // 找到可供用户使用的扇区数这个参数
    uint32_t sector = *(uint32_t *)&id_info[60 * 2];
    // 打印可以使用的扇区数
    printk("    SECTORS: %d\n", sector);
    // 打印可以使用的内存容量
    printk("    CAPACITY: %dMB\n", sector * 512 / 1024 / 1024);
}

/*扫描硬盘hd中地址为ext_lba的扇区中的所有分区*/
static void partition_scan(struct disk *hd, uint32_t ext_lba)
{
    struct boot_sector *bs = sys_malloc(sizeof(struct boot_sector));
    ide_read(hd, ext_lba, bs, 1);
    uint8_t part_idx = 0;
    struct partition_table_entry *p = bs->partition_table;

    while (part_idx++ < 4)
    {
        if (p->fs_type == 0x5) // 若为拓展分区
        {
            if (ext_lba == 0) // 第一次读取引导块
            {
                ext_lba_base = p->start_lba;
                partition_scan(hd, p->start_lba); // 递归调用
            }
            else
            {
                partition_scan(hd, p->start_lba + ext_lba_base);
            }
        }
        else if (p->fs_type != 0) // 其他有效分区类型
        {
            if (ext_lba == 0) // 说明没有扩展分区,全是主分区
            {
                hd->prim_parts[p_no].start_lba = ext_lba + p->start_lba;
                hd->prim_parts[p_no].sec_cnt = p->sec_cnt;
                hd->prim_parts[p_no].my_disk = hd;
                list_append(&partition_list, &hd->prim_parts[p_no].part_tag);
                sprintf(hd->prim_parts[p_no].name, "%s%d", hd->name, p_no + 1);
                p_no++;
                ASSERT(p_no < 4);
            }
            else
            {
                hd->prim_parts[l_no].start_lba = ext_lba + p->start_lba;
                hd->prim_parts[l_no].sec_cnt = p->sec_cnt;
                hd->prim_parts[l_no].my_disk = hd;
                list_append(&partition_list, &hd->prim_parts[l_no].part_tag);
                sprintf(hd->prim_parts[l_no].name, "%s%d", hd->name, l_no + 5);
                l_no++;
                if (l_no >= 8)
                {
                    return;
                }
            }
        }
        p++;
    }
    sys_free(bs);
}

/*打印分区信息*/
static bool partition_info(struct list_elem *pelem, int arg)
{
    struct partition *part = elem2entry(struct partition, part_tag, pelem);
    printk("    %s start_lba:0x%x, sec_cnt:0x%x\n", part->name, part->start_lba, part->sec_cnt);
    return false;
}

/*硬盘数据结构初始化*/
void ide_init()
{
    printk("ide_init start\n");
    // BIOS扫描获取的硬盘数保存在物理地址0x475上,低1MB虚拟地址等于物理地址
    uint8_t hd_cnt = *((uint8_t *)(0x475));
    ASSERT(hd_cnt > 0);

    channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 计算通道数,一个通道对应两个硬盘
    struct ide_channel *channel;
    uint8_t channel_no = 0, dev_no = 0;
    list_init(&partition_list);

    // 处理每个通道上的硬盘
    while (channel_no < channel_cnt)
    {
        channel = &channels[channel_no];
        sprintf(channel->name, "ide%d", channel_no); // 设置通道名
        if (channel_no == 0)                         // 主通道
        {
            channel->port_base = 0x1f0;
            channel->irq_no = 0x20 + 14; // 对应从片IRQ14引脚
        }
        else if (channel_no == 1) // 从通道
        {
            channel->port_base = 0x170;
            channel->irq_no = 0x20 + 15;
        }
        // 默认不需要等待硬盘中断
        channel->expecting_intr = false;
        lock_init(&channel->lock);
        sema_init(&channel->disk_done, 0);
        // 注册中断处理程序
        register_handler(channel->irq_no, intr_hd_handler);
        // 分别获取两个硬盘的参数
        while (dev_no < 2)
        {
            struct disk *hd = &channel->devices[dev_no];
            hd->my_channel = channel;
            hd->dev_no = dev_no;
            sprintf(hd->name, "sd%c", 'a' + channel_no * 2 + dev_no);
            identify_disk(hd);
            if (dev_no != 0) // 只处理文件盘
            {
                partition_scan(hd, 0); // 扫描文件盘分区
            }
            p_no = 0, l_no = 0;
            dev_no++;
        }
        dev_no = 0;
        channel_no++;
    }
    printk("\n  all partition info\n");
    list_traversal(&partition_list, partition_info, (int)NULL);
    printk("ide_init done\n");
}

ide.h
#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H

#include "../lib/kernel/stdint.h"
#include "../lib/kernel/list.h"
#include "../thread/sync.h"

/*分区结构*/
struct partition
{
    uint32_t start_lba;         // 起始扇区
    uint32_t sec_cnt;           // 扇区数
    struct disk *my_disk;       // 分区所属的硬盘
    struct list_elem part_tag;  // 对列标记
    char name[8];               // 分区名称
    struct super_block *sb;     // 超级块
    struct bitmap block_bitmap; // 块的位图
    struct bitmap inode_bitmap; // i节点位图
    struct list open_inodes;    // i节点队列
};

/*硬盘结构*/
struct disk
{
    char name[8];                    // 硬盘名称
    struct ide_channel *my_channel;  // 硬盘使用的ide通道
    uint8_t dev_no;                  // 本硬盘是主盘还是从盘,主0从1
    struct partition prim_parts[4];  // 主分区,上限为4
    struct partition logic_parts[8]; // 逻辑分区,理论上无上限,我设置为只支持8个
};

/*ata通道结构*/
struct ide_channel
{
    char name[8];               // ata通道名称
    uint16_t port_base;         // 本通道起始端口号
    uint8_t irq_no;             // 本通道使用的中断号
    struct lock lock;           // 通道锁
    bool expecting_intr;        // 用来表示是否等待硬盘中断
    struct semaphore disk_done; // 用来阻塞、唤醒驱动程序
    struct disk devices[2];     // 一个通道上可以连接两个硬盘
};

void ide_read(struct disk *hd, uint32_t lba, void *buf, uint8_t sec_cnt);/*从硬盘sec_cnt个扇区读取数据到内存buf的全过程*/
void ide_write(struct disk *hd, uint32_t lba, void *buf, uint8_t sec_cnt);/*从内存buf写入sec_cnt个扇区数据到硬盘的全过程*/
void intr_hd_handler(uint8_t irq_no);/*硬盘中断处理程序*/
void ide_init(void);/*硬盘驱动程序初始化*/

#endif

测试

编写makefile
BUILD_DIR = ./build
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/
ASFLAGS = -f elf
CFLAGS =  -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS =  -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
      $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
      $(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
	  $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \
	  $(BUILD_DIR)/switch.o $(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o \
	  $(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o $(BUILD_DIR)/tss.o \
	  $(BUILD_DIR)/process.o $(BUILD_DIR)/syscall-init.o $(BUILD_DIR)/syscall.o \
	  $(BUILD_DIR)/stdio.o $(BUILD_DIR)/ide.o

################	c代码编译   ##################
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
        lib/kernel/stdint.h kernel/init.h kernel/debug.h \
		kernel/memory.h thread/thread.h kernel/interrupt.h \
		device/console.h userprog/process.h lib/user/syscall.h \
		userprog/syscall-init.h lib/stdio.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \
        lib/kernel/stdint.h kernel/interrupt.h device/timer.h \
		kernel/memory.h thread/thread.h device/console.h \
		device/keyboard.h userprog/tss.h userprog/syscall-init.h \
		device/ide.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \
        lib/kernel/stdint.h kernel/global.h kernel/io.h \
		lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/timer.o: device/timer.c device/timer.h lib/kernel/stdint.h \
        kernel/io.h lib/kernel/print.h kernel/interrupt.h \
		thread/thread.h kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \
        lib/kernel/print.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/string.o: lib/string.c lib/string.h \
		kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \
		lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h \
		lib/string.h thread/sync.h thread/thread.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/bitmap.o: lib/kernel/bitmap.c lib/kernel/bitmap.h \
		lib/string.h kernel/interrupt.h lib/kernel/print.h \
		kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \
		lib/kernel/stdint.h lib/kernel/list.h lib/string.h \
		kernel/memory.h kernel/interrupt.h kernel/debug.h \
		lib/kernel/print.h userprog/process.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \
		lib/kernel/stdint.h kernel/interrupt.h kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/sync.o: thread/sync.c thread/sync.h \
		lib/kernel/stdint.h thread/thread.h kernel/debug.h \
		kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/console.o: device/console.c device/console.h \
		lib/kernel/print.h thread/sync.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/keyboard.o: device/keyboard.c device/keyboard.h \
		lib/kernel/print.h kernel/interrupt.h kernel/io.h \
		lib/kernel/stdint.h device/ioqueue.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/ioqueue.o: device/ioqueue.c device/ioqueue.h \
		lib/kernel/stdint.h thread/thread.h thread/sync.h \
		kernel/interrupt.h kernel/debug.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/tss.o: userprog/tss.c userprog/tss.h \
		lib/kernel/stdint.h thread/thread.h kernel/global.h \
		lib/kernel/print.h lib/string.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/process.o: userprog/process.c userprog/process.h \
		kernel/global.h lib/kernel/stdint.h thread/thread.h \
		kernel/debug.h userprog/tss.h device/console.h \
		lib/string.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/syscall.o: lib/user/syscall.c lib/user/syscall.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/syscall-init.o: userprog/syscall-init.c userprog/syscall-init.h \
		lib/kernel/stdint.h lib/user/syscall.h thread/thread.h \
		lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/stdio.o: lib/stdio.c lib/stdio.h \
		lib/kernel/stdint.h lib/string.h kernel/debug.h \
		lib/user/syscall.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/ide.o: device/ide.c device/ide.o \
		lib/stdio.h kernel/debug.h kernel/global.h \
		thread/sync.h kernel/io.h device/timer.h \
		kernel/interrupt.h lib/string.h
	$(CC) $(CFLAGS) $< -o $@

##############    汇编代码编译    ###############
$(BUILD_DIR)/kernel.o: kernel/kernel.S
	$(AS) $(ASFLAGS) $< -o $@

$(BUILD_DIR)/print.o: lib/kernel/print.S
	$(AS) $(ASFLAGS) $< -o $@

$(BUILD_DIR)/switch.o: thread/switch.S
	$(AS) $(ASFLAGS) $< -o $@

##############    连接所有目标文件    #############
$(BUILD_DIR)/kernel.bin: $(OBJS)
	$(LD) $(LDFLAGS) $^ -o $@

.PHONY : mk_dir hd clean all

mk_dir:
	if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi

hd:
	dd if=$(BUILD_DIR)/kernel.bin \
           of=/home/hongbai/bochs/bin/os_hd_60M.img \
           bs=512 count=200 seek=10 conv=notrunc

clean:
	cd $(BUILD_DIR) && rm -f ./*

build: $(BUILD_DIR)/kernel.bin

all: mk_dir build hd

结果截图

在这里插入图片描述


结语

现在是2025年5月10日17点12分,我上一篇博客于 2025-05-06 09:00:34 发布,这段时间干了什么?

5月7号我有一门结课考试,从5月4号到5月7号主要在复习课程,中间断断续续写了一点硬盘驱动。5月8号下午有一个大实验,加上我们学校最近在教学评估,就没有推进进度。昨天完成了数据库实验报告,写了3道算法题,也没时间继续写操作系统了。

下周是我们学校教学评估关键周,不允许旷课,上课也不能继续做操作系统了。毕竟我也不想被抓个典型被记过。显然我们的操作系统开发会受到影响,提前说明一下吧。

今天完成了第13章的开发,晚上准备进入第14章。明天一整天写一整天14章,尽量赶赶进度吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值