配合视频学习更佳!
第一节:https://www.bilibili.com/video/BV1Qj41197ut/?vd_source=701807c4f8684b13e922d0a8b116af31
第二节:https://www.bilibili.com/video/BV18u411H7T9/?vd_source=701807c4f8684b13e922d0a8b116af31
第三节:https://www.bilibili.com/video/BV1j14y1B7Pd/?vd_source=701807c4f8684b13e922d0a8b116af31
第四节:https://www.bilibili.com/video/BV1EN411h7x1/?vd_source=701807c4f8684b13e922d0a8b116af31
代码仓库:https://github.com/xukanshan/the_truth_of_operationg_system
本章是编写磁盘驱动,也就是用于与磁盘打交道(比如读、写)的程序。未来我们实现文件系统是放在磁盘上的,所以编写磁盘驱动必不可少。
现在我们的bochs内有一个hd60M.img,这是我们的主盘,里面放着我们的操作系统。为了省事,我们再创建一个磁盘,作为从盘,用于安装未来的文件系统。如同我们之前第一章创建主盘时一样,在bochs目录下:
创建磁盘
bin/bximage
然后在输入框依次输入以下,输入一个,按一次回车
1
hd
flat
80
hd80M.img
接下来,我们在bochsrc.disk文件中,写入
ata0-slave: type=disk, path="hd80M.img", mode=flat,cylinders=162,heads=16,spt=63
这样,bochs虚拟机启动时,就会识别这个磁盘并且自动挂载。
现在,我们来为这个新的磁盘进行分区,这是为了之后的文件系统做准备。分区的本质,就是将多个连续的柱面(详见书P123)划分为一个区域。
在bochs目录下,输入:
fdisk ./hd80M.img
来使用fdisk工具来为刚刚创建的磁盘开始进行分区。
然后在输入框依次输入以下,输入一个,按一次回车
m
显示菜单
x
使用额外功能
m
显示菜单
c
设定柱面
162
h
设定磁头数
16
r
返回上一级菜单
n
新增一个分区
p
分区是个主分区
1
分区号设定为1
2048
在旧的fdisk版本中,通常使用"柱面"(Cylinders)、“磁头”(Heads)和"扇区"(Sectors)这些术语来描述磁盘布局。现代磁盘和磁盘管理工具通常直接以扇区为单位进行操作。作者的1分区起始柱面是1,计算得起始扇区应该是63 * 16 = 1008,我们创建hd80M.img时,会提示Creating hard disk image 'hd80M.img' with CHS=162/16/63
,意思就是柱面162个,磁头16个,每个磁道63个扇区。但是由于fdisk工具指定第一个分区最小起始扇区为2048,所以我们只能设定1分区起始扇区为2048
33263
作者指定了1分区结束柱面为32,计算得结束扇区为(32 + 1)* 16 * 63 - 1 = 33263,-1 是因为扇区从0开始编号
n
新增一个分区
e
分区是个扩展分区。最初磁盘只支持最多分4个区。但是后来由于需求增加,磁盘需要支持能分更多区,同时又为了能够兼容旧有的最多支持4个分区,所以就发明了扩展分区。扩展分区并不直接用于存储数据,而是作为一个容器,可以创建多个逻辑分区在其中。这样,即使主分区的数量限制为四个,我们仍然可以在一个扩展分区中创建多个逻辑分区,从而实现对更多分区的需求。一个磁盘,最多一个扩展分区。
4
分区号码设定为4
33264
分区起始扇区设定为分区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
fdisk -l hd80M.img
查看分区信息
至此,我们创建分区就完毕了。我们创建的分区布局如下图:
这里面有几个要强调的点:
1、子拓展分区在逻辑上等同于一整块硬盘,也就是说,我们把总拓展分区划分成了多块硬盘;
2、MBR分区表共有4项,用于标识4个分区的起始扇区与大小,只不过我们只创建了一个分区,另一分区用作总扩展分区。而EBR分区表也有4项,第一项用于表示这个子拓展分区的逻辑分区偏移与大小,第二项用于表示下一个子拓展分区的偏移与大小,第3项与第4项没有使用,所以说一个子拓展分区就一个逻辑分区
在以前,我们内核态下进行打印一直用的console_put_xxx之类的函数,这很不方便,因为我们经常打印信息需要调用console_put_int, console_put_str, console_put_ch这三个函数配合使用。所以我们先来实现一个类似于用户态函数printf的内核态函数printk
(myos/lib/kernel/stdio-kernel.c)
#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);
}
函数声明(myos/lib/kernel/stdio-kernel.h)
#ifndef __LIB_KERNEL_STDIOSYS_H
#define __LIB_KERNEL_STDIOSYS_H
void printk(const char* format, ...);
#endif
再实现一个用于将格式化字符串放入缓冲区的函数sprintf,同printf相比,它不将信息打印在屏幕上,而是放入缓冲区中。
修改(myos/lib/stdio.c)
/* 同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;
}
添加函数声明,修改(myos/lib/stdio.h)
uint32_t sprintf(char* buf, const char* format, ...);
接下来我们正式编写硬盘驱动
首先,我们需要做的是打开硬盘对应的中断信号,主板上有两个IDE通道,每个通道可以挂载两个硬盘。第一个IDE通道的中断信号通过8259A从片的IRQ14线进入,而第二个IDE通道的中断信号通过8259A从片的IRQ15线进入(虽然下面的图上写的是保留)
由于我们的两个磁盘都是挂在了IDE通道0上,而IDE通道0又是挂在了IRQ14线上,所以我们只需要再打开这条线的中断信号就行
修改(myos/kernel/interrupt.c)
/* 初始化可编程中断控制器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");
}
硬盘是真实存在的硬件,我们要想在软件中管理它们,只能从逻辑上抓住这些硬件的特性,将它们抽象成一些数据结构,用这些数据结构来组织硬件的信息及状态,我们管理这些数据结构就是在管理真实的硬盘。所以,我们来实现与硬盘相关的数据结构。注意,以下3个数据结构形成了层级关系,即:通道挂载硬盘,共有两个通道,每个通道可挂载一主一从两个硬盘;每个硬盘都有分区,可以最多支持4个主分区,一个主分区划分成扩展分区后可以再次划分出多个逻辑分区。
(myos/device/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
有了数据结构,自然就需要根据我们的环境来创建并初始化(myos/device.ide.c)
#include "stdint.h"
#include "global.h"
#include "ide.h"
#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)
#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寄存器的一些关键位,见书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);
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");
}
函数声明,并把通道数量与通道数组全局声明,修改(myos/device/ide.h)
void ide_init(void);
extern uint8_t channel_cnt;
extern struct ide_channel channels[];
接下来我们实现一个idle
线程,用于在就绪队列为空时运行。需要注意一点:我们之前没有idle
线程,我们的系统没有出现书上说的由于就绪队列为空然后被ASSERT(!list_empty(&thread_ready_list);
悬停的情况,是因为我们的主线程(简单理解,就是main函数里面的while(1))会一直被不断加入就绪队列,所以就绪队列并不存在为空的时候。
修改(myos/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() {
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");
}
支持代码,修改(myos/kernel/global.h)
#define UNUSED __attribute__ ((unused))
硬盘是一个相对于CPU来说及其低速的设备,所以,当硬盘在进行需要长时间才能完成的工作时(比如写入数据),我们最好能让驱动程序把CPU让给其他任务。所以,我们来实现一个thread_yield
函数,就是用于把CPU让出来。实质就是将调用者重新放入就绪队列队尾。
修改(myos/thread/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);
}
thread_yield
中有个关中断的操作,会不会导致切换后由于关闭中断,而不响应时钟中断导致一直运行在切换后的进程/线程中呢?其实并不会,我们讨论两种情况,一种是进程/线程第一次上机运行,一种是进程/线程之前已经运行过,但由于时间片到期而换下过处理器。对于前者,我们进程/线程第一次上机运行都会经过kernel_thread
这个线程启动器,而这个里面是有开中断的代码的。对于后者,当切换回进程/线程时,它们执行kernel.S
中的中断退出代码jmp intr_exit
,这里面有一条指令iretd
会打开中断,让处理器能够继续响应中断代理发送来的中断信号。
由于thread_yield
不是由于时间片到期而换下处理器,所以当再次调度运行时,并不会执行kernel.S
中的中断退出代码jmp intr_exit
,所以要人为写上恢复中断状态的代码(开中断)。
函数声明(myos/thread/thread.h)
void thread_yield(void);
之前我们实现的thread_yield
是将当前任务加入就绪队列队尾,仅仅是把CPU让出来一次。我们来实现一个定时让出CPU的函数,也就是让一个任务在固定时间内都不执行。
修改(myos/device/timer.c)
#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);
}
函数声明,修改(myos/device/timer.h)
#include "stdint.h"
void mtime_sleep(uint32_t m_seconds);
现在,我们来实现驱动程序的主体部分,也就是实际与硬盘打交道的函数,实质就是将一系列寄存器操作进行封装
修改(myos/device/ide.c)
#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 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;
}
/* 从硬盘读取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");
}
函数声明,修改(myos/device/ide.h)
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);
void intr_hd_handler(uint8_t irq_no);
现在,我们来验证驱动程序能够运行,我们用它来:1、发送identify命令给硬盘来获取硬盘信息;2、扫描分区表;
修改(myos/device/ide.c)
#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);
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);
}
/* 扫描硬盘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");
}
修改(myos/kernel/init.c)完成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(); // 初始化硬盘
}
修改(myos/kernel/main.c)
#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);
}
由于我们fdisk工具分区时,设定分区起始与大小是用扇区做单位而非作者操作时的柱面,所以我们显示的硬盘分区信息start_lba
与sec_cnt
与他不一样。