不太好用比较好看的格式来说明这章的内容,就我个人的感受来说,主要是科普了一下计算机和硬盘是如何交互的,顺便对外部设备的驱动编写有了一点比较模糊的认识。
名次解释
首先是关于硬盘的几个名词解释:
- 盘面:磁盘上的任何一面都能称之为盘面
- 柱面:将多个磁盘叠在一起,相同磁道号构成的圆柱面
- 磁头:用于读写磁盘的设备,一个磁盘上下两面各有一个
- 磁道:任何一个磁盘上用于储存数据的带磁同心圆
- 分区:认为界定一个磁盘各个区域的名词
- 扇区:标准扇区512字节,每个磁道由多个扇区构成
反正具体的样貌大概都能搜出来,名次解释并没有太大意义,这里写出来是为了让文章看起来比较舒服。
另外还需要记录一点,有关磁盘储存数据的方式:
- 每个主盘的第一个磁道用于存放MBR,而MBR只占用一个扇区,多余扇区往往不使用。一个磁道一般63个扇区(过去是这样,现在更多更大了,但出于向前兼容的缘故,应该认为每个磁道的扇区数相同)
- 第一个扇区除了MBR外还需要存放64字节的分区表,分区表记录整块磁盘的分区数据
不过现代硬盘为了支持更多的分区(早期只支持4个主分区),引申出了逻辑分区的概念。将硬盘分为3个主分区和一个逻辑分区。
逻辑分区是理论上可以无限被分割的分区,它为从自身再分配出去的每个分区单独赋予一张分区表,每张分区表通过隐式链接的方法可以追溯到下一个分区。
每个分区的第一个磁道都是引导记录,只是第一个分区的叫做MBR(Main Boot Record),其他的都叫做EBR(Extended Boot Record)。而每个分区的第二个磁道开始还放了一个OBR(OS Boot Record)。EBR和MBR是完全一样的结构,只是在名字上做了区别;而OBR则不同于MBR,它就是普通的存放在磁道上的数据而已,用于完成操作系统的自举。
分区表条目如下:
1 2 3 4 5 6 7 8 9 10 11 12
| 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字节大小
|
主要通过start_lba+sec_cnt*512来实现下一个分区的寻址,所以叫隐式链接。
本书有一张非常形象的图用以解释这个方法,不过因为我懒得拍一张下来,有兴趣的师傅可以去翻翻看,P577-图13-23。
IDE通道实现
操作系统和硬盘的交互主要是走IDE(Integrated Drive Electronics)通道,个人目前对IDE的认知是一套由操作系统实现的驱动接口。所以实现硬盘驱动就是在写IDE。
不过作者在本章才实现thread_yield,让这章的结构看起来有些混乱(虽然这似乎看起来是顺理成章的事情),所以关于thread_yield和idle的内容会放在本片笔记的结尾。
首先是关于操作系统如何与硬盘进行交互的内容:
- BIOS在启动之初就会像磁盘写入一系列数据,其中硬盘数量被写在0x475地址处
- 和之前的8259A芯片等设备相同,硬盘也提供了一些寄存器用以让操作系统向其发送指令,包括IDENTIFY、READ_SECTOR、WRITE_SECTOR三个指令。
- 操作系统向对应的寄存器中写入硬盘编号、起始偏移、所需扇区数后,待硬盘完成对应的寻址和返回操作以后,便能够从固定的端口读出硬盘数据
- 另外IDENTIFY指令发送后,硬盘会返回一系列有关硬盘本身的信息,可以用它们来构建整个硬盘的分区结构
- 硬盘也分主盘和从盘,在发送指令的时候需要指定发送的目标硬盘。当硬盘完成任务以后会触发8259A芯片上的IRQ14和IRQ15中断响应处理器
更加具体的操作直接看代码和注释吧。
init_ide:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| /* 硬盘数据结构初始化 */ void ide_init() { uint8_t hd_cnt = *((uint8_t*)(0x475));// 获取硬盘的数量 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"); }
|
过程并不复杂,根据注释大概就能理解过程了,细节参考一下本书代码中的结构体和讲解应该不难理解。
该函数主要是完成两个ide通道的初始化,让之后读取能够顺利进行:
1 2 3 4 5 6 7 8 9 10
| /* ata通道结构 */ struct ide_channel { char name[8]; // 本ata通道名称, 如ata0,也被叫做ide0. 可以参考bochs配置文件中关于硬盘的配置。 uint16_t port_base; // 本通道的起始端口号 uint8_t irq_no; // 本通道所用的中断号 struct lock lock; bool expecting_intr; // 向硬盘发完命令后等待来自硬盘的中断 struct semaphore disk_done; // 硬盘处理完成.线程用这个信号量来阻塞自己,由硬盘完成后产生的中断将线程唤醒 struct disk devices[2]; // 一个通道上连接两个硬盘,一主一从 };
|
identify_disk就是发送identify指令并获取硬盘信息,而partition_scan负责扫描该磁盘,并向hd中填入数据(换个说法吧,partition_scan会开始扫描磁盘,通过磁盘里每个MBR和EBR的分区表来初始化hd指针指向的结构体)。
select_disk:
1 2 3 4 5 6 7
| 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); }
|
写入硬盘寄存器,表示需要访问对应的磁盘
1 2 3 4 5 6 7 8 9 10
| static void select_sector(struct disk* hd, uint32_t lba, uint8_t sec_cnt) { struct ide_channel* channel = hd->my_channel; outb(reg_sect_cnt(channel), sec_cnt); // 如果sec_cnt为0,则表示写入256个扇区 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); }
|
同理,将需要写的扇区起始地址和需要访问的扇区数写入对应的寄存器。
完成之后,再向硬盘发送read指令然后挂起进程陷入沉睡,等待硬盘响应(发起中断)后,告诉硬盘可以继续发出中断后,从固定端口读写数据即可。
中断处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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)); } }
|
线程调度
最后是有关线程调度的新内容,首先是主动挂起:
1 2 3 4 5 6 7 8 9
| 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); }
|
就是简单的把自己设为ready状态并挂进等待队列而已。
另外一个是在调度队列中没有可调度的线程时,让调度器不至于出错而设定的线程:
1 2 3 4 5 6 7 8
| /* 系统空闲时运行的线程 */ static void idle(void* arg UNUSED) { while(1) { thread_block(TASK_BLOCKED); //执行hlt时必须要保证目前处在开中断的情况下 asm volatile ("sti; hlt" : : : "memory"); } }
|
hlt指令是让处理器停止运行,直到遇到中断为止。
初始化时会主动创建该线程并将其阻塞。当调度器在调度队列中找不到可调度的线程时,会主动唤醒该线程,该线程会手动阻塞自己并等待下一次中断发生。
插画ID:91443649