手写简易操作系统(二十一)--硬盘驱动

前情提要

上面一节我们实现了 mallocfree 的系统调用,这一节我们来实现硬盘驱动。

一、硬盘分区

我们的文件系统安装在一块全新的硬盘中,我们先创建它,然后在给他分区。

1.1、创建硬盘

首先是创建,这个之前我们已经干过一次了

image-20240329212149454

然后是修改配置文件,虽然创建了,但也需要让虚拟机知道

image-20240329212232205

添加最下面的一行。在物理地址 0x475 处存储着主机安装硬盘的数量。它是由BIOS检测并写入的。启动bochs后可以调试一下,获得现在的硬盘数

image-20240329212958435

1.2、创建硬盘分区

当初硬盘制造者认为,一台机器上顶多安装4个操作系统,每个操作系统各占1个分区,所以硬盘支持4个分区足矣。想想也是,谁没事在1台电脑上不断重启机器只为来回切换4个操作系统呢?但是随着硬盘容量越来越大,为方便文件管理,必须想办法支持更多的分区。

分区是逻辑上划分磁盘空间的方式,归根结底是人为地将硬盘上的柱面扇区划分成不同的分组,每个分组都是单独的分区。各分区都有“描述符”来描述分区本身所在硬盘上的起止界限等信息,在硬盘的MBR中有个64字节“固定大小”的数据结构,这就是著名的分区表,分区表中的每个表项就是一个分区的“描述符”,表项大小是16字节,因此64字节的分区表总共可容纳4个表项,这就是为什么硬盘仅支持4个分区的原因。

其实分区表的长度并不是由结构本身限制的,而是由其所在的位置限制的,它必须存在于MBR引导扇区或EBR引导扇区中。在这512字节中,前446字节是硬盘的参数和引导程序,然后才是64字节的分区表,最后是2字节的魔数55aa。。随着计算机的发展,很多程序已经对这个扇区有依赖了,尤其是一些引导型程序(如BIOS),都会在该扇区的512字节中的固定位置读取关键数据,如果更改了此扇区中的数据结构长度,那么很多程序都必须做出改变。

为此,硬盘厂商准备在分区“描述符”动动手脚。在这个“描述符”中有个属性是文件系统id,它表示文件系统的类型,为支持更多的分区,专门增加一种id属性值(id为5),用来表示该分区可被再次划分出更多的子分区,这就是逻辑分区。因为只是在分区表项中通过属性来判断分区类型,所以这4个分区中的任意一个都可以作为扩展分区。扩展分区是可选项,有没有都行,但最多只有1个,1个扩展分区在理论上可被划分出任意多的子扩展分区。

发明扩展分区的目的是为了支持任意数量的分区,但具体划分出多少分区,完全是由用户决定的,所以,扩展分区是种抽象、不具实体的分区,它类似于一种“宣告”,告诉大家此分区需要再被划分出子分区,也就是所谓的逻辑分区,逻辑分区才可以像其他主分区那样使用。因此,逻辑分区只存在于扩展分区,它属于扩展分区的子集。

现在我们开始分区

image-20240329214550937

第一个分区,主分区,分区号1,扇区号 2048-20480

第二个分区,主分区,分区号2,扇区号 20481-42500

第三个分区,主分区,分区号3,扇区号 43008-64000

第四个分区,扩展分区,分区号4,扇区号 65536-204623

在第四分区中创建逻辑分区,分区号5,扇区号 67584-167584

在第四分区中创建逻辑分区,分区号6,扇区号 169984-204623

image-20240329215522869

最后将分区信息写入到磁盘内。

image-20240329220042000

二、磁盘分区表简析

磁盘分区表(Disk Partition Table)简称DPT,是由多个分区元信息汇成的表,表中每一个表项都对应一个分区,主要记录各分区的起始扇区地址,大小界限等。

最初的磁盘分区表位于MBR引导扇区中,早在加载loader时就和大伙儿介绍过MBR,MBR(Main Boot Record)即主引导记录,它是一段引导程序,其所在的扇区称为主引导扇区,

该扇区位于0盘0道1扇区(物理扇区编号从1开始,逻辑扇区地址LBA从0开始),也就是硬盘最开始的扇区,扇区大小为512字节,这512字节内容由三部分组成。

(1)主引导记录MBR。
(2)磁盘分区表DPT。
(3)结束魔数55AA,表示此扇区为主引导扇区,里面包含控制程序。

MBR引导程序位于主引导扇区中偏移0~0x1BD的空间,共计446字节大小,这其中包括硬盘参数及部分指令(由BIOS跳入执行),它是由分区工具产生的,独立于任何操作系统。

磁盘分区表位于主引导扇区中偏移0x1BE~0x1FD的空间,总共64字节大小,每个分区表项是16字节,因此磁盘分区表最大支持4个分区。分区表项结构如下

image-20240329215913622

刚刚好是16字节。

2.1、查看主分区分区表

文件系统类型是指NTFS、FAT32、EXT2等,我们在fdisk过程中用l命令列出的便是。为了能够真正理解上面的内容,我们用工具看一下磁盘的MBR。

image-20240329220435261

可以看到结尾是55aa,这个首先没问题,

看四个分区结构如下

00 20 21 00 83 46 06 01 00 08 00 00 01 48 00 00 
00 66 26 01 83 A4 27 02 00 58 00 00 05 4E 00 00 
00 AC 2B 02 83 FA 38 03 00 A8 00 00 01 52 00 00 
00 14 11 04 05 BB 3F 0C 00 00 01 00 50 1F 02 00

第一个扇区的起始偏移扇区为 0x4801 ,起始偏移扇区为 0x0800,这些都没问题。注意读取是小端字节序。

第四个扇区的起始偏移扇区为 0x021f0 ,起始偏移扇区为 0x0800

主分区的看完了,我们看一下逻辑分区的

2.2、逻辑分区分区表

扩展分区中的所有分区表被组织成单向链表,咱们查看链表中的第1个结点,也就是第1个子扩展分区的EBR引导扇区起始偏移地址为 65536* 512 = 33554432

image-20240329221707731

我们还是将有用的部分提取出来

00 34 31 04 83 6E 05 0A 00 08 00 00 A1 86 01 00 
00 73 2A 0A 05 BB 3F 0C 00 90 01 00 50 8F 00 00 

首先看第一条,他指出,第一个扩展分区的大小为 0x0186A1,第一个扩展分区的相对于当前分区的偏移地址为 0x0800, 加上当前的扇区号0x10000正好就是第一个逻辑扇区的起始地址。

再看第二条,他指出,第二个扩展分区的链表相对于当前分区的偏移地址为 0x019000,加上当前的扇区号为 167936,在查看一下第二个扩展分区的链表节点

image-20240329222848064
第二个扩展分区的链表可以看出,当前分区的大小为 0x8750,当前分区的相对偏移地址为 0x0800,算一下绝对的地址即为 169984

三、编写硬盘驱动程序

硬盘的一些端口还是看一下之前的文章 手写简易操作系统(三)–加载Loader

3.1、硬盘驱动的数据结构

/* 分区结构 */
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还是从1struct 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];	    // 一个通道上连接两个硬盘,一主一从
};

可以看到,结构分为三层,第一层是ata通道,一个ata通道有两块硬盘。第二层是硬盘,一块硬盘最多有四个主分区,无数个逻辑分区,但是我们这里只支持8个逻辑分区。第三层就是分区了,分区是我们控制的最底层,它包含起始扇区,扇区数,所归属的硬盘等结构。

3.2、初始化

硬盘还是像之前在loader中一样,读取寄存器。这里我们先把这些端口定义出来

/* 定义硬盘各寄存器的端口号 */
#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              // 第6位为1,LBA寻址方式
#define BIT_DEV_MASTER	0x10          // 第4位为1,从盘
#define BIT_DEV_SLAVE	0x00          // 第4位为1,从盘/* 一些硬盘操作的指令 */
#define CMD_IDENTIFY	   0xec	      // identify指令
#define CMD_READ_SECTOR	   0x20       // 读扇区指令
#define CMD_WRITE_SECTOR   0x30	      // 写扇区指令/* 定义可读写的最大扇区数,调试用的 */
#define max_lba ((100*1024*1024/512) - 1)/* 通道数量 */
uint8_t channel_cnt;/* 最多支持两个通道 */
struct ide_channel channels[2];

硬盘上述的数据结构初始化过程如下

/* 硬盘数据结构初始化 */
void ide_init() {printk("ide_init begin!\n");// 获取硬盘的数量,硬盘数量由BIOS保存在0x475的内存地址int8_t hd_cnt = *((uint8_t*)(0x475));// 保证硬盘数量是大于0的ASSERT(hd_cnt > 0);// 一个ide通道上有两个硬盘,根据硬盘数量反推有几个ide通道channel_cnt = DIV_ROUND_UP(hd_cnt, 2);// 分别处理每个通道上的硬盘for (uint8_t channel_no = 0; channel_no < channel_cnt; channel_no++) {// 指针指向不同的通道struct ide_channel* channel = &channels[channel_no];// 为每个通道命名sprintf(channel->name, "ide%d", channel_no);// 初始化每个通道的端口基址及中断向量if (channel_no == 0) {channel->port_base = 0x1f0;	  // ide0通道的起始端口号是0x1f0channel->irq_no = 0x20 + 14;  // 从片8259a上倒数第二的中断引脚,温盘,也就是ide0通道的的中断向量号}else if (channel_no == 1) {channel->port_base = 0x170;   // ide1通道的起始端口号是0x170channel->irq_no = 0x20 + 15;  // 从8259A上的最后一个中断引脚,我们用来响应ide1通道上的硬盘中断}channel->expecting_intr = false;  // 未向硬盘写入指令时不期待硬盘的中断// 初始化通道锁lock_init(&channel->lock);/* 初始化为0,目的是向硬盘控制器请求数据后,硬盘驱动sema_down此信号量会阻塞线程,直到硬盘完成后通过发中断,由中断处理程序将此信号量sema_up,唤醒线程. */sema_init(&channel->disk_done, 0);}printk("ide_init done!\n");
}

3.3、完成thread_yield 和 idle 线程

thread_yield的功能是主动把CPU使用权让出来,它与thread_block的区别是thread_yield执行后任务的状态是TASK_READY,即让出CPU后,它会被加入到就绪队列中,下次还能继续被调度器调度执行,而thread_block执行后任务的状态是TASK_BLOCKED,需要被唤醒后才能加入到就绪队列。

thread_yield很简单,就是将当前线程状态置为 TASK_READY ,由于线程是主动放弃的CPU控制权,所以不改变其优先级。直接加入多级优先队列

void thread_yield(void) {enum intr_status old_status = intr_disable();struct task_struct* cur = running_thread();   cur->status = TASK_READY;mlfq_push_wspt(cur);           // 不改变其优先级和时间片schedule();intr_set_status(old_status);
}

创建idle线程的代码也很简单

/* 系统空闲时运行的线程 */
static void idle(void* arg UNUSED) {while(1) {thread_block(TASK_BLOCKED); //执行hlt时必须要保证目前处在开中断的情况下asm volatile ("sti; hlt" : : : "memory");}
}/* 初始化线程环境 */
void thread_init(void) {put_str("thread_init start\n");lock_init(&pid_lock);            // pid锁初始化mlfq_init();                     // 多级队列初始化make_main_thread();              // 创建主线程idle_thread = thread_start("idle", idle, NULL); // 创建idle线程put_str("thread_init done\n");
}

其中UNUSED的宏定义是为了在编译时不产生未使用变量的报错。idle会先被添加到多级队列中,先被调度上CPU执行一次,这次会执行到idle线程阻塞自己,阻塞自己后会自动调度上别的线程。

当多级队列中没有线程了,就会唤醒idle线程,唤醒后的idle线程执行的是内联汇编

asm volatile ("sti; hlt" : : : "memory");

开中断和等待,hlt的等待不是空转CPU,而是真的CPU等待。开中断的目的是等外部中断来打断自己。

3.4、实现休眠函数

休眠函数的实现就要依赖上面的thread_yield 函数了

/**********************
@author: liyajun
@data: 2024.3.30 20:04
@description: 以tick为单位的sleep
***********************/
static void ticks_to_sleep(uint32_t sleep_ticks) {uint32_t start_tick = ticks;/* 若间隔的ticks数不够便让出cpu */while (ticks - start_tick < sleep_ticks) {thread_yield();}
}/**********************
@author: liyajun
@data: 2024.3.30 20:04
@description: 以毫秒为单位的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); 
}

首先是以ticks为标准的休眠函数,如果当前还不到休眠时间的话,那么直接让出CPU的使用权,实现以毫秒为基准的休眠函数的话就是将其改为以ticks为标准,毕竟CPU的计时是以ticks为基准的。

四、完善硬盘驱动

本节主要是完成两个函数,一个是读,一个是写

/* 硬盘hd的lba起始扇区,读取sec_cnt个扇区到buf */
void ide_read(struct disk* hd, uint32_t lba, void* buf, uint32_t sec_cnt);
/* 将buf中sec_cnt扇区数据写入硬盘 */
void ide_write(struct disk* hd, uint32_t lba, void* buf, uint32_t 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地址(即扇区号)outb(reg_lba_l(channel), lba);		     // lba地址的低8位outb(reg_lba_m(channel), lba >> 8);	     // lba地址的8~15位outb(reg_lba_h(channel), lba >> 16);     // lba地址的16~23位// 写入lba的高4位地址,顺便加上控制字uint8_t reg_device = 0;if (hd->dev_no == 0) reg_device = BIT_DEV_MBS | BIT_DEV_LBA | BIT_DEV_MASTER | lba >> 24;if (hd->dev_no == 1) reg_device = BIT_DEV_MBS | BIT_DEV_LBA | BIT_DEV_SLAVE | lba >> 24;outb(reg_dev(hd->my_channel), reg_device);
}/* 向通道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,为0则读取256个扇区 */
static void read_from_sector(struct disk* hd, void* buf, uint8_t sec_cnt) {// 要读取的字节数uint32_t size_in_byte = sec_cnt == 0 ? 256 * SEC_BIT : sec_cnt * SEC_BIT;// 读取这些字节insw(reg_data(hd->my_channel), buf, size_in_byte / 2);
}/* 将buf中sec_cnt扇区的数据写入硬盘,为0则写入256个扇区 */
static void write2sector(struct disk* hd, void* buf, uint8_t sec_cnt) {// 要写入的字节数uint32_t size_in_byte = sec_cnt == 0 ? 256 * SEC_BIT : sec_cnt * SEC_BIT;// 写入这些字节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) {// 每次读入的扇区数,最大256if ((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);// 4 阻塞自己,等待硬盘中断程序唤醒自己sema_down(&hd->my_channel->disk_done);// 5 醒来后,检测硬盘状态是否可读,不可读的话在此输出错误信息if (!busy_wait(hd)) {char error[64];sprintf(error, "%s read sector %d failed!!!!!!\n", hd->name, lba);PANIC(error);}// 6 把数据从硬盘的缓冲区中读出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) {// 每次写入的扇区数,最大256if ((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);// 6 在硬盘响应期间阻塞自己sema_down(&hd->my_channel->disk_done);// 7 醒来后执行下一次的写入,或者结束,释放锁secs_done += secs_op;}lock_release(&hd->my_channel->lock);
}/* 硬盘中断处理程序 */
static void intr_hd_handler(uint8_t irq_no) {// 保证是两个硬盘通道的中断,第一个通道的硬盘中断号是0x2e,第二个是0x2fASSERT(irq_no == 0x2e || irq_no == 0x2f);// 获得是哪个通道uint8_t ch_no = irq_no - 0x2e;// 获得通道的指针struct ide_channel* channel = &channels[ch_no];// 不必担心此中断是否对应的是这一次的expecting_intr,每次读写硬盘时会申请锁,从而保证了同步一致性if (channel->expecting_intr) {// 期待中断为假channel->expecting_intr = false;// 唤醒读写的程序sema_up(&channel->disk_done);// 读取状态寄存器使硬盘控制器认为此次的中断已被处理,从而硬盘可以继续执行新的读写inb(reg_status(channel));}
}

最后将硬盘中断函数注册。

五、扫描分区表

5.1、获取硬盘信息

identify命令是0xec,它用于获取硬盘的参数,不过奇怪的是此命令返回的结果都是以字为单位,并不是字节,这一点要注意。咱们只是来验证驱动程序,下表中只列出了咱们用到的三个参数。

字节偏移量描述
10-19硬盘序列号,长度为20的字符串
27-46硬盘型号,长度为40的字符串
60-61可供用户使用的扇区数,长度为2的整型

5.2、分区表扫描

需要以MBR引导扇区为入口,遍历所有主分区,然后找到总扩展分区,在其中递归遍历每一个子扩展分区,找出逻辑分区。由于涉及到分区的管理,因此我们得给每个分区起个名字。

这里介绍一下Linux现成的命名方案,

  1. SCSI/SATA 硬盘命名
    • sda, sdb, sdc 等:表示 SCSI 或 SATA 接口的硬盘,字母后面的数字表示硬盘的编号,从 a 开始递增。
    • sda1, sda2, sdb1, sdc1 等:表示 SCSI 或 SATA 接口的硬盘的分区,数字表示分区的编号,从 1 开始递增。
  2. IDE 硬盘命名
    • hda, hdb, hdc 等:表示 IDE 接口的硬盘,字母后面的数字表示硬盘的编号,从 a 开始递增。
    • hda1, hda2, hdb1, hdc1 等:表示 IDE 接口的硬盘的分区,数字表示分区的编号,从 1 开始递增。
  3. NVMe 硬盘命名
    • /dev/nvmeXnY:表示 NVMe 控制器的编号为 X,命名空间的编号为 Y
  4. 其他设备
    • 除了硬盘之外,其他设备(如光驱、USB 设备等)也有相应的命名规则,例如 /dev/cdrom 表示光驱,/dev/usbX 表示 USB 设备等。

首先先构建一个结构体用来存分区表项中的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));	 // 保证CPU不会优化/* 引导扇区,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,此函数用来处理identify命令的返回信息*/
static void swap_pairs_bytes(const char* dst, char* buf, uint32_t len) {// 硬盘参数信息是以字为单位的,在16位的字中,相邻字符的位置是互换的,所以通过此函数做转换。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;uint8_t sn_len = 20;uint8_t md_start = 27 * 2;uint8_t md_len = 40;swap_pairs_bytes(&id_info[sn_start], buf, sn_len);printk("   disk %s info:\n", hd->name);printk("      SN: %s\n", 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 / 2 / 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);// 指向四个主分区struct partition_table_entry* p = bs->partition_table;// 遍历分区表4个分区表项for (uint8_t part_idx = 0; part_idx < 4; part_idx++) {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++;// 只支持4个主分区if (l_no >= 4) return;}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++;// 只支持8个扩展分区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);// 在此处return false与函数本身功能无关,只是为了让主调函数list_traversal继续向下遍历元素return false;
}

老实说,在操作系统看来,数据容器是以分区作为区分的,而不是硬盘,更不是通道。

5.3、仿真

image-20240330182125127

结束语

本节我们实现了对硬盘参数的读取,以及对分区信息的读取,主要的信息还是起始扇区,分区大小。下一节我们将在此基础上实现文件系统,由于文件系统较为复杂,所以我们可能会分为多章节。

老规矩,本节的代码地址:https://github.com/lyajpunov/os

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/788694.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux网络编程一(协议、TCP协议、UDP、socket编程、TCP服务器端及客户端)

文章目录 协议1、分层模型结构2、网络应用程序设计模式3、ARP协议4、IP协议5、UDP协议6、TCP协议 Socket编程1、网络套接字(socket)2、网络字节序3、IP地址转换4、一系列函数5、TCP通信流程分析 第二次更新&#xff0c;自己再重新梳理一遍… 协议 协议&#xff1a;指一组规则&…

Kafka架构概述

Kafka的体系结构 Kafka是由Apache软件基金会管理的一个开源的分布式数据流处理平台。Kafka具有支持消息的发布/订阅模式、高吞吐量与低延迟、持久化、支持水平扩展、高可用性等特点。可以将Kafka应用于大数据实时处理、高性能数据管道、流分析、数据集成和关键任务应用等场景。…

20240402—Qt如何通过动态属性设置按钮样式?

前言 正文 1、点击UI文件 2、选择Bool型或是QString 3、设置后这里出现动态属性 4、这qss文件中绑定该动态属性 QPushButton[PopBlueBtn"PopBlueBtn"]{background-color:#1050B7;color:#FFFFFF;font-size:20px;font-family:Source Han Sans CN;//思源黑体 CNbor…

【JavaEE初阶系列】——一万字带你了解 JUC常见类 以及 线程安全集合类(哈希表)

目录 &#x1f6a9;JUC(java.util.concurrent) 的常见类 &#x1f388;Callable 接口 &#x1f308;理解 Callable(相关面试题) &#x1f308;理解 FutureTask &#x1f4dd;线程创建方式 &#x1f388; ReentrantLock可重入锁 &#x1f308;ReentrantLock 优势&#x…

4.2日java总结,以及窗口的创建

今日份学习——字符串的进阶 1.StringBulider StringBulider是一个java里的关键字&#xff0c;可以看做一个容器&#xff0c;但是其是一个可以改变的容器&#xff0c;对其有四种操作可以进行&#xff0c;分别是添加元素&#xff08;append&#xff09;&#xff0c;反转元素&a…

谷粒商城实战(009 缓存-分布式锁)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第158p-第p165的内容 分布式锁 原理和使用 使用下shell对产生的命令进行发送 查看 -> 撰写 -> 撰写栏 idea 选中的代码提取成方法 加锁…

【教学类-09-07】20240401细线迷宫图02+箭头图片(A4横版一页-2份竖版)

作品展示 作品展示 word模板 重点说明 代码展示 批量制作细线条的迷宫图(A4横板一面2张竖版)箭头图片 作者&#xff1a; 1、落难Coder https://blog.csdn.net/u014297502/article/details/124839912 2、AI对话大师 3、阿夏 作者&#xff1a;2024年4月3日 numint(input(几人&…

Android14之BpBinder构造函数Handle拆解(二百零四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

Docker:探索容器化技术,重塑云计算时代应用交付与管理

一&#xff0c;引言 在云计算时代&#xff0c;随着开发者逐步将应用迁移至云端以减轻硬件管理负担&#xff0c;软件配置与环境一致性问题日益凸显。Docker的横空出世&#xff0c;恰好为软件开发者带来了全新的解决方案&#xff0c;它革新了软件的打包、分发和管理方式&#xff…

AIGC之gradio系列学习教程(一)

主题:本篇即为入门,后续将会从函数和使用场景介绍。 Gradio 是一个开源 Python 包,可让快速为机器学习模型、API 或任何任意 Python 函数构建演示或 Web 应用程序。然后,您可以使用 Gradio 的内置共享功能在短短几秒钟内共享演示或 Web 应用程序的链接。无需 JavaScript、…

C#中值类型与引用类型的存储

目录 值对象与引用对象的存储 引用对象的成员存储 值对象与引用对象的存储 数据项的类型定义了存储数据需要的内存大小及组成该类型的数据成员。类型还决定了对象在内存中的存储位置——栈或堆。 C#中类型分为两种&#xff1a;值类型和引用类型&#xff0c;这两种类型的对象…

【蓝桥杯 C++高级组省赛以及2020年-蓝桥杯C++省赛合集+部分答案】

一、选择题&#xff08;单项选择&#xff0c;每空30分&#xff09; 请将选择题答案填入答题卡蓝色框内 第一题&#xff08;难度系数 1&#xff09; 结构化程序所要求的基本结构不包括( )。 A.顺序结构 B.GOTO()跳转 C.选择(分支)结构 D.重复(循环)结构 第二题&#xff…

银行监管报送系统介绍(十五):金融审计平台

《“十四五”国家审计工作发展规划》中重点强调&#xff0c;金融审计&#xff1a;以防范化解重大风险、促进金融服务实体经济&#xff0c;推动深化金融供给侧结构性改革、建立安全高效的现代金融体系为目标&#xff0c;加强对金融监管部门、金融机构和金融市场运行的审计。 —…

面试题:MySQL 事务 日志 MVCC

事务的特性 ACID 事务的隔离级别 并发事务问题 脏读&#xff1a;一个事务读到另一个事务还没有提交的数据不可重复读&#xff1a;一个事务先后读取同一条记录&#xff0c;但两次读取的数据不同幻读&#xff1a;一个事务按照条件查询数据时&#xff0c;没有对应的数据行&#xf…

Oracle EBS AR接口和OM销售订单单价为空数据修复

最近,用户使用客制化Web ADI 批量导入销售订单行功能,把销售订单行的单价更新成空值,直到发运确认以后,财务与客户对帐才发现大量销售订单的单价空,同时我们检查AR接口发现销售订单的单价和金额均为空。 前提条件 采用PAC成本方式具体问题症状 销售订单行的单价为空 Path:…

Redhat 7.9 安装dm8配置文档

Redhat 7.9 安装dm8配置文档 一 创建用户 groupadd -g 12349 dinstall useradd -u 12345 -g dinstall -m -d /home/dmdba -s /bin/bash dmdba passwd dmdba二 创建目录 mkdir /dm8 chown -R dmdba:dinstall /dm8三 配置/etc/security/limits.conf dmdba soft nproc 163…

在CentOS 7上安装Python 3.7.7

文章目录 一、实战步骤1. 安装编译工具2. 下载Python 3.7.7安装包3. 上传Python 3.7.7安装包4. 解压缩安装包5. 切换目录并编译安装6. 配置Python环境变量7. 使配置生效8. 验证安装是否成功 二、实战总结 一、实战步骤 1. 安装编译工具 在终端中执行以下命令 yum -y groupin…

XRDP登录ubuntu桌面闪退问题

修改 /etc/xrdp/startwm.sh unset DBUS_SESSION_BUS_ADDRESS unset XDG_RUNTIME_DIR . $HOME/.profile

javascript常见的事件属性

焦点事件 focus/blur <input type"text" /><script>const input document.querySelector("input")// 绑定焦点事件input.addEventListener("focus" ,function(){console.log("有焦点触发")})// 失去焦点事件input.addEve…

Git分支提交时自动大写 fatal: the remote end hung up unexpectedly

先说结论&#xff1a; 进入 .git/refs/heads目录&#xff0c;会看到Feature文件夹&#xff0c;重命名为feature即可。 表现&#xff1a; 通过终端命令创建的分支 git checkout -b feature/name 使用git push后自动变成了Feature/name 并且有时候在本地创建feature/1234567…