手写简易操作系统(二十一)--硬盘驱动

前情提要

上面一节我们实现了 mallocfree 的系统调用,这一节我们来实现硬盘驱动。

一、硬盘分区

我们的文件系统安装在一块全新的硬盘中,我们先创建它,然后在给他分区。

1.1、创建硬盘

首先是创建,这个之前我们已经干过一次了

然后是修改配置文件,虽然创建了,但也需要让虚拟机知道

添加最下面的一行。在物理地址 0x475 处存储着主机安装硬盘的数量。它是由BIOS检测并写入的。启动bochs后可以调试一下,获得现在的硬盘数

1.2、创建硬盘分区

当初硬盘制造者认为,一台机器上顶多安装4个操作系统,每个操作系统各占1个分区,所以硬盘支持4个分区足矣。想想也是,谁没事在1台电脑上不断重启机器只为来回切换4个操作系统呢?但是随着硬盘容量越来越大,为方便文件管理,必须想办法支持更多的分区。

分区是逻辑上划分磁盘空间的方式,归根结底是人为地将硬盘上的柱面扇区划分成不同的分组,每个分组都是单独的分区。各分区都有"描述符"来描述分区本身所在硬盘上的起止界限等信息,在硬盘的MBR中有个64字节"固定大小"的数据结构,这就是著名的分区表,分区表中的每个表项就是一个分区的"描述符",表项大小是16字节,因此64字节的分区表总共可容纳4个表项,这就是为什么硬盘仅支持4个分区的原因。

其实分区表的长度并不是由结构本身限制的,而是由其所在的位置限制的,它必须存在于MBR引导扇区或EBR引导扇区中。在这512字节中,前446字节是硬盘的参数和引导程序,然后才是64字节的分区表,最后是2字节的魔数55aa。。随着计算机的发展,很多程序已经对这个扇区有依赖了,尤其是一些引导型程序(如BIOS),都会在该扇区的512字节中的固定位置读取关键数据,如果更改了此扇区中的数据结构长度,那么很多程序都必须做出改变。

为此,硬盘厂商准备在分区"描述符"动动手脚。在这个"描述符"中有个属性是文件系统id,它表示文件系统的类型,为支持更多的分区,专门增加一种id属性值(id为5),用来表示该分区可被再次划分出更多的子分区,这就是逻辑分区。因为只是在分区表项中通过属性来判断分区类型,所以这4个分区中的任意一个都可以作为扩展分区。扩展分区是可选项,有没有都行,但最多只有1个,1个扩展分区在理论上可被划分出任意多的子扩展分区。

发明扩展分区的目的是为了支持任意数量的分区,但具体划分出多少分区,完全是由用户决定的,所以,扩展分区是种抽象、不具实体的分区,它类似于一种"宣告",告诉大家此分区需要再被划分出子分区,也就是所谓的逻辑分区,逻辑分区才可以像其他主分区那样使用。因此,逻辑分区只存在于扩展分区,它属于扩展分区的子集。

现在我们开始分区

第一个分区,主分区,分区号1,扇区号 2048-20480

第二个分区,主分区,分区号2,扇区号 20481-42500

第三个分区,主分区,分区号3,扇区号 43008-64000

第四个分区,扩展分区,分区号4,扇区号 65536-204623

在第四分区中创建逻辑分区,分区号5,扇区号 67584-167584

在第四分区中创建逻辑分区,分区号6,扇区号 169984-204623

最后将分区信息写入到磁盘内。

二、磁盘分区表简析

磁盘分区表(Disk Partition Table)简称DPT,是由多个分区元信息汇成的表,表中每一个表项都对应一个分区,主要记录各分区的起始扇区地址,大小界限等。

最初的磁盘分区表位于MBR引导扇区中,早在加载loader时就和大伙儿介绍过MBR,MBR(Main Boot Record)即主引导记录,它是一段引导程序,其所在的扇区称为主引导扇区,

该扇区位于0盘0道1扇区(物理扇区编号从1开始,逻辑扇区地址LBA从0开始),也就是硬盘最开始的扇区,扇区大小为512字节,这512字节内容由三部分组成。

(1)主引导记录MBR。

(2)磁盘分区表DPT。

(3)结束魔数55AA,表示此扇区为主引导扇区,里面包含控制程序。

MBR引导程序位于主引导扇区中偏移0~0x1BD的空间,共计446字节大小,这其中包括硬盘参数及部分指令(由BIOS跳入执行),它是由分区工具产生的,独立于任何操作系统。

磁盘分区表位于主引导扇区中偏移0x1BE~0x1FD的空间,总共64字节大小,每个分区表项是16字节,因此磁盘分区表最大支持4个分区。分区表项结构如下

刚刚好是16字节。

2.1、查看主分区分区表

文件系统类型是指NTFS、FAT32、EXT2等,我们在fdisk过程中用l命令列出的便是。为了能够真正理解上面的内容,我们用工具看一下磁盘的MBR。

可以看到结尾是55aa,这个首先没问题,

看四个分区结构如下

00 20 21 00 83 46 06 01 00 08 00 00 01 48 00 00 
00 66 26 01 83 A4 27 02 00 58 00 00 05 4E 00 00 
00 AC 2B 02 83 FA 38 03 00 A8 00 00 01 52 00 00 
00 14 11 04 05 BB 3F 0C 00 00 01 00 50 1F 02 00

第一个扇区的起始偏移扇区为 0x4801 ,起始偏移扇区为 0x0800,这些都没问题。注意读取是小端字节序。

第四个扇区的起始偏移扇区为 0x021f0 ,起始偏移扇区为 0x0800

主分区的看完了,我们看一下逻辑分区的

2.2、逻辑分区分区表

扩展分区中的所有分区表被组织成单向链表,咱们查看链表中的第1个结点,也就是第1个子扩展分区的EBR引导扇区起始偏移地址为 65536* 512 = 33554432

我们还是将有用的部分提取出来

00 34 31 04 83 6E 05 0A 00 08 00 00 A1 86 01 00 
00 73 2A 0A 05 BB 3F 0C 00 90 01 00 50 8F 00 00 

首先看第一条,他指出,第一个扩展分区的大小为 0x0186A1,第一个扩展分区的相对于当前分区的偏移地址为 0x0800, 加上当前的扇区号0x10000正好就是第一个逻辑扇区的起始地址。

再看第二条,他指出,第二个扩展分区的链表相对于当前分区的偏移地址为 0x019000,加上当前的扇区号为 167936,在查看一下第二个扩展分区的链表节点

第二个扩展分区的链表可以看出,当前分区的大小为 0x8750,当前分区的相对偏移地址为 0x0800,算一下绝对的地址即为 169984

三、编写硬盘驱动程序

硬盘的一些端口还是看一下之前的文章 手写简易操作系统(三)--加载Loader

3.1、硬盘驱动的数据结构

c 复制代码
/* 分区结构 */
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];			         // 本硬盘的名称,如sda等
    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];	    // 一个通道上连接两个硬盘,一主一从
};

可以看到,结构分为三层,第一层是ata通道,一个ata通道有两块硬盘。第二层是硬盘,一块硬盘最多有四个主分区,无数个逻辑分区,但是我们这里只支持8个逻辑分区。第三层就是分区了,分区是我们控制的最底层,它包含起始扇区,扇区数,所归属的硬盘等结构。

3.2、初始化

硬盘还是像之前在loader中一样,读取寄存器。这里我们先把这些端口定义出来

c 复制代码
/* 定义硬盘各寄存器的端口号 */
#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              // 第6位为1,LBA寻址方式
#define BIT_DEV_MASTER	0x10          // 第4位为1,从盘
#define BIT_DEV_SLAVE	0x00          // 第4位为1,从盘

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

/* 定义可读写的最大扇区数,调试用的 */
#define max_lba ((100*1024*1024/512) - 1)

/* 通道数量 */
uint8_t channel_cnt;

/* 最多支持两个通道 */
struct ide_channel channels[2];

硬盘上述的数据结构初始化过程如下

c 复制代码
/* 硬盘数据结构初始化 */
void ide_init() {
    printk("ide_init begin!\n");
    // 获取硬盘的数量,硬盘数量由BIOS保存在0x475的内存地址
    int8_t hd_cnt = *((uint8_t*)(0x475));
    // 保证硬盘数量是大于0的
    ASSERT(hd_cnt > 0);
    // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
    channel_cnt = DIV_ROUND_UP(hd_cnt, 2);
    // 分别处理每个通道上的硬盘
    for (uint8_t channel_no = 0; channel_no < channel_cnt; channel_no++) {
        // 指针指向不同的通道
        struct ide_channel* channel = &channels[channel_no];
        // 为每个通道命名
        sprintf(channel->name, "ide%d", channel_no);
        // 初始化每个通道的端口基址及中断向量
        if (channel_no == 0) {
            channel->port_base = 0x1f0;	  // ide0通道的起始端口号是0x1f0
            channel->irq_no = 0x20 + 14;  // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
        }
        else if (channel_no == 1) {
            channel->port_base = 0x170;   // ide1通道的起始端口号是0x170
            channel->irq_no = 0x20 + 15;  // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断
        }
        channel->expecting_intr = false;  // 未向硬盘写入指令时不期待硬盘的中断
        // 初始化通道锁
        lock_init(&channel->lock);

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

3.3、完成thread_yield 和 idle 线程

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

thread_yield很简单,就是将当前线程状态置为 TASK_READY ,由于线程是主动放弃的CPU控制权,所以不改变其优先级。直接加入多级优先队列

c 复制代码
void thread_yield(void) {
    enum intr_status old_status = intr_disable();
    struct task_struct* cur = running_thread();   
    cur->status = TASK_READY;
    mlfq_push_wspt(cur);           // 不改变其优先级和时间片
    schedule();
    intr_set_status(old_status);
}

创建idle线程的代码也很简单

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

/* 初始化线程环境 */
void thread_init(void) {
    put_str("thread_init start\n");
    lock_init(&pid_lock);            // pid锁初始化
    mlfq_init();                     // 多级队列初始化
    make_main_thread();              // 创建主线程
    idle_thread = thread_start("idle", idle, NULL); // 创建idle线程
    put_str("thread_init done\n");
}

其中UNUSED的宏定义是为了在编译时不产生未使用变量的报错。idle会先被添加到多级队列中,先被调度上CPU执行一次,这次会执行到idle线程阻塞自己,阻塞自己后会自动调度上别的线程。

当多级队列中没有线程了,就会唤醒idle线程,唤醒后的idle线程执行的是内联汇编

c 复制代码
asm volatile ("sti; hlt" : : : "memory");

开中断和等待,hlt的等待不是空转CPU,而是真的CPU等待。开中断的目的是等外部中断来打断自己。

3.4、实现休眠函数

休眠函数的实现就要依赖上面的thread_yield 函数了

c 复制代码
/**********************
@author: liyajun
@data: 2024.3.30 20:04
@description: 以tick为单位的sleep
***********************/
static void ticks_to_sleep(uint32_t sleep_ticks) {
    uint32_t start_tick = ticks;

    /* 若间隔的ticks数不够便让出cpu */
    while (ticks - start_tick < sleep_ticks) {
        thread_yield();
    }
}

/**********************
@author: liyajun
@data: 2024.3.30 20:04
@description: 以毫秒为单位的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); 
}

首先是以ticks为标准的休眠函数,如果当前还不到休眠时间的话,那么直接让出CPU的使用权,实现以毫秒为基准的休眠函数的话就是将其改为以ticks为标准,毕竟CPU的计时是以ticks为基准的。

四、完善硬盘驱动

本节主要是完成两个函数,一个是读,一个是写

c 复制代码
/* 硬盘hd的lba起始扇区,读取sec_cnt个扇区到buf */
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);
/* 将buf中sec_cnt扇区数据写入硬盘 */
void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);

这一节是在是没什么太关键的只是,主要就是读写寄存器,我在这里粘一下代码

c 复制代码
/* 向硬盘控制器写入起始扇区地址及要读写的扇区数 */
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地址(即扇区号)
    outb(reg_lba_l(channel), lba);		     // lba地址的低8位
    outb(reg_lba_m(channel), lba >> 8);	     // lba地址的8~15位
    outb(reg_lba_h(channel), lba >> 16);     // lba地址的16~23位
    // 写入lba的高4位地址,顺便加上控制字
    uint8_t reg_device = 0;
    if (hd->dev_no == 0) reg_device = BIT_DEV_MBS | BIT_DEV_LBA | BIT_DEV_MASTER | lba >> 24;
    if (hd->dev_no == 1) reg_device = BIT_DEV_MBS | BIT_DEV_LBA | BIT_DEV_SLAVE | lba >> 24;
    outb(reg_dev(hd->my_channel), reg_device);
}

/* 向通道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,为0则读取256个扇区 */
static void read_from_sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
    // 要读取的字节数
    uint32_t size_in_byte = sec_cnt == 0 ? 256 * SEC_BIT : sec_cnt * SEC_BIT;
    // 读取这些字节
    insw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}

/* 将buf中sec_cnt扇区的数据写入硬盘,为0则写入256个扇区 */
static void write2sector(struct disk* hd, void* buf, uint8_t sec_cnt) {
    // 要写入的字节数
    uint32_t size_in_byte = sec_cnt == 0 ? 256 * SEC_BIT : sec_cnt * SEC_BIT;
    // 写入这些字节
    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;
}

/* 从硬盘读取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) {
        // 每次读入的扇区数,最大256
        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);
        // 4 阻塞自己,等待硬盘中断程序唤醒自己
        sema_down(&hd->my_channel->disk_done);
        // 5 醒来后,检测硬盘状态是否可读,不可读的话在此输出错误信息
        if (!busy_wait(hd)) {
            char error[64];
            sprintf(error, "%s read sector %d failed!!!!!!\n", hd->name, lba);
            PANIC(error);
        }
        // 6 把数据从硬盘的缓冲区中读出
        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) {
        // 每次写入的扇区数,最大256
        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);
        // 6 在硬盘响应期间阻塞自己
        sema_down(&hd->my_channel->disk_done);
        // 7 醒来后执行下一次的写入,或者结束,释放锁
        secs_done += secs_op;
    }
    lock_release(&hd->my_channel->lock);
}

/* 硬盘中断处理程序 */
static void intr_hd_handler(uint8_t irq_no) {
    // 保证是两个硬盘通道的中断,第一个通道的硬盘中断号是0x2e,第二个是0x2f
    ASSERT(irq_no == 0x2e || irq_no == 0x2f);
    // 获得是哪个通道
    uint8_t ch_no = irq_no - 0x2e;
    // 获得通道的指针
    struct ide_channel* channel = &channels[ch_no];
    // 不必担心此中断是否对应的是这一次的expecting_intr,每次读写硬盘时会申请锁,从而保证了同步一致性
    if (channel->expecting_intr) {
        // 期待中断为假
        channel->expecting_intr = false;
        // 唤醒读写的程序
        sema_up(&channel->disk_done);
        // 读取状态寄存器使硬盘控制器认为此次的中断已被处理,从而硬盘可以继续执行新的读写
        inb(reg_status(channel));
    }
}

最后将硬盘中断函数注册。

五、扫描分区表

5.1、获取硬盘信息

identify命令是0xec,它用于获取硬盘的参数,不过奇怪的是此命令返回的结果都是以字为单位,并不是字节,这一点要注意。咱们只是来验证驱动程序,下表中只列出了咱们用到的三个参数。

字节偏移量 描述
10-19 硬盘序列号,长度为20的字符串
27-46 硬盘型号,长度为40的字符串
60-61 可供用户使用的扇区数,长度为2的整型

5.2、分区表扫描

需要以MBR引导扇区为入口,遍历所有主分区,然后找到总扩展分区,在其中递归遍历每一个子扩展分区,找出逻辑分区。由于涉及到分区的管理,因此我们得给每个分区起个名字。

这里介绍一下Linux现成的命名方案,

  1. SCSI/SATA 硬盘命名
    • sda, sdb, sdc 等:表示 SCSI 或 SATA 接口的硬盘,字母后面的数字表示硬盘的编号,从 a 开始递增。
    • sda1, sda2, sdb1, sdc1 等:表示 SCSI 或 SATA 接口的硬盘的分区,数字表示分区的编号,从 1 开始递增。
  2. IDE 硬盘命名
    • hda, hdb, hdc 等:表示 IDE 接口的硬盘,字母后面的数字表示硬盘的编号,从 a 开始递增。
    • hda1, hda2, hdb1, hdc1 等:表示 IDE 接口的硬盘的分区,数字表示分区的编号,从 1 开始递增。
  3. NVMe 硬盘命名
    • /dev/nvmeXnY:表示 NVMe 控制器的编号为 X,命名空间的编号为 Y
  4. 其他设备
    • 除了硬盘之外,其他设备(如光驱、USB 设备等)也有相应的命名规则,例如 /dev/cdrom 表示光驱,/dev/usbX 表示 USB 设备等。

首先先构建一个结构体用来存分区表项中的16字节数据,在创建一个引导扇区的结构体,可以提取到四个主分区的数据。

c 复制代码
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));	 // 保证CPU不会优化

/* 引导扇区,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));

首先就是处理硬盘参数

c 复制代码
/* 将dst中len个相邻字节交换位置后存入buf,此函数用来处理identify命令的返回信息*/
static void swap_pairs_bytes(const char* dst, char* buf, uint32_t len) {
    // 硬盘参数信息是以字为单位的,在16位的字中,相邻字符的位置是互换的,所以通过此函数做转换。
    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];
    uint8_t sn_start = 10 * 2;
    uint8_t sn_len = 20;
    uint8_t md_start = 27 * 2;
    uint8_t 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 sectors = *(uint32_t*)&id_info[60 * 2];
    printk("      SECTORS: %d\n", sectors);
    printk("      CAPACITY: %dMB\n", sectors / 2 / 1024);
}

其次就是扫描每个硬盘的分区,将其加入分区队列中

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);
    // 指向四个主分区
    struct partition_table_entry* p = bs->partition_table;
    // 遍历分区表4个分区表项
    for (uint8_t part_idx = 0; part_idx < 4; part_idx++) {
        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++;
                // 只支持4个主分区
                if (l_no >= 4) return;
            }
            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++;
                // 只支持8个扩展分区
                if (l_no >= 8) return;
            }
        }
        p++;
    }
    sys_free(bs);
}

最后就是打印硬盘分区队列

c 复制代码
/* 打印分区信息 */
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);
    // 在此处return false与函数本身功能无关,只是为了让主调函数list_traversal继续向下遍历元素
    return false;
}

老实说,在操作系统看来,数据容器是以分区作为区分的,而不是硬盘,更不是通道。

5.3、仿真

结束语

本节我们实现了对硬盘参数的读取,以及对分区信息的读取,主要的信息还是起始扇区,分区大小。下一节我们将在此基础上实现文件系统,由于文件系统较为复杂,所以我们可能会分为多章节。

老规矩,本节的代码地址:https://github.com/lyajpunov/os

相关推荐
_小猪沉塘1 小时前
L11&12&13 【哈工大_操作系统】内核级线程&内核级线程实现&操作系统之“树”
操作系统
vvvae12342 小时前
分布式数据库
数据库
雪域迷影2 小时前
PostgreSQL Docker Error – 5432: 地址已被占用
数据库·docker·postgresql
bug菌¹3 小时前
滚雪球学Oracle[4.2讲]:PL/SQL基础语法
数据库·oracle
逸巽散人3 小时前
SQL基础教程
数据库·sql·oracle
月空MoonSky4 小时前
Oracle中TRUNC()函数详解
数据库·sql·oracle
momo小菜pa4 小时前
【MySQL 06】表的增删查改
数据库·mysql
向上的车轮5 小时前
Django学习笔记二:数据库操作详解
数据库·django
编程老船长5 小时前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
全栈师6 小时前
SQL Server中关于个性化需求批量删除表的做法
数据库·oracle