《操作系统真象还原》第十三章

《操作系统真象还原》第十三章

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

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

硬盘及分区表

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

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

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

ata0-slave: type=disk, path="hd80M.img", mode=flat,cylinders=162,heads=16,spt=63

将master改成slave,填入bochsrc.disk

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

在这里插入图片描述

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

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

创建硬盘分区表

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

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

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

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

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

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

使用fdisk进行分区:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可能由于fdisk命令版本不同,我这里需要通过fdisk -help设置参数后才与上面的过程一致,需要在fdisk命令中手动指定以下两项。

在这里插入图片描述

[(46条消息) 操作系统真象还原13章/一]-创建从盘并分区_Y4Snail的博客-CSDN博客_操作系统真相还原硬盘分区

fdisk -c="dos" -u="cylinders"  ./hd80M.img

磁盘分区表浅析

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

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

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

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

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

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

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

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

分区表项结构如图所示:

在这里插入图片描述

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

编写硬盘驱动程序

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

硬盘初始化

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

kernel/interrupt.c
// IRQ2用于级联从片, 必须打开, 否则无法响应从片上的中断
// 主片上打开的中断有IRQ0的时钟, IRQ1的键盘和级联从片的IRQ2, 其它全部关闭
outb(PIC_M_DATA, 0xf8);

// 打开从片上的IRQ14, 此引脚接收硬盘控制器的中断
outb(PIC_S_DATA, 0xbf);
lib/kernel/stdio-kernel.h
#ifndef __LIB_KERNEL_STDIOSYS_H
#define __LIB_KERNEL_STDIOSYS_H

#include "stdint.h"

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

#endif
lib/kernel/stdio-kernel.c

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

#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);
}
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_lab;             // 起始扇区
    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
device/ide.c

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

#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");
}

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

实现 thread_yield 和 idle 线程

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

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

global.h
// 通常,如果声明了某个变量,但从未对其进行引用,编译器将发出警告。此属性指示编译器您预计不会使用某个变量,并指示它在未使用该变量	 时不要发出警告
#define UNUSED __attribute__ ((unused))		// 表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息
thread/thread.h
/* 主动让出 cpu, 换其它线程运行 */
void thread_yield(void);
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);
    // // 将当前线程状态设置为 TASK_READY, 之后进行 CPU 重新调度
    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.h
void mtime_sleep(uint32_t m_seconds);
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次,所以每10毫秒发生1次中断

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

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

完善硬盘驱动程序上

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

device/ide.h
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);
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 函数:选择待操作的硬盘是主盘还是从盘

    利用 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 read 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() {
...

        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
uint8_t channel_cnt;                // 按硬盘数计算的通道数
struct ide_channel channels[2];     // 有两个 ide 通道

// 用于记录总扩展分区的起始 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++) {
        // 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);
}

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

attribute ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。这个功能是跟操作系统没关系,跟编译器有关。

扫描分区表

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 使用了 sys_malloc 进行内存申请堆空间进行存放,通过递归获取指定硬盘中的每一个分区表,将分区表信息存储在参数中给定的hd指针里

init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
#include "../thread/thread.h"
#include "../device/console.h"
#include "../device/keyboard.h"
#include "../userprog/tss.h"
#include "../userprog/syscall-init.h"
#include "../device/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();         // 初始化硬盘
}

ide.c完整代码:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行 Bochs

编译,运行:

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


############ C 代码编译 ##############
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h	\
					 lib/stdint.h kernel/init.h lib/string.h \
					 lib/kernel/stdio-kernel.h
	$(CC) $(CFLAGS) $< -o $@


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


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


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


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


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


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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


.PHONY: mk_dir hd clean all

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

hd:
	sudo dd if=$(BUILD_DIR)/kernel.bin \
            of=/home/steven/source/os/bochs/hd60M.img \
            bs=512 count=200 seek=9 conv=notrunc

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

build: $(BUILD_DIR)/kernel.bin

all: mk_dir build hd

在这里插入图片描述

获取到了硬盘的信息

本篇总结

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值