操作系统真象还原 学习笔记14--编写硬盘驱动程序

selph
selph
发布于 2021-02-21 / 1297 阅读
0
0

操作系统真象还原 学习笔记14--编写硬盘驱动程序

本篇对应书籍第十三章的内容

本篇内容介绍了硬盘分区表的结构和存放,以及编写了简易版本的硬盘驱动程序

本篇难点:

  • 要有耐心去看硬盘IO操作是如何封装的

硬盘及分区表

之前的那个虚拟磁盘用来装系统内核了,为了图省事,再整一块虚拟硬盘来用

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

通过bximage创建一个80M的硬盘,得到硬盘参数:

  ata0-master: type=disk, path="hd80M.img", mode=flat

将master改成slave,填入bochsrc.disk

启动Bochs,查看内存0x475,获取当前安装的硬盘数:

image-20210215192758014

BIOS 检查的时候,会把安装的硬盘数写入内存 0x475 的位置

到此,硬盘创建好安装完毕了

创建硬盘分区表

文件系统 = 数据结构 + 算法,所以说,文件系统也是软件,管理对象是文件,管辖范围是分区

关于柱面、扇区、磁道、磁头、盘面等知识之前讲过了,这里就不回顾了

硬盘容量 = 每磁道扇区数x柱面数x512字节x磁头数

分区表位于 MBR 中,只有64字节大小,每一个分区表项都占有16字节,所以最多有4个分区

为了支持任意数量分区,发明了扩展分区,通过id属性值判断分区类型

分区表中共 4个分区,哪个做扩展分区都可以,扩展分区是可选的,但最多只有1个,其余的都是主分区。在过去没有扩展分区时,这 4 个分区都是主分区;为了兼容 4 个主分区的情况,扩展分区中的第 1 个逻辑分区的编号从 5 开始。

使用fdisk进行分区:

image-20210220170730793

随便分一分,一个主分区,一个扩展分区,扩展分区随便分了4份,类型都是0x66

磁盘分区表浅析

磁盘分区表,Disk Partition Table,由多个元信息组成的表,每个表项对应一个分区,主要记录各分区的起始扇区地址、大小界限等

最初的磁盘分区表位于 MBR 引导扇区中(MBR 扇区由三部分组成:主引导记录MBR、磁盘分区表DPT、结束魔数55AA)

在硬盘中,最开始的扇区是 MBR 扇区,接着是多个空闲扇区,然后才是具体的分区,中间这个空闲扇区是由分区工具决定的,一般分区的起始地址都是柱面的整数倍,对于不够1个柱面的剩余空间不再使用

以前的磁盘分区表默认支持 4 个主分区,但是需要的分区多了,就发明了扩展分区,从这4个主分区选一个出来作为主扩展分区来使用,主扩展分区可以划分为多个子扩展分区,每个子扩展分区在逻辑上相当于硬盘

扩展分区采用链式结构,将所有子扩展分区表串在一起,分区表也采用这种结构,表项分两部分:

  • 描述逻辑分区的信息
  • 描述下一个子扩展分区的地址

每个逻辑分区最开始的扇区,EBR扩展引导扇区,用于存储子扩展分区的分区表,逻辑结构和MBR扇区一样

MBR 和 EBR 的第一个表项都指向一个分区的起始,起始地址是扇区地址,该逻辑分区最开始的扇区,OBR,操作系统引导扇区,第二个分区表项指向下一个子扩展分区的 EBR

分区表项结构如图所示:

image-20210220165922490

这一块需要解析img文件到十六进制进行查看学习,这里不方便操作,就先跳过了,具体需要的时候再回过头来了解

编写硬盘驱动程序

驱动程序是对硬件接口操作的封装,来简化这些接口的使用

硬盘初始化

硬盘的中断信号挂在 8259A 从片 IRQ15 的位置,要使用该位置,需要把主片的 IRQ2 也打开

kernel/interrupt.c

	//打开主片上的 IR0 也就是目前只接受时钟产生的中断
	outb (PIC_M_DATA, 0xf8);
	outb (PIC_S_DATA, 0xbf);

lib/kernel/stdio-kernel.c

为了图输出方便,封装一个内核的printf就是printk函数

#include "stdio-kernel.h"
#include "print.h"
#include "stdio.h"
#include "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);
}

device/ide.h

这里定义硬盘相关的数据结构,分区结构,硬盘结构,ata通道结构,都是之后操作要用到的,用到的时候再具体介绍功能

#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];     // 一个通道上连接两个硬盘, 一主一从
};
#endif

device/ide.c

使用宏保存基础的内容,通过ide_init函数做一些基础工作:

#include "ide.h"
#include "sync.h"
#include "io.h"
#include "stdio.h"
#include "stdio-kernel.h"
#include "interrupt.h"
#include "memory.h"
#include "debug.h"
#include "console.h"
#include "timer.h"
#include "string.h"
#include "list.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//, 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);

        channel_no++; // 下一个 channel
    }
    // 打印所有分区信息
    printk("ide_init done\n");
}

到这里初始化工作就完毕了

实现 thread_yield 和 idle 线程

这里需要完成一些基础构件才能继续进行

thread_yield 的功能是主动把 CPU 使用权让出来,执行后任务状态变为 READY 然后立即重新加入就绪队列等待调度执行

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(void) {
...
    // 如果就绪队列中没有可运行的任务, 就唤醒 idle
    if (list_empty(&thread_ready_list)) {
        thread_unblock(idle_thread);
    }
...
}

// 主动让出 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);
    cur->status = TASK_READY;
    schedule();
    intr_set_status(old_status);
}
// 初始化线程环境
void thread_init(void) {
...
    // 将当前 main 函数创建为线程
    make_main_thread();
    idle_thread = thread_start("idle", 10, idle, NULL);
...
}

这里做了两件事:创建的thread_yield函数,和创建空闲线程idle

thread_yield的工作有三步:

  1. 将自己加入就绪队列
  2. 状态设置为READY
  3. 调用 schedule 进行任务调度

因为前两步必须是原子操作,所以要先关中断

为了以防线程都执行完了,导致没有任务可调度而挂起,这里创建了一个idle线程,当就绪队列中没有任务了,就会执行该线程,该线程的功能是内联汇编hlt来让CPU真正意义上的挂起,不再执行指令,直到有中断产生,CPU再继续执行指令

实现简单的休眠函数

在等待硬盘操作的过程中,最好把CPU让出来,为了实现等待,需要定义一个休眠函数,这里实现一个简易版的休眠功能:

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) {
   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次,所以每10毫秒发生1次中断

ticks_to_sleep 函数以中断为单位来进行等待,等到过了一定中断数之后才开始执行

mtime_sleep 函数是上一个函数的封装,以毫秒数来进行等待,把毫秒数换算成要等待的周期数,然后再调用上一个函数进行

完善硬盘驱动程序上

基础部分已经准备完毕,接下来看硬盘的中断处理函数部分:

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);
    // 写入扇区号
    outb(reg_lba_l(channel), lba);
    outb(reg_lba_m(channel), lba >> 8);
    outb(reg_lba_h(channel), lba >> 16);
    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) {
        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) {
        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;
    uint16_t time_limit = 30 * 1000; // 可以等待 30000 毫秒
    while (time_limit -= 10 >= 0) {
        if (!(inb(reg_status(channel)) & BIT_STAT_BSY)) {
            return (inb(reg_status(channel)) & BIT_STAT_DRQ);
        } else {
            mtime_sleep(10); // 睡眠 10 毫秒
        }
    }
    return false;
}

这里是几个功能函数:

  • select_disk 函数:选择待操作的硬盘是主盘还是从盘

    利用 device 寄存器中的 dev 位,0表示主盘,1表示从盘

  • select_sector 函数:向硬盘控制器写入起始扇区地址及要读写的扇区数

  • read_from_sector 函数:硬盘读入 sec_cnt 个扇区的数据到 buf

  • write2sector 函数:将 buf 中 sec_cnt 扇区的数据写入硬盘

  • busy_wait 函数:等待 30 秒

完善硬盘驱动程序下

下半部分:

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);
    if (channel->expecting_intr) {
        channel->expecting_intr = false;
        sema_up(&channel->disk_done);
        inb(reg_status(channel));
    }
}
// 硬盘数据结构初始化
void ide_init() {
...

        sema_init(&channel->disk_done, 0);
        register_handler(channel->irq_no, intr_hd_handler);

        channel_no++; // 下一个 channel

这里是对上半部分那几个函数的封装,以及中断处理程序

硬盘中断仅在发出请求之后,硬盘处理完成之后进行触发,中断处理程序就是还原通道的信号量,以便下一个程序继续使用硬盘

通过读取status寄存器,可以清掉硬盘的中断

获取硬盘信息

这里只处理hd80M.img,主分区分别占据sdb1~4,逻辑分区占据sdb5~n

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!!!!!", hd->name);
        PANIC(error);
    }
    read_from_sector(hd, id_info, 1);

    char buf[64];
    uint8_t sn_start = 10 * 2, sn_len = 20, 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);
    uint32_t sectors = *(uint32_t*)&id_info[60 * 2];
    printk("    SECTORS: %d\n", sectors);
    printk("    CAPACITY: %dMB\n", sectors*512/1024/1024);
}

这里通过定义一些数据结构,获取到硬盘中的分区表,读取分区表中的内容,进行输出,从而得到硬盘信息

扫描分区表

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 表示是第一次读取引导块, 也就是主引导记录所在的扇区
                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)
                    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 使用了 sys_malloc 进行内存申请堆空间进行存放,通过递归获取指定硬盘中的每一个分区表,将分区表信息存储在参数中给定的hd指针里

运行 Bochs

编译,运行:

image-20210221102626963

获取到了硬盘的信息

本篇总结

本篇内容介绍了硬盘分区表的存放方式,存放内容等,介绍了扩展分区是怎么扩展的

本篇内容带着实现了一套简易的硬盘驱动程序,驱动就是对底层重复操作的一个函数封装,这里封装了对硬盘的读写操作,获取硬盘信息操作和扫描分区表操作

在这期间,我们改进了内核里的一个问题,就是当任务全部都执行完毕后,没有任务执行而报错的问题,通过创建idle线程,在空闲时启动,通过内联汇编执行hlt命令让CPU真正意义上的挂起,不再执行指令,等出现中断的时候,再进行恢复

本篇还封装了一个内核版本的printf:printk 函数,方便打印字符串用

这一篇内容我学的怪烦的,感觉很没劲,全篇都在对硬盘IO进行各种封装,然后获取硬盘信息,操作硬盘,本篇的重点在于硬盘驱动程序,所以懒惰的我就直接copy代码来感受驱动程序的编写流程啦

驱动程序的编写是通过层层封装来实现的,先封装最基本的IO操作,然后根据IO操作封装出功能函数,成为驱动程序

无聊的内容到此就要结束了,真不错

参考资料


评论