本篇对应书籍第十三章的内容
本篇内容介绍了硬盘分区表的结构和存放,以及编写了简易版本的硬盘驱动程序
本篇难点:
- 要有耐心去看硬盘IO操作是如何封装的
硬盘及分区表
之前的那个虚拟磁盘用来装系统内核了,为了图省事,再整一块虚拟硬盘来用
创建从盘&获取安装的磁盘数
通过bximage
创建一个80M的硬盘,得到硬盘参数:
ata0-master: type=disk, path="hd80M.img", mode=flat
将master改成slave,填入bochsrc.disk
启动Bochs,查看内存0x475,获取当前安装的硬盘数:
BIOS 检查的时候,会把安装的硬盘数写入内存 0x475 的位置
到此,硬盘创建好安装完毕了
创建硬盘分区表
文件系统 = 数据结构 + 算法,所以说,文件系统也是软件,管理对象是文件,管辖范围是分区
关于柱面、扇区、磁道、磁头、盘面等知识之前讲过了,这里就不回顾了
硬盘容量 = 每磁道扇区数x柱面数x512字节x磁头数
分区表位于 MBR 中,只有64字节大小,每一个分区表项都占有16字节,所以最多有4个分区
为了支持任意数量分区,发明了扩展分区,通过id属性值判断分区类型
分区表中共 4个分区,哪个做扩展分区都可以,扩展分区是可选的,但最多只有1个,其余的都是主分区。在过去没有扩展分区时,这 4 个分区都是主分区;为了兼容 4 个主分区的情况,扩展分区中的第 1 个逻辑分区的编号从 5 开始。
使用fdisk进行分区:
随便分一分,一个主分区,一个扩展分区,扩展分区随便分了4份,类型都是0x66
磁盘分区表浅析
磁盘分区表,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
//打开主片上的 IR0 也就是目前只接受时钟产生的中断
outb (PIC_M_DATA, 0xf8);
outb (PIC_S_DATA, 0xbf);
lib/kernel/stdio-kernel.c
为了图输出方便,封装一个内核的printf就是printk函数
#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);
}
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_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; // 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]; // 一个通道上连接两个硬盘, 一主一从
};
#endif
device/ide.c
使用宏保存基础的内容,通过ide_init函数做一些基础工作:
#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_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//, 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);
channel_no++; // 下一个 channel
}
// 打印所有分区信息
printk("ide_init done\n");
}
到这里初始化工作就完毕了
实现 thread_yield 和 idle 线程
这里需要完成一些基础构件才能继续进行
thread_yield 的功能是主动把 CPU 使用权让出来,执行后任务状态变为 READY 然后立即重新加入就绪队列等待调度执行
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);
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的工作有三步:
- 将自己加入就绪队列
- 状态设置为READY
- 调用 schedule 进行任务调度
因为前两步必须是原子操作,所以要先关中断
为了以防线程都执行完了,导致没有任务可调度而挂起,这里创建了一个idle线程,当就绪队列中没有任务了,就会执行该线程,该线程的功能是内联汇编hlt
来让CPU真正意义上的挂起,不再执行指令,直到有中断产生,CPU再继续执行指令
实现简单的休眠函数
在等待硬盘操作的过程中,最好把CPU让出来,为了实现等待,需要定义一个休眠函数,这里实现一个简易版的休眠功能:
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) {
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.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);
// 写入扇区号
outb(reg_lba_l(channel), lba);
outb(reg_lba_m(channel), lba >> 8);
outb(reg_lba_h(channel), lba >> 16);
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) {
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) {
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;
}
这里是几个功能函数:
-
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 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);
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
// 用于记录总扩展分区的起始 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 += 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!!!!!", 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);
}
这里通过定义一些数据结构,获取到硬盘中的分区表,读取分区表中的内容,进行输出,从而得到硬盘信息
扫描分区表
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 表示是第一次读取引导块, 也就是主引导记录所在的扇区
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)
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指针里
运行 Bochs
编译,运行:
获取到了硬盘的信息
本篇总结
本篇内容介绍了硬盘分区表的存放方式,存放内容等,介绍了扩展分区是怎么扩展的
本篇内容带着实现了一套简易的硬盘驱动程序,驱动就是对底层重复操作的一个函数封装,这里封装了对硬盘的读写操作,获取硬盘信息操作和扫描分区表操作
在这期间,我们改进了内核里的一个问题,就是当任务全部都执行完毕后,没有任务执行而报错的问题,通过创建idle线程,在空闲时启动,通过内联汇编执行hlt命令让CPU真正意义上的挂起,不再执行指令,等出现中断的时候,再进行恢复
本篇还封装了一个内核版本的printf:printk 函数,方便打印字符串用
这一篇内容我学的怪烦的,感觉很没劲,全篇都在对硬盘IO进行各种封装,然后获取硬盘信息,操作硬盘,本篇的重点在于硬盘驱动程序,所以懒惰的我就直接copy代码来感受驱动程序的编写流程啦
驱动程序的编写是通过层层封装来实现的,先封装最基本的IO操作,然后根据IO操作封装出功能函数,成为驱动程序
无聊的内容到此就要结束了,真不错
参考资料
- 《操作系统真象还原》Chapter 13
- csdn博主swings_ss分享的配套代码:https://github.com/zhangwenxiao/os-core