操作系统真象还原实验记录之实验二十三:编写硬盘驱动程序

1.硬盘分区

1.1 创建Seven80.img硬盘

./bximage -mode=create  -imgmode=flat -hd=80 -q Seven80.img

1.2 查看bochs配置的硬盘数

1.3 修改bochsrc.disk

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

柱面数=162,磁头数=16,每磁道扇区数=63,总大小=79.73MB

162 x 16 x 63 x 512 =83607552字节=79.73MB

1.4 分区

1.4.1 分区及MBR简介

首先,一个分区可以理解成c盘或d盘,一个分区包含若干柱面,必须占用完整的磁道,每个分区都可以有自己的操作系统、文件系统,MBR可以选择性地加载每个分区自己专属的loader.S,然后依靠这个loader.s加载自己分区的操作系统。
所以一般来说,应该先依靠分区工具fdisk填写分区表,对硬盘进行分区,再给电脑加载操作系统,分区工具独立于任何操作系统。

1.4.2 操作流程

先查看,Disk identifier为磁盘描述符,可以理解成硬盘分区表为空,
cylinder是柱面,heads是磁头数即盘面
MBR第一个扇区前446字节是硬盘参数和引导程序,然后64字节为分区表,有四个分区表项,最后两字节是魔数55aa。
4个分区表项任意一个可以作为扩展分区。
一个分区包含若干个柱面,分区不能共享柱面

id为5表示扩展分区


修改文件系统id为66(未知),因为这是我们即将要自制的文件系统,
id=83为Linux,id=5表示扩展分区

W将分区表写入第一个磁盘那个 64字节处

上面一共有1个主分区,5个逻辑分区
分区表以及每个分区的位置,书上给了截图


首先一个磁道有63个扇区,一个分区必须占有一个完整的磁道,而MBR位于第一个扇区作为引导扇区,那么它后面的62个扇区不能划分为主分区空间,也就是浪费了。
其次MBR结构是446+64+4字节,0x1be到0x1fd这64字节才是MBR分区表,这个主分区表一共四项,我们刚刚实验填了第一项和第四项,指定了第一个表项记录主分区,第四个表项记录扩展分区,第二、三个为空,如上图所示。

第一个表项偏移扇区0x3F=63,意味着主分区从第63扇区开始,浪费了62个扇区。
第四个表项id=0x5表示扩展分区,这个偏移扇区0x7E00表示子第一个扩展分区的分区表的位置也就是EBR引导扇区位置,它和MBR引导扇区结构一毛一样。扇区数指的是所有子扩展分区总扇区数


根据0x7E00,作者来到了0x7E00 * 512字节处,依然偏移地址0x1be,不过只有两个表项,所以是0x1be到0x1dd。
第一个表项id=0x66是我们设定的逻辑分区的id,这意味着这个表项记录了五个逻辑分区的第一个分区,这个分区的地址是0X7E00+0x3F,依然是浪费了62个扇区。
第二个表项显然,又是第二个子扩展分区的分区表的指针了




第五个是最后一个子扩展分区了,所以没有指针了

综上,扩展分区可以划分无限个子扩展分区,他们的分区表是依靠链表的形式存储的

分区表表项结构(16字节)主分区共四个64字节

2.编写硬盘驱动程序

前言

第三章MBR访问硬盘中,加载了loader.s
其中loader.s预先以用dd命令写入了第二个扇区
mbr.s利用I/O指令给相应I/O端口下命令,将loader.s读到了I/O接口数据缓冲区。
利用循环程序不断检查状态寄存器,loader.s全部读完后,又开始给I/O端口下达新的命令,将loader.s从I/O接口的数据缓冲区读到内存。

本次实验不再采用程序查询方式,而采用中断处理方式。
硬盘读loader.s的时候,程序应该阻塞,然后调度执行其他程序,把CPU让出来
硬盘读工作完成后,回向8259A发送中断请求,其中Primary通道接从片IRQ14,Secondary通道接从片IRQ15。
然后中断执行中断处理程序唤醒硬盘驱动程序到就绪队列,来将loader.s读到内存。

要用的知识

我们模拟的实验,主板上只有两个IDE卡槽,可以理解为两个I/O接口,
IDE0也叫通道1也叫Primary通道,该I/O接口内的端口范围是:
命令快寄存器端口0x1F0~0x1F7
控制块寄存器端口是0x3F6

通道2即Secondary通道端口范围
命令快寄存器端口0x170~0x177
控制块寄存器端口是0x376

每个卡槽可以插两块主从硬盘。

给I/O接口发命令的时候,device寄存器第四位dev位指定是对主还是从盘操作,
但是中断信号cpu无法区分是主盘还是从盘发出的,因此加了一个通道锁,当cpu给主盘下完命令的后,只有等主盘中断请求,将中断信号处理掉,给从盘下命令才不会被锁阻塞。

8259A管理的中断,如果中断源从从片来且手动模式下的话,中断处理程序就要向主、从片发送EOI。
我们的中断处理程序除了系统调用int 0x80号中断以外,其余全部都传了,包括这次的硬盘中断处理程序,
不过,不仅仅要传EOI,中断处理程序还需要显示通知硬盘控制器中断完成,不然硬盘控制器下次不会中断请求,所以这次硬盘中断处理程序最后读取了一次端口status,清理硬盘控制器的中断。

2.1 实验流程

这次实验读取了硬盘信息,扫描了所有主分区,扩展分区
其中,获取硬盘信息要向命令寄存器端口传0xec,获得的格式如下

一个字是两字节,序列号20个字节,型号40个字节,可用扇区数2字节,每个字是小端存储,高地址位是低字节,所以取出信息到内存后,还要对每个字的两个字节进行交换。

整个流程如下:
向控制寄存器传达命令0xec后,硬盘会开始工作,将硬盘信息不断读到I/O接口的数据缓冲区,硬盘工作的时候,就需要阻塞线/进程,等硬盘工作完成后,会主动发送中断请求于8259A,中断处理程序将进/线程重新唤醒,进而线程继续将I/O接口数据缓冲区的硬盘信息读到内存,然后打印在显存。

2.2 ide.h

#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H
#include "stdint.h"
#include "sync.h"
#include "list.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还是从1struct partition prim_parts[4];    // 主分区顶多是4个struct partition logic_parts[8];     // 逻辑分区数量无限,但总得有个支持的上限,那就支持8个
};/* ata通道结构 */
struct ide_channel {char name[8];        // 本ata通道名称, 如ata0,也被叫做ide0. 可以参考bochs配置文件中关于硬盘的配置。uint16_t port_base;      // 本通道的起始端口号uint8_t irq_no;         // 本通道所用的中断号struct lock lock;bool expecting_intr;       // 向硬盘发完命令后等待来自硬盘的中断struct semaphore disk_done;     // 硬盘处理完成.线程用这个信号量来阻塞自己,由硬盘完成后产生的中断将线程唤醒struct disk devices[2];  // 一个通道上连接两个硬盘,一主一从
};void intr_hd_handler(uint8_t irq_no);
void ide_init(void);
extern uint8_t channel_cnt;
extern struct ide_channel channels[];
extern struct list partition_list;
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);
#endif

partition 分区结构体,暂时用得上前5个
内含所属disk指针。

disk 内含所属通道指针,和拥有的分区

ide_channel 内含自己拥有的两个硬盘,还有自己的锁和信号量。

注意三者的关系,到底谁拥有谁。
主板上一共有两个通道(Primary通道和Secondary通道)
通道里面有两个磁盘(主盘、从盘)
磁盘里面有自己的分区结构(4个主分区,若干的逻辑分区)

所以我们的代码只需要声明两个通道作为全局变量就ok了

2.3 ide.c

2.3.1 ide_init函数

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);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通道的起始端口号是0x1f0channel->irq_no   = 0x20 + 14;    // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号break;case 1:channel->port_base    = 0x170;     // ide1通道的起始端口号是0x170channel->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");
}/* 硬盘中断处理程序 */
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));}
}/* 打印分区信息 */
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;
}

ide_init函数就是整个实验的开头
它初始化了两个通道,注册了两个通道的中断处理程序,
然后对每个通道的两个硬盘也初始化。
其中硬盘的初始化调用了identify_disk,扫描硬盘信息,并打印
然后调用了partition_scan扫描了从盘的分区,将扫描出的信息,初始化了硬盘以及从盘的分区。
最后调用了list_traversal
遍历分区队列,打印分区信息

有一个小细节,通道名,硬盘名、分区名都是用sprintf写入的。

2.3.2 identify_disk函数

#define CMD_IDENTIFY    0xec     // identify指令/* 获得硬盘参数信息 */
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, sn_len = 20, md_start = 27 * 2, md_len = 40;swap_pairs_bytes(&id_info[sn_start], buf, sn_len);printk("   disk %s info:\n      SN: %s\n", hd->name, buf);memset(buf, 0, sizeof(buf));swap_pairs_bytes(&id_info[md_start], buf, md_len);printk("      MODULE: %s\n", buf);uint32_t sectors = *(uint32_t*)&id_info[60 * 2];printk("      SECTORS: %d\n", sectors);printk("      CAPACITY: %dMB\n", sectors * 512 / 1024 / 1024);
}/* 向通道channel发命令cmd */
static void cmd_out(struct ide_channel* channel, uint8_t cmd) {
/* 只要向硬盘发出了命令便将此标记置为true,硬盘中断处理程序需要根据它来判断 */channel->expecting_intr = true;outb(reg_cmd(channel), cmd);
}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';
}

select_disk(hd);先判断读的是主盘还是从盘信息,这关系到dev位。
然后向硬盘的I/O接口的控制寄存器发送0xec,硬盘开始工作,读出硬盘信息到I/O接口的缓冲区,读的时候利用通道信号量阻塞自己。
当硬盘读完后,发送中断请求,中断处理程序唤醒,busy_wait 判断30s内硬盘是否不忙
read_from_sector,将一个扇区的硬盘信息从缓冲区读到内存。

磁盘信息一个扇区,但用得上的只有下面三个

磁盘信息以两字节为单位,每个字采用小端存储,所以要对每个字进行字节交换处理才能得到正确磁盘信息
sn表硬盘序列号,20个字节,md表硬盘型号,40个字节
swap_pairs_bytes对sn、md交换字节,并打印
最后sector是扇区数,2个字节,c语言也是小端存储,所以不用字节交换,直接运算打印

2.3.3 partition_scan函数


struct list partition_list;  // 分区队列
/* 用于记录总扩展分区的起始lba,初始为0,partition_scan时以此为标记 */
int32_t ext_lba_base = 0;
uint8_t p_no = 0, l_no = 0;    // 用来记录硬盘主分区和逻辑分区的下标/* 构建一个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));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);
}

分区扫描

我们的分区设置前面分析了

MBR引导扇区里446字节偏移处64字节,四个分区表项
第一个表项主分区,
第四个表项扩展分区,相当于指针指向下一个逻辑分区

然后便是五个逻辑分区,每个逻辑分区都有和MBR一模一样结构的引导扇区,里面含有四个分区表项。
第一个表项是该逻辑分区的记录,第二个是下一个逻辑分区的指针。

综上,partition_scan要先把MBR读到内存,利用c语言定义结构体boot_sector 、partition_table_entry 的妙用,完成了四个分区表项的遍历。
(虽然我菜,但是我觉得非常的妙,这就是c语言编程的魅力)





显然,每一个子分区的基地址是上一个分区表项start_lba逐渐累加的,所以这个函数是递归调用的
注意:start_lba就是每个分区表项的偏移扇区。

这个递归,模拟一下程序执行流

首先我们的主盘Seven60.img用于加载内核,没有分区,dev_no=0表示主盘。所以这里扫描的是从盘Seven80.img的分区。
partition_scan(hd,0);
ide_read(hd, ext_lba, bs, 1);将Seven80.img的MBR读到了内存bs处,然后依次遍历4个分区表项。
第一个分区表项是主分区,于是执行第四个if,把硬盘的主分区数组初始化,顺便加入分区队列用于打印。
第二、三个表项无效。
第四个是总扩展分区表项,执行第一个else,
递归调用partition_scan(hd, 0x7e00);来到了第一个子扩展分区。
首先读出该引导扇区于bs处,
然后遍历4个分区表项
第一个分区表项是逻辑分区,走最后一个else
初始化hd的逻辑分区数组,顺便加入分区队列用于打印。
第二个分区是下一个子扩展分区指针,走第二个if
递归调用partition_scan(hd,0x7e00+0x46E0);
后面的就一模一样了。

2.4 硬盘驱动程序代码

上述的
identify_disk,从主、从两硬盘均读取了一个扇区的硬盘信息于内存
partition_scan,读取了从盘的MBR、EBR于内存等等。

已经用到了读磁盘I/O,读的时候要阻塞,读完要中断恢复成就绪状态。
所以需要实现读写的硬盘驱动程序

2.4.1 thread_yield函数增加于thread.c

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

将运行态程序修改成就绪态,放入就绪队列,调度切换其他线程。

2.4.2 idle函数增加于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() {
........略
/* 如果就绪队列中没有可运行的任务,就唤醒idle */if (list_empty(&thread_ready_list)) {thread_unblock(idle_thread);}
}
......略void thread_init(void) {
。。。。略
/* 将当前main函数创建为线程 */make_main_thread();/* 创建idle线程 */idle_thread = thread_start("idle", 10, idle, NULL);put_str("thread_init done\n");。。。略
}

全局PCB,idle_thread线程,一上处理机 就立刻被自己阻塞,就绪队列为空才会被唤醒为就绪态然后上处理机,执行hlt停机,开中断。

2.4.3 休眠函数于timer.c

#define IRQ0_FREQUENCY      100
#define mil_seconds_per_intr (1000 / IRQ0_FREQUENCY)
uint32_t ticks;          // ticks是内核自中断开启以来总共的嘀嗒数// 以tick为单位的sleep,任何时间形式的sleep会转换此ticks形式
static void ticks_to_sleep(uint32_t sleep_ticks) {uint32_t start_tick = ticks;while (ticks - start_tick < sleep_ticks) {       // 若间隔的ticks数不够便让出cputhread_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);
}

mtime_sleep是休眠多少秒
ticks_to_sleep是休眠多少个时钟周期。
第一次进入ticks_to_sleep,ticks赋值给start_tick,那必然直接被yeild。
当第二次被调度时,由于静态函数,start_tick不会被再次赋值,tick - start_tick就是间隔时间周期。

2.4.4 硬盘驱动程序于ide.c

上半部

/* 从硬盘读取sec_cnt个扇区到buf */
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt) {   // 此处的sec_cnt为32位大小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);             // 先将待读的块号lba地址和待读入的扇区数写入lba寄存器/* 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));}
}

ide_read
/* 从硬盘读取sec_cnt个扇区到buf */

整个流程是

  1. select_disk(hd),选择主、从盘,设置dev位。
  2. select_sector,写扇区数和起始扇区号
  3. cmd_out写读命令,硬盘开始读入缓冲区,读时阻塞。
  4. 读完,中断恢复阻塞,busy_wait(hd)查看30s内硬盘是否忙,忙说明硬盘故障
  5. read_from_sector,从缓冲区读入内存。完成数据从硬盘到I/O接口再到内存。

ide_write
/* 将buf中sec_cnt扇区数据写入硬盘 */
整个流程是

  1. select_disk(hd),选择主、从盘,设置dev位。
  2. select_sector,写扇区数和起始扇区号
  3. cmd_out写写命令,硬盘开始读入缓冲区,读时阻塞。
  4. busy_wait(hd)查看30s内硬盘是否忙,忙说明硬盘故障
  5. write2sector,将数据从数据寄存器写入缓冲区,硬盘一旦收到缓冲区数据就会立刻开始写操作,故阻塞。
  6. 写完则中断唤醒。
    (个人认为,第四步应该放在第六步后面。中断不一定是写完,可能是故障,所以busy_wait检查。第五步后,硬盘才会开始写。)

读写磁盘均要给该通道上锁,不然中断处理程序不知道唤醒主盘还是从盘。通道有了锁,那么通道的信号量的等待队列只会有一个线程,唤醒即可。

下半部
#include "ide.h"
#include "sync.h"
#include "io.h"
#include "stdio.h"
#include "stdio-kernel.h"
#include "interrupt.h"
#include "memory.h"
#include "debug.h"
#include "console.h"
#include "timer.h"
#include "string.h"
#include "list.h"/* 定义硬盘各寄存器的端口号 */
#define reg_data(channel)    (channel->port_base + 0)
#define reg_error(channel)   (channel->port_base + 1)
#define reg_sect_cnt(channel)    (channel->port_base + 2)
#define reg_lba_l(channel)   (channel->port_base + 3)
#define reg_lba_m(channel)   (channel->port_base + 4)
#define reg_lba_h(channel)   (channel->port_base + 5)
#define reg_dev(channel)     (channel->port_base + 6)
#define reg_status(channel)  (channel->port_base + 7)
#define reg_cmd(channel)     (reg_status(channel))
#define reg_alt_status(channel)  (channel->port_base + 0x206)
#define reg_ctl(channel)     reg_alt_status(channel)/* reg_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硬盘/* 选择读写的硬盘 */
static void select_disk(struct disk* hd) {uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;if (hd->dev_no == 1) {   // 若是从盘就置DEV位为1reg_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);
}/* 硬盘读入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;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;
}

2.4.5 interrupt.c修改

static void pic_init(void){
...略
outb(PIC_M_DATA,0xf8);
outb(PIC_S_DATA,0xbf);。。。略}

打开时钟IRQ0,IRQ1键盘,IRQ2级联,IRQ14硬盘。

2.4.6 stdio_kernel.c

#include "stdio-kernel.h"
#include "print.h"
#include "stdio.h"
#include "console.h"
#include "global.h"#define va_start(args, first_fix) args = (va_list)&first_fix
#define va_end(args) args = NULL/* 供内核使用的格式化输出函数 */
void printk(const char* format, ...) {va_list args;va_start(args, format);char buf[1024] = {0};vsprintf(buf, format, args);va_end(args);console_put_str(buf);
}

2.4.7 stdio_kernel.h

#ifndef __LIB_KERNEL_STDIOSYS_H
#define __LIB_KERNEL_STDIOSYS_H
#include "stdint.h"
void printk(const char* format, ...);
#endif

3.实验结果


这次的main.c只有init_all()
也就是只创建了主线程以及idle线程

主线程负责加载内核,初始化一切,还有本次实验的读取磁盘信息及分区并打印。

而idle线程一上处理机就把自己阻塞,
等到主线程读磁盘的时候,主线程将自己阻塞,此时就绪队列为空,idle被唤醒到就绪队列,然后立刻被调度上处理机,执行hlt。然后被时钟中断唤醒,停止hlt,执行hlt下一条指令,及又阻塞自己,然后调度的时候就绪队列还是为空,就又被唤醒。直到磁盘读操作完成,主线程被唤醒,就绪队列才不会为空。

值得注意的是,本次实验busy_wait不会唤醒idle。
因为busy_wait核心是thread_yield,它会将主线程改为就绪态再调度就绪队列其他线程,然而就绪队列就这一个主线程,所以它立刻又被调度上处理机,然后又将自己改为就绪态,调度就绪队列其他线程,又调度到自己。
所以如果status的busy位一直为忙,那么cpu将一直被消耗在调度切换函数中,并没有hlt休息。

操作系统真象还原实验记录之实验二十三:硬盘分区,并编写硬盘驱动程序相关推荐

  1. 操作系统真象还原实验记录之实验十一:实现中断处理(二)

    操作系统真象还原实验记录之实验十一:实现中断处理(二) 书p335 7.6.2 改进中断处理程序,并调快时钟 1.实验代码第一次修改 对应 书p335 7.6.2 改进中断处理程序 这次是上一次实验的 ...

  2. 操作系统真象还原实验记录之实验十二:实现ASSERT

    操作系统真象还原实验记录之实验十二:实现ASSERT,通过makefile完成编译 对应书P367 第8.2节 1.相关基础知识 见书 2.实验代码 完成了开关中断函数.实现assert断言函数用于调 ...

  3. 操作系统真象还原实验记录之实验七:加载内核

    操作系统真象还原实验记录之实验七:加载内核 对应书P207 1.相关基础知识总结 1.1 elf格式 1.1.1 c程序如何转化成elf格式 写好main.c的源程序 //main.c int mai ...

  4. 操作系统真象还原实验记录之实验六:内存分页

    操作系统真象还原实验记录之实验五:内存分页 对应书P199页 5.2 1.相关基础知识总结 页目录 页目录项 页表 页表项 物理页 虚拟地址 物理地址 概念略 页目录项及页表项 低12位都是属性.高2 ...

  5. 操作系统真象还原实验记录之实验一:第一次编写mbr

    操作系统真象还原之实验一:第一次编写mbr 对应书中第2.3节:让mbr飞一会 第58页 1.相关基础知识提炼总结 1.1电脑开机前与后 在电脑未开机前,BIOS就被事先写入到内存的F0000~FFF ...

  6. 操作系统真象还原实验记录之实验三十四:实现管道

    操作系统真象还原实验记录之实验三十四:实现管道 1.管道相关知识总结 先说我们操作系统的管道实现: 上述图中,管道缓冲区就是一页内存,这一页内存被我们当成了环形缓冲区结构, 当这页管道被创建出来后,全 ...

  7. 《操作系统真象还原》1-3章 学习记录

    文章目录 前言 一.开始实验前的一些基本问题解答? section的含义? vstart的含义? $ 和 $$区别? 实模式的特点? CPU如何和硬盘进行交互? CPU和IO设备交互方式? 程序载入内 ...

  8. 《操作系统真象还原》第十四章 ---- 实现文件系统 任务繁多 饭得一口口吃路得一步步走啊(上二)

    文章目录 专栏博客链接 相关查阅博客链接 本书中错误勘误 闲聊时刻 部分缩写熟知 实现文件描述符的原理 文件描述符的介绍 文件描述符与inode的介绍 文件描述符与PCB的描述符数组的介绍 实现文件操 ...

  9. 《操作系统真象还原》从零开始自制操作系统 全流程记录

    文章目录 前引 章节博客链接 实现源码链接 前引 这本<操作系统真象还原>里面一共有十五个章节 大约760页 这些系列博客也是我在做完哈工大操作系统Lab之后 觉得还是有些朦朦胧胧 毅然决 ...

最新文章

  1. java hashmap 转对象_Java – 从HashMap中获取正确的转换对象
  2. 【SRIO】4、Xilinx RapidIO核详解
  3. 南京林业大学计算机专升本,2018江苏专转本学校之:南京林业大学
  4. python是c语言吗-初学者python和c语言先学哪个好呢?
  5. Python_堆栈和队列
  6. 树形结构 —— 树与二叉树 —— 树的直径
  7. 基于JAVA+SpringMVC+Mybatis+MYSQL的网络投票系统
  8. DSO的记录模式Record Mode字段测试
  9. 你理解这些Cisco NAT分类和原理吗
  10. 用 Tenorshare ReiBoot修复iPhone无法开机
  11. C++--第5课 - 新的关键字
  12. WPF Visifire 图表控件
  13. Windows中使用Netsh Winsock Reset命令解决网络连接问题
  14. unity3D人物模型
  15. Windows XP SP3 Beta版本(KB936929)已经发布到Connect
  16. 唯冠为何赢了苹果却不付律师费
  17. php怎么将农历转换成公历,PHP 实现公历日期与农历日期的互转换
  18. 关于 C++ 打印 PDF 打印及 PDF 转图片、合并
  19. win7 安装openssh_05、Win7上openSSH的安装与配置
  20. 中国不承认国际驾驶证

热门文章

  1. 用c++写一百以内的质数
  2. Mega-wechat微信模板消息发送服务
  3. 滤波时选用电感,电容值的方法
  4. 计算机英语·总篇·A-Z
  5. 显示标题栏中标题左侧的小图icon
  6. 免费常用API大全,程序员必备
  7. cockroachdb_CockroachDB评论:分布式SQLSwift发展
  8. 为什么重写equals方法必须重写hashcode方法
  9. 获取Android手机MAC的一些方法
  10. android 搜索栏滑动跟随缩放和移动,缩放变形问题