手写操作系统(四十五)-硬盘驱动程序

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

1.硬盘初始化

硬盘上有两个ata通道,也称为IDE通道。

第1个ata通道上的两个硬盘(主和从)的中断信号挂在8259A从片的IRQ14上,是两个硬盘共享同一个IRQ接口,硬盘发生中断的条件是我们对硬件执行了某些命令,然后硬盘完成任务后才发中断,在对硬盘发命令时,需要提前指定是对主盘,还是从盘操作,这是在硬盘控制器的device寄存器中第4位的dev位指定的,因此就可以区分是哪个硬盘来了中断信号。

第2个ata通道接在8259A从片的IRQ15上,该ata通道上可支持两个硬盘。 来自8259A从片的中断是由8259A主片帮忙向处理器传达的,8259A从片是级联在8259A主片的IRQ2接口的,因此为了让处理器也响应来自8259A从片的中断,屏蔽中断寄存器必须也把IRQ2打开。

在/kernel/interrupt.c文件pic_init函数加入

// 主片上打开的中断有IRQ0的时钟, IRQ1的键盘和级联从片的IRQ2, 其它全部关闭
outb(PIC_M_DATA, 0xf8);

// 打开从片上的IRQ14, 此引脚接收硬盘控制器的中断
outb(PIC_S_DATA, 0xbf);

重新设置了中断屏蔽寄存器,目前是在主片8259A上打开的中断IRQ0的时钟、IRQ1的键盘和级联从片的IRQ2,从片8259A上打开的中断是IRQ14的硬盘。 在中断处理程序中,如果中断源是来自从片8259A的话,在发送中断结束信号EOI的时候,主片和从片都要发送。 否则,将无法继续响应新的中断。 不过我们的中断处理程序一直都是向主从两片 8259A 同时发送 EOI。

这里我们提供一个在内核中实现格式化输出函数 printk

/lib/kernel/stdio-kernel.c

#include "stdio-kernel.h"
#include "print.h"
#include "../stdio.h"
#include "../device/console.h"
#include "global.h"

#define va_start(args, first_fix) args = (va_list) &first_fix
#define va_end(args) args = NULL

/* 供内核使用的格式化输出函数 */
void printk(const char* format, ...) {
    va_list args;
    va_start(args, format);
    char buf[1024] = {0};
    vsprintf(buf, format, args);
    va_end(args);
    console_put_str(buf);
}

其实就是调用console_put_str(buf)而已

对应/lib/kernel/stdio-kernel.h

#ifndef __LIB_KERNEL_STDIOSYS_H
#define __LIB_KERNEL_STDIOSYS_H

#include "stdint.h"

void printk(const char* format, ...);

#endif

 

接下来是硬盘相关的数据结构了,它定义在 device/ide.h 中,

#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H

#include "stdint.h"
#include "sync.h"
#include "bitmap.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;     // inode 位图
    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_init(void);

extern uint8_t channel_cnt;

extern struct ide_channel channels[];

#endif

struct partition分区表结构,成员start_lba表示分区的起始扇区,sec_cnt是分区的容量扇区数,一个硬盘有很多分区,因此成员my_disk表示此扇区属于哪个硬盘。 part_tag是本分区的标记,将来会将分区汇总到队列中,需要用此标记。 name是分区名称,如sdal、 sda2。 “struct super_block*sb”是超级块指针,此处只是用来占个位置,当前节的代码中并没有定义超级块的结构,因此更不可能包含超级块的头文件,因为这里的超级块是struct super-block*指针,32位系统下指针都是32位,在有指针操作的时候才会涉及到数据宽度,目前并没有使用它,因此编译并没有问题。 后面的3个成员是文件系统中涉及的。 为了减少对低速磁盘的访问次数,文件系统通常以多个扇区为单位来读写磁盘,这多个扇区就是块。 block_bitmap是块位图,用来管理本分区所有块,为了简单,这里的块大小是由1个扇区组成的,所以block_bitmap就是管理扇区的位图。 inode_bitmap是i结点管理位图。 open_inodes是分区所打开的inode队列。 都是后面在文件系统使用的。

struct disk代表硬盘,name表示硬盘的名称,比如sda、 sdb等。 一个通道上有两块硬盘,所以my_channel用于指定本硬盘所属的通道,成员dev_no用来表示本硬盘是主盘,还是从盘,prim_parts是本硬盘中的主分区数量,最多是4个主分区,logic_parts是逻辑分区的数量,这里限制为8个。

struct ide_channel表示ide通道,也就是ata通道。 成员 name 是通道的名称,如 ata0 或 ide0。 port_base是本通道的端口基址,这里只处理两个通道的主板,每个通道的端口范围是不一样的,通道 1 (Primary通道)的命令块寄存器端口范围是0x1F0~0x1F7,控制块寄存器端口是0x3F6,通道2 (Secondary通道)命令块寄存器端口范围是0x170~0x177,控制块寄存器端口是0x376,通道1的端口可以以0x1F0为基数,其命令块寄存器端口在此基数上分别加上0~7就可以了,控制块寄存器端口在此基数上加上 0x206,同理,通道 2 的基数就是 0x170。成员irq_no是本通道的中断号,在硬盘的中断处理程序中要根据中断号来判断在哪个通道中操作,将来实现硬盘中断处理程序时就清楚了。成员lock是本通道的锁,用来实现通道的互斥。1个通道中有主、从两块硬盘,向硬盘下达命令的时候可以通过device寄存器中的dev位来指定操作哪块硬盘,这很好区分。 但硬盘完成操作后,它还得通知调用者任务执行的结果,是顺利完成了,还是失败了,如果是读硬盘的话,现在可以取数据了,这里是让硬盘主动发中断来通知调用者的。 可是一个通道只能有1个中断信号,因此通道中的两个硬盘也只能共用同一个中断,中断发生时,中断处理程序是不能区分中断信号来自哪一个硬盘,所以一次只允许通道中的1个硬盘操作,因此在通道中设置锁来实现互斥,对通道中任何一个硬盘操作时都要申请该锁以实现独享通道。 成员expecting_intr表示本通道正等待硬盘中断。 驱动程序向硬盘发完命令后等待来自硬盘的中断,中断处理程序中会通过此成员来判断此次的中断是否是因为之前的硬盘操作命令引起的,如果是,则进行下一步动作,如获取数据等。 成员disk_done是个信号量,它的作用是驱动程序向硬盘发送命令后,在等待硬盘工作期间可以通过此信号量阻塞自己,避免干等着浪费CPU。 等硬盘工作完成后会发出中断,中断处理程序通过此信号量将硬盘驱动程序唤醒。 成员devices是个长度为2的数组,表示一个通道中的两个硬盘。

 

真正逻辑在/device/ide.c

#include "ide.h"
#include "sync.h"
#include "stdio.h"
#include "stdio-kernel.h"
#include "interrupt.h"
#include "memory.h"
#include "debug.h"
#include "string.h"

/* 定义硬盘各寄存器的端口号 */
#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)
#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))
#define reg_alt_status(channel)  (channel->port_base + 0x206)
#define reg_ctl(channel)     reg_alt_status(channel)

/* reg_alt_status寄存器的一些关键位 */
#define BIT_STAT_BSY     0x80        // 硬盘忙
#define BIT_STAT_DRDY    0x40        // 驱动器准备好     
#define BIT_STAT_DRQ     0x8         // 数据传输准备好了

/* device寄存器的一些关键位 */
#define BIT_DEV_MBS 0xa0             // 第7位和第5位固定为 1
#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];     // 有两个 ide 通道


/* 硬盘数据结构初始化 */
void ide_init() {
    printk("ide_init start\n");
    uint8_t hd_cnt = *((uint8_t*) (0x475));     // 获取硬盘的数量
    ASSERT(hd_cnt > 0);
    // 一个 ide 通道上有两个硬盘, 根据硬盘数量反推有几个 ide 通道
    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);

        // 为每个 ide 通道初始化端口基址及中断向量
        switch(channel_no) {
            case 0:
                channel->port_base = 0x1f0;     // ide0 通道的起始端口号是 0x1f0
                channel->irq_no = 0x20 + 14;    // ide0 通道的中断向量号
                break;

            case 1:
                channel->port_base = 0x170;     // ide1 通道的起始端口号是 0x170
                channel->irq_no = 0x20 + 15;    // ide1 通道的中断向量号
                break;
        }

        channel->expecting_intr = false;        // 未向硬盘写入指令时不期待硬盘的中断
        lock_init(&channel->lock);

        // 初始化为 0, 目的是向硬盘控制器请求数据后, 硬盘驱动 sema_down 此信号量会阻塞线程,
        // 直到硬盘完成后通过发中断, 由中断处理程序将此信号量 sema_up, 唤醒线程
        sema_init(&channel->disk_done, 0);

        channel_no++;                           // 下一个 channel
    }

    // 打印所有分区信息
    printk("ide_init done\n");
}

上面的定义了一些宏来辅助

ide_init硬盘初始化函数,此函数目前所做的工作是初始化以上定义的数据结构,将来还要注册硬盘中断处理程序、检测硬盘参数和扫描分区表。

uint8_t hd_cnt = *((uint8_t*) (0x475))在地址 0x475 处获取硬盘的数量,将其存入变量 hd_cnt 中。低端1MB 以内的虚拟地址和物理地址相同,所以虚拟地址 0x475 可以正确访问到物理地址 0x475。代码”DIV_ROUND_UP(hd_ent, 2);”简单地推算出了通道数,存入变量channel_ent中。

接着通过while循环处理每一个通道,在switch结构中针对这两个通道依次初始化端口基址port_base和中断号irq_no。 这里通道1端口基址是0x1f0,中断号是0x2e,即0x20+14。通道 2的端口基址是 0x170,中断号是0x2f,即0x20+15。下面初始化通道expecting_intr 为false,再初始化通道的锁 channel->lock 和信号量channel-> disk_done。

 

2.thread_yield和idle线程

thread_yield定义在thread.c中,它的功能是主动把CPU使用权让出来,它与thread_block的区别是 thread_yield执行后任务的状态是TASK_READY,即让出CPU后,它会被加入到就绪队列中,下次还能继续被调度器调度执行,而thread_block执行后任务的状态是TASK_BLOCKED,需要被唤醒后才能加入到就绪队列。

 

接下来修改/thread/thread.c

struct task_struct* idle_thread;        // idle 线程
// 系统空闲时运行的线程
static void idle(void* arg UNUSED) {
    while (1) {
        thread_block(TASK_BLOCKED);
        // 执行 hlt 时必须要保证目前处在开中断的情况下
        asm volatile ("sti; hlt" : : : "memory");
    }
}

/*实现任务调度*/
void schedule(){
    ASSERT(intr_get_status() == INTR_OFF);
    struct task_struct* cur = running_thread();
    if(cur->status == TASK_RUNNING){
        //若此线程只是cpu时间片到了,将其加入就绪队列队尾
        ASSERT(!elem_find(&thread_ready_list,&cur->general_tag));
        list_append(&thread_ready_list,&cur->general_tag);
        cur->ticks = cur->priority;
        cur->status = TASK_READY;
    }else{
        //若此线程需要某事件发生后才能继续上cpu运行,不需要将其放入队列中,因为当前线程不在就绪队列中
    }

    // 如果就绪队列中没有可运行的任务, 就唤醒 idle
    if (list_empty(&thread_ready_list)) {
        thread_unblock(idle_thread);
    }

    ASSERT(!list_empty(&thread_ready_list));
    thread_tag = NULL;
    thread_tag = list_pop(&thread_ready_list);
    struct task_struct* next = elem2entry(struct task_struct,general_tag,thread_tag);   //将thread_tag转化为线程(链表)
    next->status = TASK_RUNNING;
    /*激活任务页表等*/
    process_activate(next);
    switch_to(cur,next);
}

// 主动让出 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));
    list_append(&thread_ready_list, &cur->general_tag);
    // // 将当前线程状态设置为 TASK_READY, 之后进行 CPU 重新调度
    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);
    // 将当前 main 函数创建为线程
    make_main_thread();
    idle_thread = thread_start("idle", 10, idle, NULL);
    put_str("thread_init done\n");
}

添加idle函数和thread_yield函数,修改schedule函数和thread_init函数

thread_yield函数核心就是3步:

  • 先将当前任务重新加入到就绪队列(队尾)。
  • 然后将当前任务的 status 置为 TASK_READY。
  • 最后调用 schedule重新调度新任务。

前2步必须是原子操作,如果在开中断的情况下,刚完成第一步把当前任务 添加到就绪队列,第二步修改状态的代码“cur->status = TASK_READY”尚未执行,因此当前任务的 状态依然是TASK_RUNNING。如果此时发生时钟中断,当前任务正巧被换下CPU,调度器schedule判断当前任务的状态是TASK_RUNNING,就要将其重新添加到就绪队列,为避免重复添加,这时会触发 ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));。

当就绪队列中没有任务时,调度器没有任务可调度,系统就会通过“ASSERT(llist_empty(&thread_ready_list));”悬停。这种情况不能再持续下去了,所以本次在thread.c中顺便塞了个idle线程, idle线程用于系统空闲时,也就是就绪队列中没有任务时才运行的。

idle函数的原理是在函数体中执行”thread_block(TASK_BLOCKED)”阻塞自己,在其被唤醒后,通过内联汇编执行hlt指令,使系统挂起,达到真正的空闲。hlt指令的功能让处理器停止执行指令,也就是将处理器挂起(并不是类似”jmp$”那样空兜CPU,CPU利用率100%),使处理器得到休息,CPU利用率一下子就掉下来了,在那一小段时间 CPU 利用率为 0。处理器已经停止运行,因此并不会再产生内部异常,唯一能唤醒处理器的就是外部中断,当外部发生后,处理器恢复执行后面的指令。处理器需要被唤醒,必须要保证在开中断的情况下执行hlt,因此内联汇编代码中,先执行sti,再执行hlt。

schedule会将idle_thread解除阻塞,idle_thread在第一次创建时会被加入到就绪队列,因此会执行一次,然后阻塞。当就绪队列为空时, schedule会将idle_thread解除阻塞,也就是thread_unblock(idle_thread)唤醒idle_thread,idle_thread 会执行“sti;hlt”,先开中断,再挂起 CPU。

在thread_init函数中完成idle_thread的创建工作。

对应/thread/thread.h加入:

/* 主动让出 cpu, 换其它线程运行 */
void thread_yield(void);

 

 

3.实现简单的休眠函数

硬盘和CPU是相互独立的个体,但由于硬盘是低速设备,其在处理请求时往往消耗很长的时间,为避免浪费CPU资源,在等待硬盘操作的过程中最好把CPU主动让出来,让CPU去执行其他任务,所以我们在timer.c中定义休眠函数。

修改/device/timer.c

加入以下代码

#define IRQ0_FREQUENCY     100

#define mil_seconds_per_intr (1000 / IRQ0_FREQUENCY)    // 每多少毫秒发生一次中断

// 以 tick 为单位的 sleep, 任何时间形式的 sleep 会转换此 ticks 形式
static void ticks_to_sleep(uint32_t sleep_ticks) {
    uint32_t start_tick = ticks;
    // 若间隔的 ticks 数不够便让出 cpu
    while (ticks - start_tick < sleep_ticks) {
        thread_yield();
    }
}

// 以毫秒为单位的 sleep
void mtime_sleep(uint32_t m_seconds) {
    // 计算要休眠的 ticks数 
    uint32_t sleep_ticks = DIV_ROUND_UP(m_seconds, mil_seconds_per_intr);
    ASSERT(sleep_ticks > 0);
    ticks_to_sleep(sleep_ticks);
}

定义的宏mil_seconds_per_intr,其意义是每多少毫秒发生一次中断,也就是以毫秒计算的中断周期。我们已经把时钟中断频率设置成了每秒100次,因此mil_seconds_per_intr的值是1000/100=10毫秒,1个中断周期是10毫秒,我们用它实现简单的延时功能。

ticks_to_sleep函数接受一个参数sleep_ticks,sleep_ticks是中断发生的次数ticks,即嘀哒数,功能是让任务休眠sleep_ticks个ticks,也就是此函数按照时钟嘀数来休眠。原理是利用两次时钟中断发生的间隔ticks实现的,也就是两次采样ticks之差。函数中使用的变量ticks是全局变量,它是由时钟中断处理函数intr_timer_handler更新的,每次时钟中断发生它的值就加1,这里取两次采样间隔,通过while循环不断获取当前的ticks,只要当前的ticks值减去第一次调用时的ticks(这里是start tick),所得的差小于sleep ticks (休眠的ticks数),就调用thread yield让出CPU,直到不满足此条件为止(发生了足够多次数的时钟中断),从而达到了延时的目的。

mtime_sleep函数接受一个参数毫秒m_seconds,其功能是使程序休眠m_seconds毫秒,此函数按照毫秒来休眠。然而mtime_sleep只是个外壳,我们并没有真正做到按时间来休眠,按时间休眠的原理是: 将休眠的毫秒时间m_seconds转换为时钟中断发生的间隔ticks数,然后调用ticks_to_sleep,也就是说 最终还是由ticks_to_sleep完成休眠。代码uint32_t sleep_ticks = DIV_ROUND_UP(m_seconds, mil_seconds_per_intr)便是将时间转换后的ticks数,本质上就是用休眠的毫秒数除以中断发生的毫秒周期。最后调用”ticks_to_sleep(sleep_ticks)”实现休眠。

 

4.完善硬盘驱动程序

接下来是硬盘的中断处理函数部分。

/device/ide.c

/* 选择读写的硬盘 */
static void select_disk(struct disk* hd) {
    uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;
    if(hd->dev_no == 1) {
        // 若是从盘就置 DEV 位为 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);   // 如果 sec_cnt为 0, 则表示写入256个扇区
    // 写入扇区号
    // lba地址的低 8位, 不用单独取出低8位. outb函数中的汇编指令 outb %b0, %w1会只用 al
    outb(reg_lba_l(channel), lba);
    // lba地址的 8~15 位
    outb(reg_lba_m(channel), lba >> 8);
    // lba地址的 16~23 位
    outb(reg_lba_h(channel), lba >> 16);

    // 因为 lba 地址的 24~27 位要存储在 device 寄存器的 0~3 位,
    // 无法单独写入这 4 位, 所以在此处把 device 寄存器再重新写入一次
    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 size_in_byte;
    if(sec_cnt == 0) {
        // 因为 sec_cnt 是8位变量, 由主调函数将其赋值时, 若为 256 则会将最高位的 1 丢掉变为 0
        size_in_byte = 256 * 512;

    } else {
        size_in_byte = sec_cnt * 512;
    }
    insw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}


/* 将 buf 中 sec_cnt 扇区的数据写入硬盘 */
static void write2sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
    uint32_t size_in_byte;
    if(sec_cnt == 0) {
        // 因为 sec_cnt 是8位变量, 由主调函数将其赋值时, 若为 256 则会将最高位的 1 丢掉变为 0
        size_in_byte = 256 * 512;

    } else {
        size_in_byte = sec_cnt * 512;
    }
    outsw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}


/* 等待 30 秒 */
static bool busy_wait(struct disk* hd) {
    struct ide_channel* channel = hd->my_channel;
    // 可以等待30000毫秒
    uint16_t time_limit = 30 * 1000;
    while((time_limit -= 10) >= 0) {
        if(!(inb(reg_status(channel)) & BIT_STAT_BSY)) {
            // 如果硬盘数据准备好了
            return (inb(reg_status(channel)) & BIT_STAT_DRQ);

        } else {
            // 睡眠 10 毫秒
            mtime_sleep(10);
        }
    }
    // 超时硬盘数据未准备好
    return false;
}

select_disk函数接受一个参数,硬盘指针hd,功能是选择待操作的硬盘是主盘或从盘。原理是利用device寄存器中的dev位,该位为0表示是通道中的主盘,为1表示是通道的从盘。 先用宏BIT_DEV_MBS|BIT_DEV_LBA 拼凑出device的值存入变量reg_device,再根据hd->dev_no的值判断是主盘,还是从盘,若为1则表示是从盘, 于是再将变量reg_device的值加上BIT_DEV_DEV。最后通过outb函数将变量reg_device写入硬盘所在通道的 device寄存器,即reg_dev(hd->my_channel),这样就完成了主盘或从盘的选择。

select_sector函数接受3个参数,硬盘指针hd、扇区起始地址lba、扇区数sec_cnt,功能是向硬盘控制器写入起始扇区地址及要读写的扇区数。 功能分两步实现,第1步outb(reg_sect_cnt(channel), sec_cnt)先在Sector count寄存器中写入待读写的扇区数, Sector count寄存器是8位宽度,范围是0-255,因此当写入该寄存器的值为0时,表示256个扇区。 第2步是分别在寄存器LBA low、LBA mid、LBA high中写入扇区LBA地址的低8位、中间8位和高8位,LBA地址共28位,第24~27位写在device寄存器的低4位中,因此outb(reg_dev(channel), BIT_DEV_MBS | BIT_DEV_LBA | (hd->dev_no == 1 ? BIT_DEV_DEV : 0) | lba >> 24),重新把device寄存器写了一次,保留原来信息的同时,又补充了LBA的第24~27位。

cmd_out函数接受2个参数,通道channel和硬盘操作命令cmd,函数功能是将通道channel发cmd命令。 发命令的时候要将通道的expecting_intr置为true,这是为硬盘中断处理程序埋下伏笔,表示将来该通道发出的中断信号也许是由此次命令操作引起的,因此此通道正期待来自硬盘的中断。然后再将命令cmd写入通道的cmd寄存器。

read_from_sector函数接受3个参数,分别是待操作的硬盘hd、缓冲区buf、读取的扇区数sec_cnt,功能是从硬盘hd中读入sec_cnt个扇区的数据到buf,此函数内部是调用insw来完成硬盘读取的, insw的参数是字,也就是2个字节,因此要将扇区数转换成字后再调用insw。先转换成字节,再将字节除以2就是字了,这里将sec_cnt转换为字节时要对sec_cnt判断一下,如果sec_cnt为0的话,这表示256个扇区,并不是0扇区,因此size_in_byte = 256 * 512,否则的话, size_in_byte的值就等于sec_cnt * 512。size_in_byte变为合适的字节后,将size_in_byte/2转换为字作为参数调用 insw 完成读取扇区。

write2sector函数接受3个参数,硬盘hd、缓冲区buf、扇区数sec_ent,功能是将buf中 sec_cnt扇区的数据写入硬盘hd。 这里写扇区是调用outsw完成的,outsw的参数也是字,因此也需要将扇 区数sec_cnt转换为字,原理同read_from_sector相同,不再赘述。

busy_wait函数接受1个参数,硬盘hd,功能是等待硬盘30秒。 硬盘是个低速设备,因此在其响应过程中,驱动程序可以让出CPU使用权使其他任务得到调度,这就是busy_wait的作用。 我们在30 秒内等待硬盘响应,若成功则返回true,否则false。 变量time_limit的值是30*1000毫秒,即30秒,接着通过while循环,每次将time_limit减10 毫秒,在inb(reg_status(channel)读取status寄存器,通过宏BIT_STAT_BSY判断status寄存器的BSY位是否为1,如果为1,则表示硬盘繁忙,这时候就调用mtime_sleep(10)去休眠10毫秒。 如果BSY位为0则表示硬盘不忙,接着再次读取status寄存器,返回其DRQ位的值,DRQ位为1表示硬盘已经准备好数据了,可以取出。 其实 busy_wait的意思是忙等待,一般忙等待都是指CPU空转(空兜),自旋锁就是忙等待的一种形式。这里虽然用busy_wait来命名此功能,但本质上我们更高效一点,毕竟是主动让出了CPU使用权,这是由mtime_sleep函数中间接调用了thread_yield函数实现的。

 

继续更改/device/ide.c

/* 从硬盘读取 sec_cnt 个扇区到 buf */
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_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;           // 每次操作的扇区数
    uint32_t secs_done = 0;     // 已完成的扇区数

    while(secs_done < sec_cnt) {
        if((secs_done + 256) <= sec_cnt) {
            secs_op = 256;

        } else {
            secs_op = sec_cnt - secs_done;
        }

        // 2. 写入待读入的扇区数和起始扇区号
        select_sector(hd, lba + secs_done, secs_op);

        // 3. 执行的命令写入 reg_cmd 寄存器
        cmd_out(hd->my_channel, CMD_READ_SECTOR);   // 准备开始读数据
        // 将自己阻塞, 等待硬盘完成读操作后通过中断处理程序唤醒自己
        sema_down(&hd->my_channel->disk_done);

        // 4. 检测硬盘状态是否可读, 醒来后开始执行下面代码
        if(!busy_wait(hd)) {
            // 若失败
            char error[64];
            sprintf(error, "%s read sector %d failed!!!!!!\n", hd->name, lba);
            PANIC(error);
        }

        // 5. 把数据从硬盘的缓冲区中读出
        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, uint32_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;           // 每次操作的扇区数
    uint32_t secs_done = 0;     // 已完成的扇区数

    while(secs_done < sec_cnt) {
        if((secs_done + 256) <= sec_cnt) {
            secs_op = 256;

        } else {
            secs_op = sec_cnt - secs_done;
        }

        // 2. 写入待写入的扇区数和起始扇区号
        select_sector(hd, lba + secs_done, secs_op);

        // 3. 执行的命令写入 reg_cmd 寄存器
        cmd_out(hd->my_channel, CMD_WRITE_SECTOR);

        // 4. 检测硬盘状态是否可读
        if(!busy_wait(hd)) {
            // 若失败
            char error[64];
            sprintf(error, "%s write sector %d failed!!!!!!\n", hd->name, lba);
            PANIC(error);
        }

        // 5. 将数据写入硬盘
        write2sector(hd, (void*) ((uint32_t) buf + secs_done * 512), 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);
    uint8_t ch_no = irq_no - 0x2e;      // 获取通道号
    struct ide_channel* channel = &channels[ch_no];
    ASSERT(channel->irq_no == irq_no);

    // 不必担心此中断是否对应的是这一次的 expecting_intr
    // 每次读写硬盘时会申请锁, 从而保证了同步一致性
    if(channel->expecting_intr) {
        channel->expecting_intr = false;
        // 唤醒阻塞的任务
        sema_up(&channel->disk_done);

        // 读取状态寄存器使硬盘控制器认为此次的中断已被处理, 从而硬盘可以继续执行新的读写
        inb(reg_status(channel));
    }
}


/* 硬盘数据结构初始化 */
void ide_init() {
    printk("ide_init start\n");
    uint8_t hd_cnt = *((uint8_t*) (0x475));     // 获取硬盘的数量
    ASSERT(hd_cnt > 0);
    // 一个 ide 通道上有两个硬盘, 根据硬盘数量反推有几个 ide 通道
    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);

        // 为每个 ide 通道初始化端口基址及中断向量
        switch(channel_no) {
            case 0:
                channel->port_base = 0x1f0;     // ide0 通道的起始端口号是 0x1f0
                channel->irq_no = 0x20 + 14;    // ide0 通道的中断向量号
                break;

            case 1:
                channel->port_base = 0x170;     // ide1 通道的起始端口号是 0x170
                channel->irq_no = 0x20 + 15;    // ide1 通道的中断向量号
                break;
        }

        channel->expecting_intr = false;        // 未向硬盘写入指令时不期待硬盘的中断
        lock_init(&channel->lock);

        // 初始化为 0, 目的是向硬盘控制器请求数据后, 硬盘驱动 sema_down 此信号量会阻塞线程,
        // 直到硬盘完成后通过发中断, 由中断处理程序将此信号量 sema_up, 唤醒线程
        sema_init(&channel->disk_done, 0);
        register_handler(channel->irq_no, intr_hd_handler);
        channel_no++;                           // 下一个 channel
    }

    // 打印所有分区信息
    printk("ide_init done\n");
}

函数ide_read接受4个参数,硬盘hd、扇区地址lba、缓冲区buf、扇区数量sec_cnt,功能是从硬盘hd的扇区地址lba处读取sec_cnt个扇区到buf。 操作硬盘之前先将硬盘所在的通道上锁,从而保证一次只操作同一通道上的一块硬盘。 通过 select_disk(hd)选择待操作的硬盘。 因为读写扇区数端口0x1f2及0x172是8位寄存器,故每次读写最多是255个扇区(当写入端口值为0时,则写入256个扇区),所以当读写的端口数超过256时,必须拆分成多次读写操作。 每当完成一个扇区的读写后,此寄存器的值便减1,所以当读写失败时,此端口包括尚未完成的扇区数。

由于硬盘一次只能操作256个扇区,为了相对高效一些,如果待操作的扇区数 sec_cnt大于 256,尽量一次操作 256 个扇区,余下不足 256 扇区的部分一次性完成。secs_op是指每次硬盘操作的扇区数,secs_done是操作完成的扇区数。下面通过while循环,将sec_cnt个扇区按照256个分组来操作。每次操作secs_op个扇区,secs_op的值不是256,,就是小于256的余数,secs_op等于256的情况在0次以上,而小于256的情况顶多出现1次。确定操作的扇区数secs_op后,通过select_sector函数选择待操作的扇区地址及个数,之后在通过cmd_out函数向硬盘发读扇区命令。此时硬盘已经开始工作了,前面说过了,硬盘是低速设备,所以在此期间最好是把CPU使用权让出去。出让CPU使用权有两种方式,一种是用thread_block函数阻塞自己,直到运行条件成熟时再运行,也就是由别人唤醒自己后再继续执行。另一种是用thread_yield主动交出CPU,调度器将所有任务调度一圈之后又会让自己运行,但下次运行时,非常有可能的是运行条件尚未具备,比如此处是硬盘还没有完成操作,驱动程序醒来后也无所事事,还得再将CPU让出去,因此这里用thread_block阻塞驱动程序自己,直到条件成熟时再运行。这种自我阻塞是通过sema_down对信号量执行P操作完成的,即代码“sema_down(&hd->my_channel->disk_done)”。硬盘完成操作后会发中断信号,后面介绍的硬盘中断处理程序intr_hd_handler会在该通道上执行“sema_up(&channel->disk_done)”,从而唤醒当前的驱动程序。sema_down当前的驱动程序就会阻塞,在等待硬盘操作完成期间开始睡觉了。

当硬盘完成操作后会主动发中断,对应的中断处理程序会将该通道的信号量disk_done执行V操作,即代 码”sema_up(&channel->disk_done)”,从而唤醒驱动程序。驱动程序醒来之后,开始判断硬盘的状 态,通过代码”busy_wait(hd)”完成,如果不出现重大硬件损伤的话,通常情况下这种硬盘操作不会失败,因此就将程序通过 PANIC 悬停。如果成功了,就通过read_from_sector 函数将扇区数据读入到缓冲区(buftsecsdone * 512)处,随着完成的扇区数secs_done越来越多,缓冲区地址也会随之偏移。然后使secs_done加上secs_op,更新secs_done。扇区都读入后释放锁。

ide_write函数接受4个参数,硬盘hd、写入硬盘的扇区地址lba、待写入硬盘的数据所在的地址buf、待写入的数据以扇区大小为单位的数量sec_cnt。功能是将buf中sec_cnt扇区数据写入硬盘hd的lba扇区。ide_write同ide_read的逻辑是相同的,区别是 ide_write是将缓冲区buf中的数据写到硬盘,阻塞的时机也有所不同。对于读硬盘来说,驱动程序阻塞自己是在硬盘开始读扇区之后,对于写硬盘来说,驱动程序阻塞自己是在硬盘开始写扇区之后。总之,阻塞的时机一定是在硬盘开始真正忙活之后的那段漫长的时间里。其他方面同ide_read类似,不再赘述。

接着是函数intr_hd_handler,这是硬盘中断处理程序,参数是中断号irq_no。此中断处理程序负责两个通道的中断,因此irq_no要么等于0x2e,要么等于0x2f,它们分别是从片8259A的IRQ14接口和IRQ15接口。由于有两个通道,在uint8_t ch_no = irq_no – 0x2e先获取中断所属的通道号,直接用中断号irq_no减去0x2e,所得的差便是中断通道在通道数组channels中的索引值。大多数情况下,硬盘发生中断通常是由之前对其发出了操作命令引起的,这是主动使其发中断的方式,之前也说过了,为避免无法分清中断信号来自同一通道上的哪块硬盘,主动操作硬盘时申请了通道上的锁,因此,如果通道发生了中断信号,它只会是由最近一次操作的硬盘引起的,并不会是之前某次操作的硬盘发出的。在通过cmd_out函数主动向硬盘发号施令时,我们会将通道的channel->expecting_intr置为true,也就是宣称此通道正期待中断的来临,这是给硬盘中断处理程序看的,也就是之前所说的为中断处理函数埋下的伏笔。因此,在中断处理程序中要判断channel->expecting_intr的值是否为true,如果channel->expecting_intr的值为true,将其置为false (置为true是cmd_out的职责),然后给通道的信号量disk_done执行V操作,即代码“sema_up(&channel->disk_done);”,这样,阻塞在此信号量上的驱动程序便会醒来。

中断处理完成后,需要显式通知硬盘控制器此次中断已经处理完成,否则硬盘便不会产生新的中断,这也是为了保证数据的有效性和安全性。

硬盘控制器的中断在下列情况下会被清掉:

  • 读取了status 寄存器。
  • 发出了reset 命令。
  • 向 reg_cmd 写了新的命令。

我们采取第1种方法,再读一次status寄存器,也就是代码”inb(reg_status(channel));”

最后在ide_init函数加入register_handler(channel->irq_no, intr_hd_handler);,我们把硬盘中断处理程序在ide init中完成注册。

 

5.硬盘信息和扫描分区表

这里完成两项工作:

  • 向硬盘发identify命令获取硬盘的信息。
  • 扫描分区表。

identify命令是0xec,它用于获取硬盘的参数,此命令返回的结果都是以字为单位,并不是字节。

我们需要以MBR 引导扇区为入口,遍历所有主分区,然后找到总扩展分区,在其中递归遍历每一个子扩展分区,找出逻辑分区。

由于涉及到分区的管理,因此我们得给每个分区起个名字,简单起见,最好咱们借鉴现成的Linux 设备命名方案。 Linux中所有的设备都在/dev/目录下,硬盘命名规则是[x]d[y][n],其中只有字母d是固定的,其他带中括号的字符都是多选值,下面从左到右介绍各个字符。

x表示硬盘分类,硬盘有两大类,IDE磁盘和SCSI磁盘。 h代表IDE磁盘,s代表SCSI磁盘,故x取值为h 和s。

d 表示 disk,即磁盘。

y表示设备号,以区分第几个设备,取值范围是小写字符,其中a是第1个硬盘,b是第2个硬盘,依次类推。

n表示分区号,也就是一个硬盘上的第几个分区。 分区以数字1开始,依次类推。

综上所述,sda 表示第1个 SCSI 硬盘, hdc 表示第 3个 IDE 硬盘, sdal 表示第 1个SCSI 硬盘的第1个分区,hdc3表示第3个IDE硬盘的第3个分区。这里统一用SCSI硬盘的命名规则来命名虚拟硬盘Dreams.img和 DreamsFS.img。其中 Dreams.img为sda,DreamsFS.img为sdb。 Dreams.img是裸盘,没有文件系统和分区,因此只处理DreamsFS.img,将其上的主分区占据 sdb[1~4],逻辑分区占据 sdb[5~]。

同样修改

/device/ide.c

// 用于记录总扩展分区的起始 lba, 初始为 0
int32_t ext_lba_base = 0;

// 用于记录硬盘主分区和逻辑分区的下标
uint8_t p_no = 0, l_no = 0;

// 分区队列
struct list partition_list;


/* 构建一个16字节大小的结构体, 用来存分区表项 */
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];                              // 引导代码
    struct partition_table_entry partition_table[4]; // 分区表中有 4 项, 共 64 字节
    uint16_t signature;                              // 启动扇区的结束标志是 0x55, 0xaa
} __attribute__((packed));

/* 将 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 中存储 dst 中两相邻元素交换位置后的字符串
        buf[idx + 1] = *dst++;
        buf[idx] = *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~19, 返回长度为 20 的字符串
    uint8_t sn_start = 10 * 2, sn_len = 20;
    // 硬盘型号, 字偏移量(以字为单位): 27~46, 返回长度为 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     SN: %s\n", hd->name, buf);
    memset(buf, 0, sizeof(buf));

    swap_pairs_bytes(&id_info[md_start], buf, md_len);
    printk("    MODULE: %s\n", buf);

    // 可供用户使用的扇区数, 字偏移量(以字为单位): 60~61, 返回长度为 2 的整形
    uint32_t sectors = *((uint32_t*) &id_info[60 * 2]);
    printk("    SECTORS: %d\n", sectors);
    printk("    CAPACITY: %dMB\n", sectors * 512 / 1024 / 1024);
}

extlba_base是用在分区表扫描函数partition_scan中的,此变量有两个作用,一是作为扫描分区表的标记,partition_scan若发现ext_lba_base为0便知道这是第一次扫描,因此初始为0。另外就是用于记录总扩展分区地址,那时肯定就不为0了。partition_list是所有分区的列表。

接下来的 partition_table_entry 是分区表项,即分区表中的每个分区项,在结构定义结束处有个”_attribute_((packed))”,这是编译器gcc提供的属性定义,_attribute_是gcc特有的关键字,用于告诉gcc在编译时需要做些“特殊处理”,packed就是“特殊处理”,意为压缩的,即不允许编译器为对齐而在此结构中填充空隙,从而保证结构partition_table_entry的大小是 16字节,这与分区表项的大小是吻合的。

下一个是引导扇区结构体boot_sector,成员other大小是446字节,其内容是引导代码,但这并不重要,它在这里只是用来占位,因为在引导扇区中偏移446字节的地方才是分区表,目的是让下面 的分区表partition_table位置正确,partition_table是个数组,数组元素是”struct partition_table_entry”,共 4个元素,即总大小64字节。signature是魔数,类型是uint16_t,即大小是2字节,它是启动扇区的结束标 志0x55, 0xaa,占两个字节,最后一个字节是0xaa。由于x86是小端字节序,故此处变量的实际值为0xaa55,这三个成员加起来总共大小是512字节,最后也用“_attribute_((packed))”严格保证512字节大小。

函数swap_pairs_bytes接受3个参数, 目标数据地址dst、缓冲区buf、数据长度len,功能是将dst中len个相邻字节交换位置后存入buf,,buf是dst最终转换的结果。此函数用来处理identify命令的返回信息,硬盘参数信息是以字为单位的,包括偏移、长度的单位都是字,在这16位的字中,相邻字符的位置是互换的,所以通过此函数做转换。

identify_disk 函数接受 1 个参数,硬盘 hd。功能是向硬盘发送 identify 命令以获得硬盘参数信息。函数体中定义了数组id_info,用来存储向硬盘发送identify命令后返回的硬盘参数。先通过select_disk(hd)选择硬盘,接着通过cmd_out函数向硬盘发送了CMD_IDENTIFY命令后,此时硬盘开始工作,然后调用sema_down阻塞自己。待当前任务被唤醒后,调用”busy_wait(hd)” 判断硬盘状态,如果成功了,调用read_from_sector从硬盘获取信息到id_info,此时id_info中已经是硬盘的参数信息了,接下来开始打印它们。数组buf是缓冲区,是给swap_pairs_bytes使用的,用于存储转换的结果。sn_start表示序列号起始字节地址,其值为10*2,10表示字偏移量,md_start表示型号起始字节地址,其值为27* 2,27表示字偏移量。调用swap_pairs_bytes函数后, buf中已经是字节两两交换的结果,接着输出序列号,后面的输出同理。

继续修改/device/ide.c

/* 扫描硬盘 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;

    // 表遍历分区表 4 个分区表项
    while((part_idx++) < 4) {
        if(p->fs_type == 0x5) {
            // 若为扩展分区
            if(ext_lba_base != 0) {
                // 子扩展分区的 start_lba 是相对于主引导扇区中的总扩展分区地址
                // 递归扫描
                partition_scan(hd, p->start_lba + ext_lba_base);

            } else {
                // ext_lba_base 为 0 表示是第一次读取引导块, 也就是主引导记录所在的扇区
                // 记录下扩展分区的起始 lba 地址, 后面所有的扩展分区地址都相对于此
                ext_lba_base = p->start_lba;
                partition_scan(hd, p->start_lba);
            }

        } 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->logic_parts[l_no].start_lba = ext_lba + p->start_lba;
                hd->logic_parts[l_no].sec_cnt = p->sec_cnt;
                hd->logic_parts[l_no].my_disk = hd;
                list_append(&partition_list, &hd->logic_parts[l_no].part_tag);
                sprintf(hd->logic_parts[l_no].name, "%s%d", hd->name, l_no+5); // 逻辑分区数字是从 5 开始, 主分区是 1~4
                l_no++;

                if(l_no >= 8) {
                    // 只支持8个逻辑分区, 避免数组越界
                    return;
                }
            }
        }
        p++;
    }
    sys_free(bs);
}


/* 打印分区信息 */
static bool partition_info(struct list_elem* pelem, int arg UNUSED) {
    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);

    // 返回 false 与函数本身功能无关
    // 只是为了让主调函数 list_traversal 继续向下遍历元素
    return false;
}


// 硬盘数据结构初始化
void ide_init() {
    printk("ide_init start\n");
    uint8_t hd_cnt = *((uint8_t*)(0x475)); // 获取硬盘的数量
    ASSERT(hd_cnt > 0);
    list_init(&partition_list);
    // 一个 ide 通道上有两个硬盘, 根据硬盘数量反推有几个ide通道
    channel_cnt = DIV_ROUND_UP(hd_cnt, 2);
    struct ide_channel* channel;
    uint8_t channel_no = 0, dev_no = 0;

    // 处理每个通道上的硬盘
    while (channel_no < channel_cnt) {
        channel = &channels[channel_no];
        sprintf(channel->name, "ide%d", channel_no);

        // 为每个ide通道初始化端口基址及中断向量
        switch (channel_no) {
            case 0:
                channel->port_base = 0x1f0; // ide0 通道的起始端口号是 0x1f0
                channel->irq_no = 0x20 + 14; // ide0 通道的中断向量号
                break;
            case 1:
                channel->port_base = 0x170; // ide1 通道的起始端口号是 0x170
                channel->irq_no = 0x20 + 15; // ide1 通道的中断向量号
                break;
        }

        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) { // 内核本身的裸硬盘(hd60M.img)不处理
                partition_scan(hd, 0); // 扫描该硬盘上的分区
            }
            p_no = 0, l_no = 0;
            dev_no++;
        }
        dev_no = 0;

        channel_no++; // 下一个 channel
    }
    printk("\n  all partition info\n");
    // 打印所有分区信息
    list_traversal(&partition_list, partition_info, (int)NULL);
    printk("ide_init done\n");
}

函数partition_scan接受2个参数,硬盘hd和扩展扇区地址ext_lba。功能是扫描硬盘hd中地址为ext_lba的扇区中的所有分区。

每个子扩展分区中都有1个分区表,因此函数partition_scan需要针对每一个子扩展分区递归调用,每调用一次,都要用1扇区大小的内存来存储子扩展分区所在的扇区,即MBR引导扇区或EBR引导扇区。注意,由于是递归调用,每次函数未退出时又进行了函数调用,这会导致栈中原函数的局部数据不释放,并且会在栈中生成新的局部变量,尤其是局部变量很大时,这种递归调用会使栈的内存空间消耗量很大,因此用于存储分区表扇区的内存绝对不能用局部变量。比如咱们刚刚在identify_disk函数中定义的”char id_info[512]”就是局部变量,局部变量占用的是栈空间,随着子扩展分区的增多,每次调用partition_scan扫描分区时都要在栈中占用512字节的内存,分区一多,递归调用时栈就会溢出了。我们的栈加上PCB总共是4096字节,除了PCB外,栈中也就是顶多容纳7个扇区,再加上栈中已经用了一部分空间,因此顶多递归6次,第7次就会使栈溢出。为避免这种情况,我们在函数体开头用sys_malloc 动态申请struct boot_sector大小的内存(1扇区大小)来存储分区表所在的扇区,返回地址存储在指针bs,然后通过ide_read读入1扇区的数据到bs指向的内存。之前我们在定义struct boot_sector时用”_attribute_((packed))”严格限制了结构体大小 ,因此可以通过”bs->partition_table”获得分区表地址,并返回给分区表项指针p。

此时p指向分区表数组,利用指针p遍历所有分区表项。如果分表表项类型p->fs_type.为0x5,这说明是扩展分区,意味着要递归调用partition_scan,前面说过ext_lba_base有两个作用,就体现在if(ext_lba_base != 0)的条件判断中。先看else分支,它表示ext_lba_base为0,这说明这是第一次调用partition_scan,此时获取的是MBR引导扇区中的分区表,需要记录下总扩展分区的起始lba地址,因为后面所有的子扩展分区地址都相对于此。于是ext_lba_base = p->start_lba用p->start_lba为ext_lba_base赋值,因此现在ext_lba_base是总扩展分区地址,不再为0。接着在partition_scan(hd, p->start_lba)用p->start_lba即总扩展分区起始地址作为参数继续调用partition_scan,如果ext_lba_base不为0,这说明已不是第1次递归调用,此时所获取的分区表是EBR引导扇区中的,子扩展分区的起始扇区地址是相对于主引导扇区中的总扩展分区地址ext_lba_base,因此下面partition_scan(hd, p->start_lba + ext_lba_base)用”p->start_lba + ext_lba_base”作为partition_scan的参数继续调用。

以上代码片段是处理扩展分区的情况,else if(p->fs_type != 0) 之后便是处理主分区或逻辑分区。分区类型(文件系统类型)若为0,则表示empty,即无效的分区类型,因此判断,只要分区类型p->fs type不等于0,就认为是有效的分区。如果partition_scan的参数ext_lba为0,说明当前是MBR引导分区,因此此时的分区表中除了主分区就是总扩展分区,扩展分区已经在上面代码片段中处理过了,因此此时的分区必然是主分区。接着将主分区的信息收录到硬盘hd的prim_parts数组中。然后list_append(&partition_list, &hd->prim_parts[p_no].part_tag)将分区加入到分区列表partition_list中。sprintf(hd->prim_parts[p_no].name, “%s%d”, hd->name, p_no+1)通过sprintf函数拼接字符串为主分区命名,主分区名称从1起,如sda1。之后是处理逻辑分区的代码,同主分区类似,不再赘述。

partition_info函数的功能是打印分区信息,此函数被用在list_traversal中作为回调函数调用,必须有2个参数,但我们只用到分区标记pelem,所以第2个参数arg我们用UNUSED来修饰,表示未使用。UNUSED是个宏,定义在global.h中,也是利用gcc提供的属性unused实现的。

通常,如果声明了某个变量,但从未对其进行引用,编译器将发出警告。此属性指示编译器您预计不会使用某个变量,并指示它在未使用该变量时不要发出警告

我们先在/kernel/global.h添加以下

#define UNUSED __attribute__ ((unused))    // 表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息

接下来要把函数identify_disk和partition_scan加入到ide_init中。接着在调用list_traversal打印所有分区信息。

 

对应/device/ide.h加入

void ide_init(void);

void intr_hd_handler(uint8_t irq_no);

void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);

void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);

extern struct list partition_list;

 

修改init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
#include "tss.h"
#include "syscall-init.h"
#include "ide.h"

/* 负责初始化所有模块 */
void init_all() {
    put_str("init_all\n");
    idt_init();         // 初始化中断
    mem_init();         // 初始化内存管理系统
    thread_init();      // 初始化线程相关结构
    timer_init();       // 初始化 PIT
    console_init();     // 初始化终端
    keyboard_init();    // 键盘初始化
    tss_init();         // tss 初始化
    syscall_init();     // 初始化系统调用
    intr_enable();      // 后面的 ide_init 需要打开中断
    ide_init();         // 初始化硬盘
}

 

执行结果如下:

 

6.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇