在之前,我们都是通过汇编语言向硬盘的不同端口写入信息来实现硬盘的操控,但是我们现在是在C语言的环境下编程,且很多程序需要频繁的操作硬盘,我们不可能每次都通过内嵌汇编语言以out指令的形式来操作硬盘,所以,我们需要将操作硬盘的动作封装成C语言的函数,方便我们进行调用,将底层的通过端口来操作硬盘的形式封装成函数,屏蔽底层的这些对端口操作的细节就是我们现在进行的工作,即编写硬盘驱动程序。
本篇文章只关注用C语言对底层端口操作进行封装的相关内容,也就是只关注编写硬盘驱动程序相关的内容。
一、打开硬盘中断,编写供内核使用的格式化输出函数
硬盘在收到写入或读出的命令后,硬盘就会开始工作,此时CPU就可以去处理其他事情了,通常是换个线程继续执行,但是硬盘的工作处理完毕后如何通知刚才执行写入或读出命令的线程呢这就要通过中断处理程序来实现,所以我们需要先把硬盘中断打开,之前我们实现了用户态下的格式化输出函数printf,这里我们为了在内核态下调试方便补充一个内核态格式化输出函数printk。
1、修改interrupt.c,初始化可编程中断控制器8259A
/* 初始化可编程中断控制器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
/* IRQ2用于级联从片,必须打开,否则无法响应从片上的中断
主片上打开的中断有IRQ0的时钟,IRQ1的键盘和级联从片的IRQ2,其它全部关闭 */
outb (PIC_M_DATA, 0xfc);
/* 打开从片上的IRQ14,此引脚接收硬盘控制器的中断 */
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
2、在lib/kernel下添加stdio-kernel.c、stdio-kernel.h,在其中创建printk函数
#include "stdio-kernel.h"
#include "stdio.h"
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);
}
stdio-kernel.h如下:
#ifndef __LIB__STDIO_KERNEL_H
#define __LIB__STDIO_KERNEL_H
void printk(const char* format, ...);
#endif //__LIB__STDIO_KERNEL_H
二、编写与硬盘相关的数据结构
硬盘是插在主板的ide通道上的,硬盘是通过ide线与主板连接,一根ide线上可以挂主、从两块硬盘(但同一时刻只能有一块硬盘占据ide线与ide通道),且硬盘是被我们划分了不同分区后进行使用的,所以我们这里需要设计硬盘、ide通道、分区这三种数据结构。
/* 分区结构 */
struct partition {
uint32_t start_lba; //分区起始扇区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;
};
/* 硬盘结构 */
struct disk {
char name[8]; //硬盘名称
struct ide_channel* my_channel; //硬盘挂载的ide通道
uint8_t dev_no; //主盘还是副盘
struct partition prim_parts[4]; //主分区
struct partition logic_parts[8]; //逻辑分区
};
/* ata通道结构 */
struct ide_channel {
char name[8]; //通道名称
uint16_t port_base; //通道起始端口号
uint8_t irq_no; //通道所用中断号
struct lock lock; //通道锁
bool expecting_intr; //是否等待硬盘的中断
struct semaphore disk_done; //用于阻塞、唤醒驱动程序
struct disk devices[2]; //一个通道上连接两个硬盘,一主一从
};
二、宏定义硬盘各寄存器端口,初始化内存中的ide通道数据结构
位于device/ide.c文件中
#include "ide.h"
#include "stdio-kernel.h"
#include "global.h"
#include "interrupt.h"
#include "debug.h"
#include "memory.h"
#include "io.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_ALT_STAT_BSY 0x80 //硬盘正忙
#define BIT_ALT_STAT_DRDY 0x40 //驱动器准备好了
#define BIT_ALT_STAT_DRQ 0x40 //数据传输准备好了
/* device寄存器的关键位 */
#define BIT_DEV_MBS 0xa0 //第5和第7位固定为1
#define BIT_DEV_LBA 0x40 //是否启用LBA模式
#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");
/* 通过硬盘数倒推出通道数,一个ide上有两个硬盘,只初始化插有硬盘的ide通道对应的数据结构 */
uint8_t hd_cnt = *((uint8_t *) (0x475));
ASSERT(hd_cnt > 0);
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);
switch (channel_no) {
case 0:
channel->port_base = 0x1f0;
channel->irq_no = 0x2e
break;
case 1:
channel->port_base = 0x170;
channel->irq_no = 0x2f;
break;
}
channel->expecting_intr = false;
lock_init(&channel->lock);
sema_init(&channel->disk_done, 0);
channel_no++;
}
printk("ide_init done\n");
}
三、实现idle线程以及thread_yield
这一步是由于我们系统之前设计的缺陷,当就绪队列中没有线程可以上处理器执行时,整个系统会通过ASSERT(判断就绪队列不为空)报错,并通过while(1)悬停住,这里我们要改变这个局面。
本步主要是修改thread.c向其中加入全局变量struct task_struct* idle_thread以及增加idle函数作为idle线程运行的函数、修改schedule函数在无线程调用时唤醒idle线程,最后在thread_init中将idle线程创建出来并加入到就绪队列。
thread_yield函数是为后面的休眠函数做准备,核心原理就3步
(1)、将当前任务重新加入到就绪队列
(2)、将当前任务的status置为TASK_READY
(3)、调用schedule重新调度其他线程
修改后的thread.c如下:
#include "thread.h"
#include "print.h"
#include "string.h"
#include "list.h"
#include "debug.h"
#include "interrupt.h"
#include "process.h"
#include "console.h"
#include "sync.h"
#define PG_SIZE 4096
static struct task_struct* main_task_struct;
struct list thread_ready_list;
struct list thread_all_list;
struct lock pid_lock;
struct task_struct* idle_thread;
extern void switch_to(struct task_struct* cur_thread, struct task_struct* next_thread);
/* 为线程分配pid */
static pid_t allocate_pid() {
static pid_t next_pid = 0;
lock_acquire(&pid_lock);
next_pid++;
lock_release(&pid_lock);
return next_pid;
}
/* 获取当前线程指针 */
struct task_struct* running_thread() {
uint32_t esp;
asm volatile("mov %%esp, %0":"=g"(esp));
return (struct task_struct*) (esp & 0xfffff000);
}
/* 线程启动函数 */
static void kernel_thread(thread_func* func, void* arg) {
intr_enable();
func(arg);
}
/* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack相应位置 */
void init_thread_stack(struct task_struct* pthread, thread_func* function, void* arg) {
pthread->self_kstack -= sizeof(struct intr_stack);
struct thread_stack* ts = (struct thread_stack*) pthread->self_kstack;
ts->ebp = 0;
ts->ebx = 0;
ts->edi = 0;
ts->esi = 0;
ts->eip = kernel_thread;
ts->func = function;
ts->arg = arg;
}
/* 初始化线程基本信息 */
void init_thread_pcb(struct task_struct* pthread, char* thread_name, uint8_t priority) {
pthread->self_kstack = (uint32_t*) ((uint32_t) pthread + PG_SIZE);
pthread->pid = allocate_pid();
pthread->status = TASK_READY;
pthread->priority = priority;
strcpy(pthread->name, thread_name);
pthread->stack_magic = (uint32_t) 0x19970814;
pthread->ticks = priority;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL;
}
/* 根据提供的基本信息,创建线程并启动线程 */
struct task_struct* thread_start(char* thread_name, uint32_t priority, thread_func* function, void* arg) {
struct task_struct* pcb = get_kernel_pages(1);
init_thread_pcb(pcb, thread_name, priority);
init_thread_stack(pcb, function, arg);
/* 初始化线程后将PCB加入就绪队列 */
ASSERT(!elem_find(&thread_ready_list, &pcb->thread_ready_list_tag))
list_append(&thread_ready_list, &pcb->thread_ready_list_tag);
/* 确保线程不在all_list中 */
ASSERT(!elem_find(&thread_all_list, &pcb->thread_all_list_tag));
/* 将线程加入all_list中 */
list_append(&thread_all_list, &pcb->thread_all_list_tag);
return pcb;
}
/* 为主线程创造PCB并初始化 */
static void make_main_thread() {
/* 获得主线程的PCB在Loader将kernel.bin加载进来时我们就将主线程的栈设置为0x0009f000(开启页表后是0xc009f000) */
/* 所以PCB为0xc009e000 */
main_task_struct = running_thread();
init_thread_pcb(main_task_struct, "main thread", 31);
main_task_struct->status = TASK_RUNNING;
/* 确保main线程不在all_list中 */
ASSERT(!elem_find(&thread_all_list, &main_task_struct->thread_all_list_tag));
/* 将主线程加入all_list中 */
list_append(&thread_all_list, &main_task_struct->thread_all_list_tag);
}
void schedule() {
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur_thread = running_thread();
if(cur_thread->status == TASK_RUNNING) {
cur_thread->ticks = cur_thread->priority;
cur_thread->status = TASK_READY;
ASSERT(!elem_find(&thread_ready_list, &cur_thread->thread_ready_list_tag));
list_append(&thread_ready_list, &cur_thread->thread_ready_list_tag);
} else {
/* 如果不是由于时间片到期而进行调度,需要另外考虑(后续处理) */
}
if(list_empty(&thread_ready_list)) {
thread_unblock(idle_thread);
}
ASSERT(!list_empty(&thread_ready_list));
struct list_elem* next_elem = list_pop(&thread_ready_list);
struct task_struct* next_thread = elem2entry(struct task_struct, thread_ready_list_tag, next_elem);
next_thread->status = TASK_RUNNING;
process_activate(next_thread);
switch_to(cur_thread, next_thread);
}
/* 线程阻塞 */
void thread_block(enum task_status stat) {
ASSERT((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGING));
enum intr_status old_status = intr_disable();
struct task_struct* cur_thread = running_thread();
cur_thread->status = stat;
schedule();
intr_set_status(old_status);
}
/* 解除正在阻塞的线程 */
void thread_unblock(struct task_struct* thread) {
enum task_status stat = thread->status;
ASSERT(stat == TASK_BLOCKED || (stat == TASK_WAITING) || (stat == TASK_HANGING));
thread->status = TASK_READY;
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &thread->thread_ready_list_tag));
list_push(&thread_ready_list, &thread->thread_ready_list_tag);
intr_set_status(old_status);
}
static void idle(void* arg) {
while (1) {
thread_block(TASK_BLOCKED);
asm volatile("sti; hlt":::"memory");
}
}
void thread_yield() {
struct task_struct* cur = running_thread();
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &cur->thread_ready_list_tag));
list_append(&thread_ready_list, &cur->thread_ready_list_tag);
cur->status = TASK_READY;
schedule();
intr_set_status(old_status);
}
void thread_init() {
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");
}
四、实现简单的休眠函数
休眠函数的核心思路是:
(1)、将要休眠的毫秒数转化为滴答数(我们的时钟中断频率是每秒100次)
(2)、当轮到休眠线程执行时会判断经过的滴答数是否小于转化后的滴答数,如果小于说明时间未到,通过thread_yield将CPU让出。如果不小于说明已经达到指定的滴答数则线程可以继续执行。
修改后的timer.c如下(增加了ticks_to_sleep函数以及mtime_sleep函数)
#include "io.h"
#include "print.h"
#include "thread.h"
#include "debug.h"
#include "interrupt.h"
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43
#define IRQ0_FREQUENCY 100 //时钟中断频率为每秒100次
#define mil_seconds_per_intr (1000 / IRQ0_FREQUENCY) //一个中断周期的毫秒数
uint32_t ticks;
static void intr_timer_handler() {
struct task_struct* cur_pcb = running_thread();
if(cur_pcb->stack_magic != 0x19970814) {
put_str("\ncur_pcb:");
put_str(cur_pcb->name);
put_str("\ncur_pcb->stack_magic:");
put_int(cur_pcb->stack_magic);
}
ASSERT(cur_pcb->stack_magic == 0x19970814);
ticks++;
cur_pcb->elapsed_ticks++;
if(cur_pcb->ticks == 0) {
schedule();
} else {
cur_pcb->ticks--;
}
}
/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value */
static void frequency_set(uint8_t counter_port, \
uint8_t counter_no, \
uint8_t rwl, \
uint8_t counter_mode, \
uint16_t counter_value) {
/* 往控制字寄存器端口0x43中写入控制字 */
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
/* 先写入counter_value的低8位 */
outb(counter_port, (uint8_t)counter_value);
/* 再写入counter_value的高8位 */
outb(counter_port, (uint8_t)counter_value >> 8);
}
/* 初始化PIT8253 */
void timer_init() {
put_str("timer_init start\n");
/* 设置8253的定时周期,也就是发中断的周期 */
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
register_handler(intr_timer_handler, 0x20);
put_str("timer_init done\n");
}
/* sleep_ticks表示需要睡眠的时间转化成中断数是几次,本函数的作用是睡眠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);
}
五、编写硬盘驱动程序基础函数
我们最终要实现的目的是从硬盘读取sec_cnt个扇区到缓冲区buf,将缓冲区buf中sec_cnt个扇区写入到硬盘,而实现硬盘操作都需要经历以下的步骤,这些步骤在书中第三章提到过,这里重新拿出来看一下:
(1)、设置本次操作硬盘对应通道的device寄存器是否启用lba模式,读写主盘还是从盘
(2)、写入要读写的扇区数,即设置sector count寄存器读写扇区数量
(3)、在该通道的三个lba寄存器中写入扇区起始地址低24位
(4)、往device寄存器写入lba地址24~27位,由于不能单独写入这四位所以还需要重新设置第一位的lba模式以及读写主盘还是从盘
(5)、往该通道cmd寄存器写入操作命令
(6)、读取该通道的status寄存器判断硬盘工作是否完成
需要额外注意的是主板上的硬盘通道对应着固定的端口号,所以选择硬盘就是选择通道,又因为通道上可以安两块硬盘,所以再对主盘还是从盘进行选择。
这里我们将第一步封装为select_disk,书中是这样起名的,我感觉这步成为初始化硬盘的device寄存器更合适。我们将第二、