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

前情提要

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

一、硬盘分区

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

1.1、创建硬盘

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

image-20240329212149454

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

image-20240329212232205

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

image-20240329212958435

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个扩展分区在理论上可被划分出任意多的子扩展分区。

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

现在我们开始分区

image-20240329214550937

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

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

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

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

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

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

image-20240329215522869

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

image-20240329220042000

二、磁盘分区表简析

磁盘分区表(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个分区。分区表项结构如下

image-20240329215913622

刚刚好是16字节。

2.1、查看主分区分区表

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

image-20240329220435261

可以看到结尾是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

image-20240329221707731

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

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,在查看一下第二个扩展分区的链表节点

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

三、编写硬盘驱动程序

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

3.1、硬盘驱动的数据结构

/* 分区结构 */
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中一样,读取寄存器。这里我们先把这些端口定义出来

/* 定义硬盘各寄存器的端口号 */
#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];

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

/* 硬盘数据结构初始化 */
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控制权,所以不改变其优先级。直接加入多级优先队列

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线程的代码也很简单

/* 系统空闲时运行的线程 */
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线程执行的是内联汇编

asm volatile ("sti; hlt" : : : "memory");

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

3.4、实现休眠函数

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

/**********************
@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为基准的。

四、完善硬盘驱动

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

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

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

/* 向硬盘控制器写入起始扇区地址及要读写的扇区数 */
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字节数据,在创建一个引导扇区的结构体,可以提取到四个主分区的数据。

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));

首先就是处理硬盘参数

/* 将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);
}

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

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

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

/* 打印分区信息 */
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、仿真

image-20240330182125127

结束语

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/511258.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《Git版本控制管理》笔记

第三章 git --version查看版本号git --help查看帮助文档裸双破折号分离参数 git diff -w master origin – tools/Makefile将当前目录或任何目录转化为Git版本库 git init 初始化之后项目目录中&#xff0c;有名为.git的文件git status 查看git状态git commit 提供日志消息和作…

Docker、Kubernetes之间的区别

比较容器化工具&#xff1a;了解 Docker、Kubernetes 在应用程序部署和管理方面的差异。 基本概述 Docker 是一个流行的容器化平台&#xff0c;允许开发人员在容器中创建、部署和运行应用程序。 Docker 提供了一组工具和 API&#xff0c;使开发人员能够构建和管理容器化应用程…

【星海随笔】Ubuntu22.04忘记密码

服务器篇&#xff1a; 有问题可留言。 第一步 远程console界面进入该设备 并重启该设备 如果看到这个界面情况 则点击右上角按钮 【发送 CtrlAltDelete】 调出grub启动菜单 NOTE&#xff1a;启动的后半段去点击这个按钮&#xff0c;前半段一直点会一直重启 如果是直连服务器&a…

AI智能客服机器人是什么?对企业重要吗?

在数字化时代&#xff0c;客户服务是企业与客户建立牢不可破关系的重要桥梁。AI智能客服机器人&#xff0c;顾名思义&#xff0c;就是利用人工智能技术提升客户服务体验的自动化工具。今天&#xff0c;就让我们来揭开AI智能客服机器人的神秘面纱&#xff0c;并讨论它对企业的重…

增强Java技能:使用OkHttp下载www.dianping.com信息

在这篇技术文章中&#xff0c;我们将探讨如何使用Java和OkHttp库来下载并解析www.dianping.com上的商家信息。我们的目标是获取商家名称、价格、评分和评论&#xff0c;并将这些数据存储到CSV文件中。此外&#xff0c;我们将使用爬虫代理来绕过任何潜在的IP限制&#xff0c;并实…

Ollama教程——入门:开启本地大型语言模型开发之旅

Ollama教程——入门&#xff1a;开启本地大型语言模型开发之旅 引言安装ollamamacOSWindows预览版LinuxDocker ollama的库和工具ollama-pythonollama-js 快速开始运行模型访问模型库 自定义模型从GGUF导入模型自定义提示 CLI参考创建模型拉取模型删除模型复制模型多行输入多模态…

越南工厂连接中国总部服务器解决方案---案例分享

随着全球化的不断深入&#xff0c;许多中国企业走出国门&#xff0c;在世界各地设立分支机构和生产基地。然而&#xff0c;随之而来的是跨国网络通信的挑战。近期&#xff0c;客户越南的工厂与中国总部之间的网络连接出现了一些问题&#xff0c;这直接影响了企业的日常运营效率…

Go-Gin中优雅的实现参数校验,自定义错误消息提示

问题描述 在参数校验的时候我们一般会基于"github.com/go-playground/validator/v10"这个库给结构体加标签实现校验参数&#xff0c;当参数校验错误的时候&#xff0c;他的提示一般是英文的&#xff0c;怎么自定义参数错误提示呢&#xff1f;跟着我一步步来 注册校…

备战蓝桥杯---贪心刷题1

话不多说&#xff0c;直接看题&#xff1a; 本质是一个数学题&#xff1a; 我们令xi<0表示反方向传递&#xff0c;易得我们就是求每一个xi的绝对值之和min,我们令平均值为a爸。 易得约束条件&#xff1a; x1-x2a1-a,x2-x3a2-a..... 解得x1x1-0,x2x1-((n-1)*a-a2-...an)。…

硬件了解 笔记

motherboard的高低端区别在哪里&#xff1f; 核心&#xff1a;从单核变成双核&#xff0c;多核&#xff08;几核就是几个打工人&#xff09; 多线程&#xff1a;6核本来对应6个线程&#xff0c;但是多线程就是说6核对应12个线程 频率 主频&#xff1a;平时打工的速度 睿频&…

达梦数据库 优化

谁进行优化&#xff1f;优化什么&#xff1f; 优化不能仅从数据库方面考虑&#xff0c;比如&#xff0c;在存储达到数据库极限、应用涉及人员设计的代码稀巴烂的情况下&#xff0c;进行调优就是杯水车薪的效果。 涉及到优化人员&#xff1a; 数据库管理员应用程序架构师应用…

Javascript/Node.JS中如何用多种方式避免属性为空(cannot read property of undefined ERROR)

>>>>>>问题 "cannot read property of undefined" 是一个常见的 JavaScript 错误&#xff0c;包含我在内很多人都会遇到&#xff0c;表示你试图访问一个未定义&#xff08;undefined&#xff09;对象的属性。这通常是因为你在访问一个不存在的对象…

制造型企业实施WMS仓储管理系统前后的变化

在科技浪潮的推动下&#xff0c;WMS仓储管理系统逐渐崭露头角&#xff0c;成为制造企业优化运营、提升竞争力的得力助手。本文将从制造企业实施WMS仓储管理系统前后的对比入手&#xff0c;探讨这一变革所带来的深远影响。 一、WMS仓储管理系统实施前的仓储管理挑战 在WMS仓储管…

【LeetCode: 96. 不同的二叉搜索树 + 动态规划】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

从0到1完成UI自动化测试框架搭建之Pytest

上篇文章中&#xff0c;我们学会了如何使用UI Automator2atx编写简单的Android自动化脚本。 但是有个问题&#xff0c;大家可以思考下&#xff0c;光用自动化脚本让它自己动起来&#xff0c;是不是缺了点什么&#xff1f; 我们写测试用例的时候&#xff0c;是不是经常写&…

redis 的StringRedisTemplate

6.3 StringRedisTemplate 尽管JSON的序列化方式可以满足我们的需求&#xff0c;但依然存在一些问题&#xff0c;如图&#xff1a; 为了在反序列化时知道对象的类型&#xff0c;JSON序列化器会将类的class类型写入json结果中&#xff0c;存入Redis&#xff0c;会带来额外的内存…

透彻理解二分查找-算法通关村

透彻理解二分查找-算法通关村 常见的查找算法有顺序查找、二分查找、插值查找&#xff0c;树表查找、分块查找、哈希查找等等。其实二分查找、插值查找以及斐波那契查找都可以归为一类—插值查找。插值查找是在二分查找的基础上的优化查找算法。 二分查找的价值&#xff0c;请…

大数据分析与内存计算——Spark安装以及Hadoop操作——注意事项

一、Spark安装 1.相关链接 https://dblab.xmu.edu.cn/blog/4322/ 2.安装Spark&#xff08;Local模式&#xff09; 按照文章中的步骤安装即可 遇到问题&#xff1a;xshell以及xftp不能使用 解决办法&#xff1a; 在linux使用镜像网站进行下载&#xff1a;wget https://mi…

Three.js真实相机模拟

有没有想过如何在 3D Web 应用程序中模拟物理相机&#xff1f; 在这篇博文中&#xff0c;我将向你展示如何使用 Three.js和 OpenCV 来完成此操作。 我们将从模拟针孔相机模型开始&#xff0c;然后添加真实的镜头畸变。 具体来说&#xff0c;我们将仔细研究 OpenCV 的两个失真模…

【Java 集合进阶】单练集合顶层接口collction迭代器

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …