已完成实验
已完成实验链接
简介
实验 23. 硬盘分区,并编写硬盘驱动程序
总结
-
创建硬盘并分区
-
加载硬盘分区
-
实现 printk
-
使能硬盘中断 interrupt.c
-
idel 线程,主线程阻塞后此线程保底 thread.c
-
sleep 函数 timer.c
-
加载分区信息 ide.c
-
创建硬盘
fdisk /home/c/os/bochs/hd80M.img
# 1.设置柱面和磁头
m
x
m
c
162
h
16
r
# 创建主分区
n
p
1
2048
33263
# 创建拓展分区
n
e
4
33264
163295
# 创建逻辑分区
n
35312
51407
n
53456
76607
n
78656
91727
n
93776
121967
n
124016
163295
# 修改拓展分区的type
t
5
66
t
6
66
t
7
66
t
8
66
t
9
66
用 xxd 查看分区
#0x1BE=446
#0x200=512
fdisk -l /home/c/os/bochs/hd80M.img
# 查看硬盘分区表
./xxd.sh /home/c/os/bochs/hd80M.img 0x1BE 64
00 00 21 02 83 0F 3F 20 # 83 linux img1
00 08 00 00 # 起始扇区 0x0800 = 2048
F0 79 00 00 # 扇区数 0x79F0 = 31216
00 00 01 21 05 0F 3F A1 # 5 总拓展分区 img4
F0 81 00 00 # 起始扇区 0x81F0 = 33264
F0 FB 01 00 # 扇区数 0x1FBF0 = 130032
# 查看拓展分区表
# 0x81F0 * 0x200 + 0x1BE = 0x103E1BE
./xxd.sh /home/c/os/bochs/hd80M.img 0x103E1BE 64
00 00 21 23 66 0F 3F 32 # 66 逻辑分区 img5
00 08 00 00 # 起始扇区 0x0800 + 0x81F0 = 0x89F0 = 35312
E0 3E 00 00 # 扇区数 0x3EE0 = 16096
00 00 01 33 05 0F 3F 4B # 5 子拓展分区1 不显示
E0 46 00 00 # 起始扇区 0x46E0 + 0x81F0 = 0xC8D0
70 62 00 00 # 扇区数 0x6270 = 25200
# 查看子拓展分区1表
# 0xC8D0 * 0x200 + 0x1BE = 0x191A1BE
./xxd.sh /home/c/os/bochs/hd80M.img 0x191A1BE 64
00 00 21 35 66 0F 3F 4B # 66 逻辑分区 img6
00 08 00 00 # 起始扇区 0x0800 + 0xC8D0 = 0xD0D0 = 53456
70 5A 00 00 # 扇区数 0x5A70 = 23152
# 后续就是递归查找了
主要代码
stdio-kernel.c
// 文件: stdio-kernel.c
// 时间: 2024-08-06
// 来自: ccj
// 描述: 内核打印,与printf的区别是不用陷入中断,开销更小
#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
/// @brief 供内核使用的格式化输出函数
/// @param format 格式
/// @param 参数
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);
}
interrupt.c
thread.c
timer.c
ide.h
// 文件: ide.h
// 时间: 2024-08-05
// 来自: ccj
// 描述: 硬盘相关数据结构
#ifndef __DEVICE_IDE_H
#define __DEVICE_IDE_H
#include "stdint.h"
#include "sync.h"
#include "list.h"
#include "bitmap.h"
/// @brief 分区结构
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结点队列
};
/// @brief 硬盘结构
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个
};
/// @brief ata通道结构
/// ide = ata ,硬盘接口类型,如sata
struct ide_channel {
char name[8]; // 本ata通道名称, 如ata0,也被叫做ide0.
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
ide.c
// 文件: ide.c
// 时间: 2024-08-06
// 来自: ccj
// 描述: 从盘初始化
// 1.读取硬盘分区
#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硬盘
uint8_t channel_cnt; // 按硬盘数计算的通道数
struct ide_channel channels[2]; // 有两个ide通道
// 用于记录总扩展分区的起始lba,初始为0,partition_scan时以此为标记
int32_t ext_lba_base = 0;
uint8_t p_no = 0, l_no = 0; // 用来记录硬盘主分区和逻辑分区的下标
struct list partition_list; // 分区队列
/// @brief 构建一个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字节大小
/// @brief 引导扇区,mbr或ebr所在的扇区
// 512字节 = 引导代码[446] + 分区表[64] + 魔数[2]
struct boot_sector {
uint8_t other[446]; // 引导代码
struct partition_table_entry partition_table[4]; // 分区表中有4项,共64字节
uint16_t signature; // 启动扇区的结束标志是0x55,0xaa,
} __attribute__((packed));
/// @brief 发送选择硬盘指令
/// 向硬盘的端口写入指令
/// @param hd
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);
}
/// @brief 发送选择扇区起始和扇区数命令
/// @param hd 硬盘
/// @param lba 扇区索引
/// @param sec_cnt 扇区数
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地址(即扇区号)
// lba地址的低8位,不用单独取出低8位.outb函数中的汇编指令outb %b0, %w1会只用al。
outb(reg_lba_l(channel), lba);
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);
}
/// @brief 向通道channel发命令cmd,并标记有额外操作
/// @param channel ide通道索引
/// @param cmd 命令
static void cmd_out(struct ide_channel* channel, uint8_t cmd) {
channel->expecting_intr = true; // 表示下次中断有额外操作
outb(reg_cmd(channel), cmd);
}
/// @brief 从硬盘读入sec_cnt个扇区的数据到buf
/// @param hd 硬盘
/// @param buf 数据
/// @param sec_cnt 扇区数
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;
}
// 从数据端口读出 size_in_byte个比特
insw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}
/// @brief 将buf中sec_cnt扇区的数据写入硬盘
/// @param hd 硬盘
/// @param buf 数据
/// @param 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);
}
/// @brief 等待30秒
/// @param hd 硬盘指针
/// @return 超时返回false
static bool busy_wait(struct disk* hd) {
struct ide_channel* channel = hd->my_channel;
uint16_t time_limit = 30 * 1000; // 可以等待30秒
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;
}
/// @brief 从硬盘读取sec_cnt个扇区到buf
/// @param hd 硬盘
/// @param lba 地址
/// @param buf 数据
/// @param sec_cnt 扇区数
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);
// 执行的命令写入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);
}
/// @brief 将buf中sec_cnt扇区数据写入硬盘
/// @param hd 硬盘
/// @param lba 数据
/// @param buf
/// @param 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);
}
/// @brief 将dst中len个相邻字节交换位置后存入buf
/// @param dst
/// @param buf
/// @param len
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';
}
/// @brief 获得硬盘参数信息
/// @param hd 硬盘索引
static void identify_disk(struct disk* hd) {
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);
}
// 读取1个扇区到id_info
char id_info[512];
read_from_sector(hd, id_info, 1);
char buf[64];
uint8_t sn_start = 10 * 2;
uint8_t sn_len = 20;
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));
uint8_t md_start = 27 * 2;
uint8_t md_len = 40;
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);
}
/// @brief 扫描硬盘hd中地址为ext_lba的扇区中的所有分区
/// @param hd 硬盘
/// @param 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);
// 遍历分区表4个分区表项
struct partition_table_entry* p = bs->partition_table;
uint8_t part_idx = 0;
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); // 主分区数字是1~4
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);
// 逻辑分区数字是从5开始
sprintf(hd->logic_parts[l_no].name, "%s%d", hd->name, l_no + 5);
l_no++;
if (l_no >= 8) // 只支持8个逻辑分区,避免数组越界
return;
}
}
p++;
}
sys_free(bs);
}
/// @brief 打印分区信息
/// @param pelem
/// @param UNUSED
/// @return
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;
}
/// @brief 硬盘中断处理程序
/// @param irq_no 中断号
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));
}
}
/// @brief 硬盘数据结构初始化
void ide_init() {
printk("[ide] ide_init start\n");
list_init(&partition_list);
uint8_t hd_cnt = *((uint8_t*)(0x475)); // 获取硬盘的数量 2个硬盘
ASSERT(hd_cnt > 0);
channel_cnt = DIV_ROUND_UP(hd_cnt, 2); // 一个ide通道上有两个硬盘 1个ide通道
// 处理每个ide通道的两个硬盘
struct ide_channel* channel;
uint8_t channel_no = 0;
while (channel_no < channel_cnt) {
channel = &channels[channel_no];
lock_init(&channel->lock); // 初始化锁
sprintf(channel->name, "ide%d", channel_no); // 设置ide通道名字
// 为每个ide通道设置端口号、中断向量、并注册中断号处理
switch (channel_no) {
case 0:
channel->port_base = 0x1f0; // ide0通道的起始端口号是0x1f0
channel->irq_no = 0x20 + 14;
break;
case 1:
channel->port_base = 0x170; // ide1通道的起始端口号是0x170
channel->irq_no = 0x20 + 15;
break;
}
register_handler(channel->irq_no, intr_hd_handler);
sema_init(&channel->disk_done, 0);
channel->expecting_intr = false;
// 分别获取两个硬盘的参数及分区信息
uint8_t dev_no = 0;
while (dev_no < 2) {
struct disk* hd = &channel->devices[dev_no];
hd->my_channel = channel; // 设置硬盘的ide通道
hd->dev_no = dev_no; // 设置硬盘索引
sprintf(hd->name, "sd%c", 'a' + channel_no * 2 + dev_no); // 设置硬盘名 sda和sdb
// 重点 阻塞
identify_disk(hd); // 获取硬盘参数
// 扫描该硬盘上的分区
if (dev_no != 0) { // 内核本身的裸硬盘(hd60M.img)不处理
partition_scan(hd, 0);
}
p_no = 0, l_no = 0;
dev_no++;
}
channel_no++;
}
printk("\n all partition info\n");
// 打印所有分区信息
list_traversal(&partition_list, partition_info, (int)NULL);
printk("[ide] ide_init done\n");
}
init.c
// 文件: init.c
// 时间: 2024-07-22
// 来自: ccj
// 描述: 内核所有初始化操作
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "keyboard.h"
#include "console.h"
#include "tss.h"
#include "syscall-init.h"
+#include "ide.h"
/// @brief 内核所有初始化
void init_all() {
put_str("init all\n");
idt_init(); // 初始化中断
timer_init(); // 调快时钟、注册时钟中断来调度线程
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程
console_init(); // 控制台初始化最好放在开中断之前
keyboard_init(); // 键盘初始化
tss_init(); // tss初始化
syscall_init(); // 初始化系统调用
+ intr_enable(); // 打开中断
+ ide_init(); // 初始化硬盘
}
main.c
// 文件: main.c
// 时间: 2024-07-19
// 来自: ccj
// 描述: 内核从此处开始
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall.h"
#include "syscall-init.h"
#include "stdio.h"
#include "memory.h"
int main(void) {
put_str("I am kernel\n");
init_all();
while (1);
return 0;
}