操作系统 真象还原 学习笔记#13

第十三章 编写硬盘驱动程序

1.硬盘及分区表

创建一个从盘

复制代码
bin/bximage

依次输入以下命令

复制代码
1
hd
flat
80
hd80M.img

如何删除磁盘文件?

复制代码
rm ~/Desktop/bochs/hd80M.ing

接下来,在bochsrc.disk文件挂载从盘

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

查看从盘是否挂载成功

复制代码
<bochs:2> xp/b 0x475
[bochs]:
0x00000475 <bogus+       0>:	0x02

查看0x475处一字节的信息,bochs会扫描整个环境有多少个磁盘,把个数写在这个位置,可以看到结果是2,说明挂载成功了

现在进行分区,分区的本质是将多个连续的柱面划分为一个区域。

盘片:类似光盘中的一个圆盘,上面布满了磁性介质。

扇区:扇区是硬盘读写的基本单位,它在磁道上均匀分布,与磁头和磁道不同,扇区从1 开始编号。

磁道:盘片上的一个个同心圈就是磁道,它是扇区的载体,每一个磁道由外向里从0 开始编号。

磁头:可以粗略理解为磁带中的磁头。

柱面:硬盘是整个计算机系统中很大的瓶颈了,如何才能让硬盘的读写更快,工程师们想到了并行写

入的方式

分区:是由多个编号连续的柱面组成的,因此分区在物理上的表现是由某段范围内的所有柱面组成的通心环,并不是像"饼图"那种逻辑表示,当然若整个硬盘只有1 个分区,那这个分区就是个所有柱面组

成的圆柱体。

使用fdisk工具来实现分区

复制代码
fdisk ./hd80M.img

然后在输入框依次输入以下,输入一个,按一次回车

复制代码
m 显示菜单

x 使用额外功能

m 显示菜单

c 设定柱面

162

h 设定磁头数

16

r 返回上一级菜单

n 新增一个分区

p 分区是个主分区

1 分区号设定为1

2048

33263 作者指定了1分区结束柱面为32,计算得结束扇区为(32 + 1)* 16 * 63 - 1 = 33263,-1 是因为扇区从0开始编号

n 新增一个分区

e 分区是个扩展分区。最初磁盘只支持最多分4个区。但是后来由于需求增加,磁盘需要支持能分更多区,同时又为了能够兼容旧有的最多支持4个分区,所以就发明了扩展分区。扩展分区并不直接用于存储数据,而是作为一个容器,可以创建多个逻辑分区在其中。这样,即使主分区的数量限制为四个,我们仍然可以在一个扩展分区中创建多个逻辑分区,从而实现对更多分区的需求。一个磁盘,最多一个扩展分区。

4 分区号码设定为4

33264 分区起始扇区设定为分区1结束扇区下一个扇区 /4号分区紧挨着1号分区

163295 分区结束扇区设定为整个磁盘最后一个扇区,也就是说我们这个磁盘就两个分区,一个主分区1,一个扩展分区4

p 查看现有分区

n 创建分区,由于我们现在磁盘已经被两个分区占满了,所以不能再支持新的主分区创建,又由于扩展分区只能有1个,所以不能再支持扩展分区的创建。所以此时输入n,直接是创建逻辑分区(扩展分区再次分区后,每个分区叫做逻辑分区)

35312 直接设定工具允许的逻辑分区起始最小扇区

51407 作者指定了逻辑分区5结束柱面为50,计算得结束扇区为(50 + 1)* 16 * 63 - 1 = 51407,-1 是因为扇区从0开始编号

n 创建分区

53456 直接设定工具允许的逻辑分区起始最小扇区

76607 作者指定了逻辑分区6结束柱面为75,计算得结束扇区为(75 + 1)* 16 * 63 - 1 = 76607,-1 是因为扇区从0开始编号

n 创建分区

78656 直接设定工具允许的逻辑分区起始最小扇区

91727 作者指定了逻辑分区7结束柱面为90,计算得结束扇区为(90 + 1)* 16 * 63 - 1 = 91727,-1 是因为扇区从0开始编号

n 创建分区

93776 直接设定工具允许的逻辑分区起始最小扇区

121967 作者指定了逻辑分区8结束柱面为120,计算得结束扇区为(120 + 1)* 16 * 63 - 1 = 121967,-1 是因为扇区从0开始编号

n 创建分区

124016 直接设定工具允许的逻辑分区起始最小扇区

163295 设定最后一个可用扇区为逻辑分区9的结束扇区

p 显示分区

t 设定分区类型id

5 改变逻辑分区5的类型id

66 设定类型id为0x66

t 设定分区类型id

6 改变逻辑分区6的类型id

66 设定类型id为0x66

t 设定分区类型id

7 改变逻辑分区7的类型id

66 设定类型id为0x66

t 设定分区类型id

8 改变逻辑分区8的类型id

66 设定类型id为0x66

t 设定分区类型id

9 改变逻辑分区9的类型id

66 设定类型id为0x66

p 显示分区

w 将分区表写入磁盘,并退出fdisk 分区表是用于描述每个分区的信息,详见P571
  • 分区布局表

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

2.编写硬盘驱动程序

实现一个类似于用户态函数printf的内核态函数printk

复制代码
#include "stdio-kernel.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);
}

再实现一个用于将格式化字符串放入缓冲区的函数sprintf,同printf相比,它不将信息打印在屏幕上,而是放入缓冲区中。

复制代码
/* 同printf不同的地方就是字符串不是写到终端,而是写到buf中 */
uint32_t sprintf(char* buf, const char* format, ...) {
   va_list args;
   uint32_t retval;
   va_start(args, format);
   retval = vsprintf(buf, format, args);
   va_end(args);
   return retval;
}

2.编写硬盘驱动程序

硬件是实实在在的东西,要想在软件中管理它们,只能从逻辑上抓住这些硬件的特性,将它们抽象成一些数据结构,然后这些数据结构便代表了硬件,用这些数据结构来组织硬件的信息及状态,在逻辑上硬件就是这数据结构。

硬盘上有两个ata 通道,也称为IDE 通道。第1 个ata 通道上的两个硬盘(主和从)的中断信号挂在8259A 从片的IRQ14 上,是两个硬盘共享同一个IRQ接口

我们的两个磁盘都是挂在了IDE通道0上,而IDE通道0又是挂在了IRQ14线上,所以我们只需要再打开这条线的中断信号就行

打开中断信号

复制代码
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {

   /* 初始化主片 */
   outb (PIC_M_CTRL, 0x11);   // ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_M_DATA, 0x20);   // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
   outb (PIC_M_DATA, 0x04);   // ICW3: IR2接从片. 
   outb (PIC_M_DATA, 0x01);   // ICW4: 8086模式, 正常EOI

   /* 初始化从片 */
   outb (PIC_S_CTRL, 0x11);	// ICW1: 边沿触发,级联8259, 需要ICW4.
   outb (PIC_S_DATA, 0x28);	// ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
   outb (PIC_S_DATA, 0x02);	// ICW3: 设置从片连接到主片的IR2引脚
   outb (PIC_S_DATA, 0x01);	// ICW4: 8086模式, 正常EOI

 
   outb (PIC_M_DATA, 0xf8);    //IRQ2用于级联从片,必须打开,否则无法响应从片上的中断主片上打开的中断有IRQ0的时钟,IRQ1的键盘和级联从片的IRQ2,其它全部关闭
   outb (PIC_S_DATA, 0xbf);    //打开从片上的IRQ14,此引脚接收硬盘控制器的中断 

   put_str("   pic_init done\n");
}

和硬盘相关的数据结构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;	 // 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;		 // 本通道的起始端口号(书p126)
    uint8_t irq_no;		 // 本通道所用的中断号
    struct lock lock;		 // 通道锁
    bool expecting_intr;		 // 表示等待硬盘的中断
    struct semaphore disk_done;	 // 用于阻塞、唤醒驱动程序
    struct disk devices[2];	 // 一个通道上连接两个硬盘,一主一从
};


#endif

初始化硬盘

复制代码
#include "stdint.h"       // 包含标准整数类型定义
#include "global.h"       // 包含全局变量和函数声明
#include "ide.h"          // 包含IDE硬盘相关的定义和函数声明
#include "debug.h"        // 包含调试相关的宏和函数
#include "sync.h"         // 包含同步机制相关的定义和函数
#include "stdio.h"        // 包含标准输入输出函数
#include "stdio-kernel.h" // 包含内核模式下的输入输出函数
#include "interrupt.h"    // 包含中断处理相关的定义和函数
#include "memory.h"       // 包含内存管理相关的定义和函数
#include "debug.h"        // 包含调试相关的宏和函数(重复包含可能是为了确保调试宏的定义)

/* 定义硬盘各寄存器的端口号,见书p126 */
#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)  // LBA低字节寄存器
#define reg_lba_m(channel) (channel->port_base + 4)  // LBA中字节寄存器
#define reg_lba_h(channel) (channel->port_base + 5)  // LBA高字节寄存器
#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寄存器的一些关键位,见书p128 */
#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   // 指定为LBA寻址方式
#define BIT_DEV_DEV 0x10   // 指定主盘或从盘,DEV位为1表示从盘,为0表示主盘

/* 一些硬盘操作的指令 */
#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); // 断言硬盘数量大于0
    channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
    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; // 从片8259a上倒数第二的中断引脚,也就是ide0通道的中断向量号
            break;
        case 1:
            channel->port_base = 0x170; // ide1通道的起始端口号是0x170
            channel->irq_no = 0x20 + 15; // 从8259A上的最后一个中断引脚,用来响应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(闲逛)线程,用于在就绪队列为空的时候运行。之前没有出现悬停的情况,是因为主线程会被一直不断加入就绪队列,就绪队列不会为空

修改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;     // 重新将当前线程的ticks再重置为其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清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
   thread_tag = list_pop(&thread_ready_list);   
   struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
   next->status = TASK_RUNNING;
   process_activate(next); //激活任务页表
   switch_to(cur, next);   
}


/* 初始化线程环境 */
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线程 */
   idle_thread = thread_start("idle", 10, idle, NULL);
   put_str("thread_init done\n");
}

写一个thread_yield函数,就是用于把CPU让出来

复制代码
/* 主动让出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);
}

实现定时让出cpu函数

复制代码
#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   1秒= 1000毫秒 */
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); 
}                                                      

硬盘操作方法分成七步,这也是mbr代码使用硬盘的原理

(1) 先选择通道,往该通道的 sector count寄存器中写入待操作的扇区数。

(2) 往该通道上的三个LBA 寄存器写入扇区起始地址的低 24位。

(3) 往 device 寄存器中写入 LBA 地址的 24~27位,并置第6位为1,使其为LBA 模式,设置第 4位,选择操作的硬盘

(4) 往该通道上的command寄存器写入操作命令。

(5) 读取该通道上的 status寄存器,判断硬盘工作是否完成。

(6) 如果以上步骤是读硬盘,进入下一个步骤。否则,完工。

(7 )将硬盘数据读出。

实现驱动函数的主体部分

复制代码
#include "io.h"
#include "timer.h"

/* 选择读写的硬盘 */
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地址(即扇区号) */
   outb(reg_lba_l(channel), lba);		 // lba地址的低8位,不用单独取出低8位.outb函数中的汇编指令outb %b0, %w1会只用al。
   outb(reg_lba_m(channel), lba >> 8);		 // lba地址的8~15位
   outb(reg_lba_h(channel), lba >> 16);		 // lba地址的16~23位

   /* 因为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 b (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) {
        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);
    channel_cnt = DIV_ROUND_UP(hd_cnt, 2);	   // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
    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;	   // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
            break;
        case 1:
            channel->port_base	 = 0x170;	   // ide1通道的起始端口号是0x170
            channel->irq_no	 = 0x20 + 15;	   // 从8259A上的最后一个中断引脚,我们用来响应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");
}
  1. 选择硬盘 (select_disk):

    设置硬盘控制器的设备寄存器,选择要操作的硬盘(主盘或从盘)。

  2. 选择扇区 (select_sector):

    • 设置要读写的扇区数和起始扇区地址。
  3. 发送命令 (cmd_out):

    • 向硬盘控制器发送命令,并设置标记以等待中断。
  4. 从硬盘读取数据 (read_from_sector):

    • 从硬盘读取指定数量的扇区数据到缓冲区。
  5. 向硬盘写入数据 (write2sector):

    • 将缓冲区中的数据写入硬盘的指定扇区。
  6. 等待硬盘响应 (busy_wait):

    • 等待硬盘完成操作,检查硬盘状态寄存器。
  7. 读操作 (ide_read):

    • 通过多次调用 select_sectorcmd_outread_from_sector 实现从硬盘读取多个扇区的数据。
  8. 写操作 (ide_write):

    • 通过多次调用 select_sectorcmd_outwrite2sector 实现向硬盘写入多个扇区的数据。
  9. 中断处理程序 (intr_hd_handler):

    • 处理硬盘中断,唤醒等待的线程,并读取状态寄存器以确认中断已处理。
  10. 硬盘初始化 (ide_init):

    • 初始化硬盘数据结构,设置每个IDE通道的端口基址和中断向量,注册中断处理程序

现在,我们来验证驱动程序能够运行,我们用它来:1、发送identify命令给硬盘来获取硬盘信息;2、扫描分区表;

复制代码
#include "string.h"

/* 用于记录总扩展分区的起始lba,初始为0,partition_scan时以此为标记 */
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);

  // 定义一个字符数组 buf,长度为 64
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 开始的 sn_len 个字节交换后存储到 buf 中
swap_pairs_bytes(&id_info[sn_start], buf, sn_len);

// 打印磁盘信息,包括磁盘名称和序列号
printk("   disk %s info:\n      SN: %s\n", hd->name, buf);

// 将 buf 清零
memset(buf, 0, sizeof(buf));

// 调用函数 swap_pairs_bytes,将 id_info 中从 md_start 开始的 md_len 个字节交换后存储到 buf 中
swap_pairs_bytes(&id_info[md_start], buf, md_len);

// 打印模块信息
printk("      MODULE: %s\n", buf);

// 从 id_info 中读取扇区数,并存储到变量 sectors 中
uint32_t sectors = *(uint32_t*)&id_info[60 * 2];

// 打印扇区数
printk("      SECTORS: %d\n", sectors);

// 打印容量信息,单位为 MB
printk("      CAPACITY: %dMB\n", sectors * 512 / 1024 / 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);
    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);	    // 0,1,2,3
            } 
            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);

    /* 在此处return 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);
    channel_cnt = DIV_ROUND_UP(hd_cnt, 2);	   // 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道
    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;	   // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号
            break;
        case 1:
            channel->port_base	 = 0x170;	   // ide1通道的起始端口号是0x170
            channel->irq_no	 = 0x20 + 15;	   // 从8259A上的最后一个中断引脚,我们用来响应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);

        /* 分别获取两个硬盘的参数及分区信息 */
        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;			  	   // 将硬盘驱动器号置0,为下一个channel的两个硬盘初始化。
        channel_no++;				   // 下一个channel
    }

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

完成ide的初始化

复制代码
#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();   // 初始化系统调用
   ide_init();	     // 初始化硬盘
}

修改测试函数main.c其实就是在进程创建前加一个while(1)循环

复制代码
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"

void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);

int main(void) {
   put_str("I am kernel\n");
   init_all();
   while(1);
   process_execute(u_prog_a, "u_prog_a");
   process_execute(u_prog_b, "u_prog_b");
   thread_start("k_thread_a", 31, k_thread_a, "I am thread_a");
   thread_start("k_thread_b", 31, k_thread_b, "I am thread_b");
   while(1);
   return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {     
   void* addr1 = sys_malloc(256);
   void* addr2 = sys_malloc(255);
   void* addr3 = sys_malloc(254);
   console_put_str(" thread_a malloc addr:0x");
   console_put_int((int)addr1);
   console_put_char(',');
   console_put_int((int)addr2);
   console_put_char(',');
   console_put_int((int)addr3);
   console_put_char('\n');

   int cpu_delay = 100000;
   while(cpu_delay-- > 0);
   sys_free(addr1);
   sys_free(addr2);
   sys_free(addr3);
   while(1);
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {     
   void* addr1 = sys_malloc(256);
   void* addr2 = sys_malloc(255);
   void* addr3 = sys_malloc(254);
   console_put_str(" thread_b malloc addr:0x");
   console_put_int((int)addr1);
   console_put_char(',');
   console_put_int((int)addr2);
   console_put_char(',');
   console_put_int((int)addr3);
   console_put_char('\n');

   int cpu_delay = 100000;
   while(cpu_delay-- > 0);
   sys_free(addr1);
   sys_free(addr2);
   sys_free(addr3);
   while(1);
}

/* 测试用户进程 */
void u_prog_a(void) {
   void* addr1 = malloc(256);
   void* addr2 = malloc(255);
   void* addr3 = malloc(254);
   printf(" prog_a malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);

   int cpu_delay = 100000;
   while(cpu_delay-- > 0);
   free(addr1);
   free(addr2);
   free(addr3);
   while(1);
}

/* 测试用户进程 */
void u_prog_b(void) {
   void* addr1 = malloc(256);
   void* addr2 = malloc(255);
   void* addr3 = malloc(254);
   printf(" prog_b malloc addr:0x%x,0x%x,0x%x\n", (int)addr1, (int)addr2, (int)addr3);

   int cpu_delay = 100000;
   while(cpu_delay-- > 0);
   free(addr1);
   free(addr2);
   free(addr3);
   while(1);
}

最后结果:

本章小结

本章主要实现了硬盘驱动,实际上就是对实现磁盘硬件交互的高度封装。首先在主盘的基础上增加了从盘,然后建立磁盘分区表,最后一步步建立磁盘驱动,为下一章文件系统做铺垫

相关推荐
车载测试工程师2 小时前
CAPL学习-CAN相关函数-概述
网络协议·学习·capl·canoe
roman_日积跬步-终至千里2 小时前
【人工智能导论】08-学习-如何让计算机理解序列数据——用RNN/LSTM建模时序依赖,用文本嵌入表示序列元素
人工智能·rnn·学习
m0_689618282 小时前
30 分钟打印!多材料3D打印软机器人内置驱动 + 自主避障
笔记·学习·机器人
charlie1145141913 小时前
现代嵌入式C++教程:C++98——从C向C++的演化(2)
c语言·开发语言·c++·学习·嵌入式·教程·现代c++
Rousson3 小时前
硬件学习笔记--93 静电防护方案(电阻、磁珠、电感、TVS等)
笔记·单片机·学习
思成不止于此3 小时前
【MySQL 零基础入门】事务精讲(二):ACID 特性与并发问题
数据库·笔记·学习·mysql
happyhappy没有句号4 小时前
嵌入式单片机一套通关学习笔记
笔记·单片机·嵌入式硬件·学习
悠哉悠哉愿意4 小时前
【嵌入式学习笔记】工程模板建立
笔记·嵌入式硬件·学习
d111111111d4 小时前
STM32外设基地址与寄存器偏移地址的深度解析
笔记·stm32·单片机·嵌入式硬件·学习