《操作系统真象还原》第十三章
本篇对应书籍第十三章的内容
本篇内容介绍了硬盘分区表的结构和存放,以及编写了简易版本的硬盘驱动程序
硬盘及分区表
之前的那个虚拟磁盘用来装系统内核了,为了图省事,再整一块虚拟硬盘来用
创建从盘&获取安装的磁盘数
通过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的工作有三步:
- 将自己加入就绪队列
- 状态设置为READY
- 调用 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真正意义上的挂起,不再执行指令,等出现中断的时候,再进行恢复