二、9.硬盘驱动程序

文件系统是运行在操作系统中的软件模块,是操作系统提供的一套管理磁盘文件读写的方法和数据组织、存储形式,因此,文件系统=数据结构+算法,哈哈,所以它是程序。它的管理对象是文件,管辖范围是分区,因此它建立在分区的基础上,每个分区都可以有不同的文件系统。但咱们刚创建了磁盘而己,磁盘还是裸盘,即传说中的 raw disk 。

  1. 盘片:类似光盘中的一个圆盘,上面布满了磁性介质。
  2. 扇区:扇区是硬盘读写的基本单位,它在磁道上均匀分布,与磁头和磁道不同,扇区从 1 开始编号。扇区的大小字节数=256 × N. -N为自然数。通常取N 为 2,因此扇区大小为 512 字节。也许有读者会说,我怎么听说的文件存储都是以簇或块为单位的?“簇”或“块”这些是操作系统中读写数据的单位,并不是磁盘原生支持的,一个簇或块等于 1 个以上的扇区。因为磁盘本身就是整个机器的瓶颈,它是速度较低的设备,若操作系统总去访问这些低速设备就太浪费时间了,因此操作系统不可能一次只写一个扇区,为了优化 1/0,操作系统把数据积攒到“多个扇区”时再一次性写入磁盘,这里的“多个扇区”就是指操作系统的簇或块。通常标准库函数还进行了二次优化,数据可以积攒到多个族或块时才写入,不过标准库中还提供了控制选项,可以立即把数据刷进硬盘。
  3. 磁道:盘片上的一个个同心圈就是磁道,它是扇区的载体,每一个磁道由外向里从 0 开始编号。同一盘片上的每一个磁道上都由扇区组成,即磁道其实是一圈扇区。磁盘上的磁道数取决于制作工艺。有的同学又说了,离圆心近的磁道与最外圄的磁道周长肯定不一致,那这两种磁道上的扇区数一样吗?答案是:老硬盘是一样的,新式硬盘中已经改进了,外圈磁道会容纳更多的扇区,在新硬盘中有个地址转换器来兼容老硬盘的扇区寻址,因此咱们依然可以认为硬盘中每个磁道上的扇区数一样多。
  4. 磁头:就是磁头,哈哈,可以粗略理解为磁带中的磁头。毕竟需要某个设备来读盘片上的数据,这个设备就是磁头。 一个盘片分为上下两个面,各面都有一个磁头,因此一个盘片包括两个磁头,磁头号就表示盘面,平时所说的盘面号就是磁头号。虽然单个盘片的容量不断在增长,但其潜力毕竟是有限的。为了实现大容量,硬盘中必须由多个盘片来组成。既然有多个盘片,两个磁头就自然不够用了,肯定要有盘片×2 个磁头,磁头编号由上到下从 0 开始。
  5. 柱面:硬盘是整个计算机系统中很大的瓶颈了,如何才能让硬盘的读写更快,工程师们想到了并行写入的方式。这个并行就是指多个磁头同时写入。也就是通常我们在写一个文件时,是由多个磁头同时写入到不同的盘面中编号位置相同的磁道上,采用并行的方式,读写速度是单盘的(磁头数)倍。这些由不同盘面上的编号相同的磁道(这些编号相同的同心圆大小一致)从上到下所组成的圆柱体的回转面就称为柱面,因此柱面的大小等于盘面数(磁头数〉乘以每磁道扇区数。既然一组编号相同的磁道是柱面,而且柱面上的所有磁道号都相同,所以磁道号就称为柱面号。
  6. 分区:是由多个编号连续的柱面组成的,因此分区在物理上的表现是由某段范围内的所有柱面组成的通心环,并不是像“饼图”那种逻辑表示,当然若整个硬盘只有 1 个分区,那这个分区就是个所有柱面组成的圆柱体 。 分区不能跨柱面,也就是同一个柱面不能包含两个分区,一个柱面只属于一个分区,分区的起始和终止都落在完整的柱面上,并不会出现多个分区共享同一柱面的情况,这就是所谓的“分区粒度”。因此,分区大小等于“每柱面上的扇区数”乘以“柱面数”,这就是我们实际分区时,键入的大小往往与实际大小不同的原因,分区大小总会是“每柱面上的扇区数”的整数倍,也就是会以柱面向上取整。假如硬盘上有 n 个柱面, l~ 10 柱面是 a 分区, 11 ~23 是 b 分区,这两个分区不共享 11 号柱面。

硬盘容量=单片容量×磁头数。
单片容量=每磁道扇区数×磁道数× 512 字节。
磁道数又等于柱面数
硬盘容量=每磁道扇区数×柱面数× 512 字节×磁头数

一般情况下,每磁道扇区数都是 63 ,扇区大小都是 512,柱面数和磁头数取决于实际配置的。因此,在硬盘容量己知的情况下: 柱面数×磁头数=硬盘容量/63/512,我们只要凑出合适的柱面数和磁头数就行了。


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

为了拓展更多分区,硬盘厂商准备在分区“描述符”动动手脚。在这个“描述符”中有个属性是文件系统 id,它表示文件系统的类型,为支持更多的分区,专门增加一种 id 属性值( id 为 5 ),用来表示该分区可被再次划分出更多的子分区,这就是逻辑分区。因为只是在分区表项中通过属性来判断分区类型,所以这 4 个分区中的任意一个都可以作为扩展分区 。 扩展分区是可选项,有没有都行,但最多只有 1 个, 1 个扩展分区在理论上可被划分出任意多的子扩展分区,因此 1 个扩展分区足够了。注意了,这里所说的是理论上支持无限多的划分,由于硬件上的限制,分区数量也变得有限,比如 ide 硬盘只支持的个分区, scsi 硬盘只支持 15 个分区 。 硬盘本来没有扩展分区的概念,为了突破 4 个分区的限制才提出了扩展分区,为了区别这一概念,将剩下的 3 个区称为主分区 。

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

分区表中共 4 个分区,哪个做扩展分区都可以,扩展分区是可选的,但最多只有 1 个,其余的都是主分区。在过去没有扩展分区时,这 4 个分区都是主分区 i 为了兼容 4 个主分区的情况,扩展分区中的第 1 个逻辑分区的编号从 5 开始。


磁盘分区表浅析

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

磁盘分区表就是个数组,此数组长度固定为 4,数组元素是分区元信息的结构。

最初的磁盘分区表位于 MBR 引导扇区中,咱们先看看原计原昧的 MBR 引导扇区的逻辑结构。早在加载loader 时就和大伙儿介绍过MBR, MBR (Main Boot Record )即主引导记录,它是一段引导程序,其所在的扇区称为主引导扇区,该扇区位于 0 盘道 1 扇区(物理扇区编号从 1 开始,逻辑扇区地址 LBA 从 0 开始),也就是硬盘最开始的扇区,扇区大小为 512 宇节,这 512 字节内容由三部分组成。
(1) 主引导记录 MBR 。
(2)磁盘分区表 DPT 。
(3)结束魔数 55AA ,表示此扇区为主引导扇区,里面包含控制程序。

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

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

魔数 55AA 作为主引导扇区的有效标志,位于扇区偏移 0x1FE~0x1FF,也就是最后 2 个字节。
以上这三部分便是 主引导扇区 的主要结构。

在硬盘中,最开始的扇区是 MBR 引导扇区,接着是空闲的多个扇区,随后是具体的分区 。

image-20230819154942346

对于不够一个柱面的剩余的空间一般不再利用,并不参与分区。除去 MBR 引导扇区占用的 1 扇区,这部分剩余空间是 62 个扇区。这个空间按理说不属于操作系统的范围,但操作系统确实是有能力支配它。由于仅仅是 62 个扇区的空间,能用它做的事情有限,因为空间太小会使扩展性很差,比如文件系统中的块位图大小是与分区大小成正比的,若将块位图存放在此处,受到这 62 个扇区的限制,能管理的分区范围也将大大缩水,因此很少有操作系统会用到这个磁道,我们也不用它 。


随着磁盘容量越来越大,对于磁盘管理来说, 4 个分区显然不够用了,因此发明了扩展分区,寄望以此来解决原来所支持的分区数太少的问题。大伙儿知道,“分区表的长度固定”才是问题的症结所在 。 按理来说,扩展分区中包含多少个逻辑分区,扩展分区的分区表中就该有多少表项,可是任何时候新事物的发展都要把“向上兼容”当成头等大事,它既要兼容此固定长度为 4 个分区的分区表,又要突破固定分区数的限制,这似乎有点为难,该怎样设计扩展分区的分区表呢? 一个两全其美的方案是视这个扩展分区为总扩展分区,将它划分成多个子扩展分区,每个子扩展分区“在逻辑上”相当于硬盘,因此每个子扩展分区都可以有 1 个分区表。这样一来,各个分区表的长度依然固定为 4,但是允许有无限多个分区表,分区表项多了,自然支持的分区数就多了。如何将这些分区表组织到一起呢?扩展分区表采用链式结构,将所有子扩展分区的分区表串在一起,形成可容纳无限个分区表的单向链表。链表是要有结点的,这里的每个分区表就是结点,一般的链表结点除了包括数据外,还必须要包括下一个结点的地址,分区表也采用了这种结构,其表项就分为两部分, 一部分是描述逻辑分区的信息,另一部分是描述下一个子扩展分区的地址 。

一个磁盘最多支持4个主分区,就是简单的CDEF分盘,但是当主分区无法覆盖硬盘大小时,多余出来的叫扩展分区,扩展分区只能有一个,但在其上可以创建多个逻辑分区,从而实现更多的分盘(分区)。当有多个硬盘时,为了将多个硬盘的空间统一利用,就出现了卷,卷可以跨越多个物理硬盘进行工作,换句话说,卷把硬盘间的边界打通了,同一个分盘的数据可以存在多块硬盘上

要想使用分区,就离不开分区表,逻辑分区也是分区,为了使用它,也需要有元信息来描述它的范围、边界、类型等信息,因此在子扩展分区中也要有分区表来描述这些逻辑分区。分区表本身也要在子扩展分区中占磁盘空间,因此实际情况是每个子扩展分区的空间并不是只有逻辑分区,在每个子扩展分区中最开始的扇区(剧透一下,此扇区称为 EBR 引导扇区,马上要介绍它)用于存储此子扩展分区中的分区表,此扇区中的内容也是前 446 字节是引导程序,中间 64 字节是分区表,后 2 字节是 0x55 和 0xAA,您看,它同 MBR 引导扇区的结构相同。紧随其后的是空闲的一部分扇区,其余剩下的大部分扇区才被用作存储数据的分区,即逻辑分区。因此,子扩展分区包含逻辑分区,

有了扩展分区后,出于兼容性考虑,这种“观念”依然被传承下来:扩展分区被划分出多个子扩展分区,每个子扩展分区都有自己的分区表,所以子扩展分区在逻辑上相当于单独的硬盘,各分区表在各个子扩展分区最开始的扇区中,该扇区同MBR 引导扇区结构相同,由于是经扩展分区划分出来的,所以它们称为 EBR,即扩展引导记录 。 MBR 只有 1 个, EBR理论上有无限个, MBR 和 EBR 所在的扇区统称为引导扇区,它们的结构是相同的, MBR 中有的 E岛1R 中也有。我想这下您清楚了我为什么要把子扩展分区视为硬盘,因为各子扩展分区的结构也同整个硬盘结构一样,最开始的扇区都是引导扇区,中间都是空闲一小部分扇区,最后的大片扇区空间作为数据存储的分区 。


由于扩展分区采用了链式分区表,理论上可以存在无限个分区表,支持无限个逻辑分区。每一个逻辑分区所在的子扩展分区都有一个与 MBR 结构相同的 EBR, EBR 中分区表的第一分区表项用来描述所包含的逻辑分区的元信息,第二分区表项用来描述下一个子扩展分区的地址,第三、四表项未用到。

值得一提的是这两个分区表项都是指向一个分区的起始,起始地址都是个扇区地址,只不过第一个分
区表项指向的是该逻辑分区最开始的扇区,此扇区称为操作系统引导扇区,即 OBR 引导扇区 。第二个分
区表项指向下一个子扩展分区的 EBR 引导扇区。

OBR 引导扇区不是 EBR 引导扇区。 EBR 不属于分区之内,不属于操作系统管理的范围。而 OBR 引导扇区位于分区(主分区和逻辑分区)最开始的扇区,属于操作系统管理的范围 。

image-20230819165331171

活动分区是指引导程序所在的分区,活动分区标记是给 MBR 或其他需要移交 CPU 使用权的程序看的,它们通过此位来判断该分区的引导扇区中是否有可执行的程序,也就是引导程序,这个引导程序通常是操作系统内核加载器,故此引导程序通常被称为操作系统引导记录,即 OBR (OS Boot Record ) 。 如果MBR 发现该分区表项的活动分区标记为 0x80,这就表示该分区的引导扇区中有引导程序(这是 MBR 与分区工具或操作系统约定好的), MBR 就将 CPU 使用权交给此引导程序, 如果此引导程序是操作系统或其加载器,此时操作系统便掌握了 CPU 使用权,也就是大家平时所说的加载内核。这里一直说的“分区引导扇区”是位于分区最开始的扇区,是分区引导程序所在的扇区,由于此引导程序通常都是操作系统内核加载器,故此扇区被称为操作系统引导扇区,也就是 OBR 所在的扇区,即 OBR 引导扇区。

注意啦, OBR引导扇区并不是 EBR 或 MBR 引导扇区,它们虽然都包含引导程序,并且都以 0x55 和 0xAA 结束,但它们最大的区别是分区表只包含在 MBR 和 EBR 中, OBR 中可没有分区表。 MBR 和 EBR 所在的扇区不属于分区范围之内,它们是由分区工具创建并管理的,因此不归操作系统管理,操作系统不可以随意往里面写数据,尽管操作系统有能力这样做。而 OBR 引导扇区是分区中最开始的扇区,归操作系统的文件系统管理,因此操作系统通常往 OBR 引导扇区中添加内核加载器的代码,供 MBR 调用以实现操作系统自举,总之 OBR 引导扇区中绝对不包括分区表。


“分区起始偏移扇区”是个相对量,它表示各分区的起始扇区地址是相对于某“基准”的偏移扇区数,各分区的绝对扇区 LBA 地址=“基准”的绝对扇区起始 LBA 地址+各分区的起始偏移扇区。这个“基准”是指分区所依赖的上层对象,或者说是创建该分区的父对象。我知道这么还是说太抽象了,有必要再深入讨论。

硬盘分区

先梳理下分区层次关系,前面己述,总扩展分区被直接拆分成多个子扩展分区,子扩展分区又被拆分成 EBR 引导扇区、空闲扇区和逻辑分区三部分。“基准”也因分区类型而异。

对于逻辑分区而言,逻辑分区是在子扩展分区中拆分出来的,其具体地址依赖于子扩展分区自身的起始地址,因此逻辑分区的基准是子扩展分区的起始扇区 LBA 地址。

对于子扩展分区而言,子扩展分区是在总扩展分区中拆分出来的,其具体地址依赖于总扩展分区自身的起始地址,因此子扩展分区的基准是总扩展分区的起始扇区 LBA 地址。

对于主分区或总扩展分区而言,这两类分区本身是独立、无依赖的分区,因此基准为 0。此概念咱们在实践中去理解。

“分区容量扇区数”意义比较明确,就是表示分区的容量扇区数。

子扩展分区是在总扩展分区中创建的,子扩展分区的偏移扇区理应以总扩展分区的绝对扇区 LBA 地址为基准,因此,"子扩展分区的绝对扇区 LBA 地址=总扩展分区绝对扇区 LBA 地址+子扩展分区的偏移扇区”。
逻辑分区是在子扩展分区中创建的,逻辑分区的偏移扇区理应以子扩展分区的绝对扇区 LBA 地址为基准,因此,“逻辑分区的绝对扇区 LBA 地址=子扩展分区绝对扇区 LBA 地址+逻辑分区偏移扇区飞这里的子扩展分区就是当前子扩展分区。

硬件是独立的个体,它提供一套方法作为操作接口给外界调用,但此接口往往是最原始、最简陋、最繁琐的,相对咱们习惯的高级语言来说,这些接口使用起来非常麻烦,很多指令要提前设置好各种参数,基本上都是要用汇编语言来操作寄存器。基于此描述,对于驱动程序我个人的理解是驱动程序是对硬件接口的封装,它把参数设置等重复、枯燥、复杂的过程封装成一个过程,避免每次执行命令时都重复做这些工作,根据需要也可以提供相关的策略,如缓存等,让硬件操作更加容易、省事、方便,无需再显式做一些底层设置。因此没有驱动程序的话,操作系统也是可以同硬件交流的,无非是直接操作 IO 端口

硬盘初始化

为了支持硬盘操作,咱们还有几件事要做。硬盘上有两个 ata 通道,也称为 IDE 通道。第 1 个 ata 通道上的两个硬盘(主和从)的中断信号挂在 8259A 从片的 IRQ14 上,没错,是两个硬盘共享同一个 IRQ接口,也许您在想,硬件发生中断时,如何区分中断是来自哪个硬盘的?是这样的,硬盘发生中断的条件是咱们对硬件执行了某些命令,然后硬盘完成任务后才发中断,咱们在对硬盘发命令时,需要提前指定是对主盘,还是从盘操作,这是在硬盘控制器的 device 寄存器中第 4 位的 dev 位指定的 ,因此自然就知道是哪个硬盘来了中断信号,具体的作法咱们在后面实现的部分再说。

顺便说一下,第 2 个 ata 通道接在 8259A 从片的 IRQ15 上,该 ata 通道上可支持两个硬盘 。 来自 8259A 从片的中断是由 8259A 主片帮忙向处理器传达的, 8259A 从片是级联在 8259A 主片的 IRQ2 接口的,因此为了让处理器也响应来自 8259A 从片的中断,屏蔽中断寄存器必须也把 IRQ2 打开

/* 初始化可编程中断控制器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, 0xf8);

    /* 打开从片上的IRQ14,此引脚接收硬盘控制器的中断 */
    outb (PIC_S_DATA, 0xbf);

    put_str("   pic_init done\n");
}

在中断处理程序中,如果中断源是来自从片 8259A 的话,在发送中断结束信号 EOI 的时候,主片和从片都要发送。否则,将无法继续响应新的中断。不过咱们的中断处理程序一直都是向主从两片 8259A 同时发送 EOI 。

还有一件小事要做,完全是为了让咱们自己方便。目前咱们在内核中打印输出时,都是用 console_put_xxx 之类的函数,可是自从咱们为用户进程实现了 printf 函数后,越来越觉得 console_put_xxx 系列的函数不方便,为此咱们也心疼下自己,在内核中实现格式化输出函数 printk。 printk 和 printf 原理是一样的,只是 printk 是专门用在内核态的格式化输出函数,它是 printf 的孪生兄弟。由于是在内核态下实现,因此不需要系统调用啦,所以实现起来更简单了

#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);
}
//硬盘相关数据结构
/* 分区表结构 */
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;		 // 本通道的起始端口号
    uint8_t irq_no;		 // 本通道所用的中断号
    struct lock lock;		 // 通道锁
    bool expecting_intr;		 // 表示等待硬盘的中断
    struct semaphore disk_done;	 // 信号量,用于阻塞、唤醒驱动程序
    struct disk devices[2];	 // 一个通道上连接两个硬盘,一主一从
};

宏定义硬盘寄存器端口号

image-20230819184356486

/* 针对两个ata通道,定义硬盘各寄存器的端口号 */
#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
#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");
    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");
}

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

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() {
	...
    /* 如果就绪队列中没有可运行的任务,就唤醒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);
}

/* 主动让出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;//将当前任务的status置为TASK_READY
   schedule();
   intr_set_status(old_status);
}

/* 初始化线程环境 */
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");
}

实现简单的休眠函数

为避免浪费 CPU 资源,在等待硬盘操作的过程中最好把 CPU 主动让出来,让 CPU 去执行其他任务

#define IRQ0_FREQUENCY	   100 //时钟中断频率
#define mil_seconds_per_intr (1000 / IRQ0_FREQUENCY) //每多少毫秒发生一次中断
uint32_t ticks;          // ticks是内核自中断开启以来总共的嘀嗒数

/* 以tick为单位的sleep,任何时间形式的sleep会转换此ticks形式 */
static void ticks_to_sleep(uint32_t sleep_ticks) {//让任务休眠 sleep_ticks 个 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); 
}

完善硬盘驱动程序

/* 选择读写的硬盘,主盘OR从盘 */
static void select_disk(struct disk* hd) {
    uint8_t reg_device = BIT_DEV_MBS | BIT_DEV_LBA;//device寄存器关键位
    if (hd->dev_no == 1) {	// 若是从盘就置DEV位为1
        reg_device |= BIT_DEV_DEV;
    }
    outb(reg_dev(hd->my_channel), reg_device);//将变量 reg_device 写入硬盘所在通道的 device 寄存器
}

/* 向硬盘控制器写入起始扇区地址及要读写的扇区数 */
//硬盘指针 hd、扇区起始地址 lba、扇区数 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位,不用单独取出低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秒 */
//硬盘是个低速设备,因此在其响应过程中,驱动程序可以让出 CPU 使用权使其他任务得到调度,这就是 busy_wait 的作用。
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毫秒(其实是让出CPU,其由ticks_to_sleep实现)
        }
    }
    return false;
}


/* 从硬盘 hd 的扇区地址 lba 处读取 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) {//读写扇区数端口时8位寄存器,所以当读写的端口数超过 256 时,必须拆分成多次读写操作
        if ((secs_done + 256) <= sec_cnt) {//未读取的扇区数超过256
            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扇区数据写入硬盘hd的lba扇区 */
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);
}

/* 硬盘中断处理程序 */
//irq_no 要么等于0x2e,要么等于0x2f,它们分别是从片 8259A 的 IRQ14 接口和 IRQ15接口
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);//唤醒阻塞在disk_done上的驱动程序

        /* 读取状态寄存器使硬盘控制器认为此次的中断已被处理,从而硬盘可以继续执行新的读写 */
        inb(reg_status(channel));//读一次 status 寄存器,清除硬盘控制器的中断
    }
}

/* 硬盘数据结构初始化 */
void ide_init() {
    printk("ide_init start\n");
    ...

    /* 处理每个通道上的硬盘 */
    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");
}

获取硬盘信息,扫描分区表

identify 命令是 0xec,它用于获取硬盘的参数,不过奇怪的是此命令返回的结果都是以字为单位,并不是字节

分区表扫描的工作稍微复杂一些,咱们需要以 MBR 引导扇区为入口,遍历所有主分区,然后找到总扩展分区,在其中递归遍历每一个子扩展分区,找出逻辑分区。由于涉及到分区的管理,因此我们得给每个分区起个名字,简单起见,最好咱们借鉴现成的 Linux 设备命名方案。 Linux 中所有的设备都在/dev/目录下,硬盘命名规则是[x]d[y][n],其中只有字母 d 是固定的,其他带中括号的字符都是多选值,下面从左到右介绍各个字符。

  • x 表示硬盘分类,硬盘有两大类, IDE 磁盘和 SCSI 磁盘。 h 代表 IDE 磁盘, s 代表 SCSI 磁盘,故 x 取值为 h 和 s
  • d 表示 disk,即磁盘
  • y 表示设备号,以区分第几个设备,取值范围是小写字符,其中 a 是第 1 个硬盘, b 是第 2 个硬盘,依次类推
  • n 表示分区号,也就是一个硬盘上的第几个分区。分区以数字 1 开始,依次类推

综上所述, sda 表示第 1 个 SCSI 硬盘, hdc 表示第 3 个 IDE 硬盘, sda1 表示第 1 个 SCSI 硬盘的第 1 个分区, hdc3 表示第 3 个 IDE 硬盘的第 3 个分区。

/* 用于记录总扩展分区的起始lba,初始为0,partition_scan时以此为标记 */
int32_t ext_lba_base = 0;
//此变量有两个作用,一是作为扫描分区表的标记, partition_scan 若发现 ext_lba_base 为 0 便知道这是第一次扫描,因此初始为 0。另外就是用于记录总扩展分区地址,那时肯定就不为 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));	 // gcc特有关键字,paeked 就是“特殊处理”,意为压缩的,即不允许编译器为对齐而在此结构中填充空隙,保证此结构是16字节大小

/* 引导扇区,mbr或ebr所在的扇区 */
struct boot_sector {
    uint8_t  other[446];		 // 引导代码,占位,在引导扇区中偏移 446 字节的地方才是分区表
    struct   partition_table_entry partition_table[4];       // 分区表中有4项,共64字节
    uint16_t signature;		 // 启动扇区的结束标志是0x55,0xaa,
} __attribute__ ((packed));

/* 将dst中len个相邻字节交换位置后存入buf */
/*用来处理 identify 命令的返回信息,硬盘参数信息是以字为单位的,包括偏移、长度的单位都是字,在这 16 位的字中,相邻字符的位置是互换的,所以通过此函数做转换。*/
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';
}

/* 向硬盘发送 identify 命令以获得硬盘参数信息 */
static void identify_disk(struct disk* hd) {
    char id_info[512];//存储向硬盘发送 identify 命令后返回的硬盘参数
    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);//从硬盘获取信息到id_info

    char buf[64];
    uint8_t sn_start = 10 * 2, /*序列号起始字节地址,10是字偏移量*/
    		sn_len = 20,
    		md_start = 27 * 2, /*型号起始地址,27是字偏移量*/
    		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的扇区中的所有分区 */
/*每个子扩展分区中都有 1 个分区表,因此需要针对每一个子扩展分区递归调用,每调用一次,都要用 1 扇区大小的内存来存储子扩展分区所在的扇区,即 MBR 引导扇区或 EBR 引导扇区。注意,由于是递归调用,每次函数未退出时又进行了函数调用,这会导致栈中原函数的局部数据不释放,并且会在栈中生成新的局部变量,尤其是局部变量很大时,这种递归调用会使栈的内存空间消耗量很大,因此用于存储分区表扇区的内存绝对不能用局部变量。*/
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表示是第一次读取引导块,也就是MBR主引导记录所在的扇区
                /* 记录下扩展分区的起始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 数组中
                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");
    ...

        /* 处理每个通道上的硬盘 */
        while (channel_no < channel_cnt) {
            ...

                /* 分别获取两个硬盘的参数及分区信息 */
                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");
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言小白急救 指针初级讲解(四千字教程)

系列文章目录 C语言小白急救 表达式求值&#xff08;两千字教程&#xff09; C语言小白急救 操作符详解(8千字保姆级教程) C语言小白急救 扫雷游戏&#xff08;万字保姆级教程&#xff09; C语言小白急救 使用C语言编写‘三子棋‘ 文章目录 系列文章目录[C语言小白急救 表达式…

Docker的基本使用

Docker 概念 Docker架构 docker分为客户端&#xff0c;Docker服务端&#xff0c;仓库 客户端 Docker 是一个客户端-服务器&#xff08;C/S&#xff09;架构程序。Docker 客户端只需要向 Docker 服务端发起请求&#xff0c;服务端将完成所有的工作并返回相应结果。 Docker …

设计模式之创建者模式

文章目录 一、介绍二、应用三、案例1. 麦当劳11随心配2. 代码演示3. 演示结果 四、优缺点五、送给读者 一、介绍 建造者模式(Builder Pattern)属于创建型设计模式&#xff0c;很多博客文章的对它的作用解释为用于将复杂对象的创建过程与其细节表示分离。但对于初学者来说&…

机器学习深度学习——NLP实战(情感分析模型——textCNN实现)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——NLP实战&#xff08;情感分析模型——RNN实现&#xff09; &#x1f4da;订阅专栏&#xff1a;机器学习…

Lnton羚通算法算力云平台【PyTorch】教程:torch.nn.Softsign

torch.nn.Softsign 原型 CLASS torch.nn.Softsign() 图 代码 import torch import torch.nn as nnm nn.Softsign() input torch.randn(4) output m(input)print("input: ", input) print("output: ", output)# input: tensor([ 0.0046, -0.4135, -2…

003-Nacos 2.1.x 注册实例源码分析

目录 Nacos 2.1.X注册实例入口接口流程Client 注册 事件处理 服务订阅入口 Nacos 2.1.X 注册实例 入口 com.alibaba.nacos.naming.remote.rpc.handler.InstanceRequestHandler#handleService service Service.newService(request.getNamespace(), request.getGroupName(), r…

7-10 查验身份证

分数 15 全屏浏览题目 切换布局 作者 陈越 单位 浙江大学 一个合法的身份证号码由17位地区、日期编号和顺序编号加1位校验码组成。校验码的计算规则如下&#xff1a; 首先对前17位数字加权求和&#xff0c;权重分配为&#xff1a;{7&#xff0c;9&#xff0c;10&#xff0c…

dockerfile编写LNMP

目录 1. 项目环境 2. 服务器环境 二、部署nginx&#xff08;容器IP为192.168.158.26&#xff09; 1、整个Dockerfile文件内容 ​编辑 2、配置nginx.conf文件 3、构建镜像 三、部署mysql 1、整个Docker文件内容 3、生成镜像 4、启动镜像容器 5、验证mysql 四、PHP部署 1…

深入理解Semaphore

Semaphore&#xff08;信号量&#xff09;是操作系统中PV操作的原语在java中的实现&#xff0c;它也是基于AQS实现的。其中PV操作是操作系统中一种实现进程互斥与同步的有效方法。PV操作与信号量&#xff08;S&#xff09;的处理有关&#xff0c;P表示通过&#xff0c;V表示释放…

SpringCloud Gateway服务网关的介绍与使用

目录 1、网关介绍2、SpringCloudGateway工作原理3、三大组件3.1 、Route&#xff08;路由&#xff09;3.2、断言 Predicate3.3、过滤器 filter 4、Gateway整合nacos的使用4.1 、引入依赖4.2、 编写基础类和启动类4.3、 编写基础配置和路由规则4.4 、测试结果 1、网关介绍 客户…

面试之HTTP

1.HTTP与HTTPS的区别 HTTP运行在TCP之上&#xff1b;HTTPS是运行在SSL之上&#xff0c;SSL运行在TCP之上两者使用的端口不同&#xff1a;HTTP使用的是80端口&#xff0c;HTTPS使用的是443端口安全性不同&#xff1a;HTTP没有加密&#xff0c;安全性较差&#xff1b;HTTPS有加密…

为什么选择elasticsearch分布式搜索引擎

文章目录 &#x1f52d;什么是elasticsearch&#x1f320;ELK技术栈&#x1f320;elasticsearch和lucene&#x1f320;为什么不是其他搜索技术&#xff1f; &#x1f52d;总结 &#x1f52d;什么是elasticsearch elasticsearch是一款非常强大的开源搜索引擎&#xff0c;具备非常…

让智慧城市更进一步,无人机解决方案全面应用

在城市规划中&#xff0c;无人机正在颠覆传统的操作和思维方式。这种技术不仅改变了城市管理获取和分析信息的方式&#xff0c;还提供了前所未有的视角&#xff0c;使城市管理能够更加明智地制定策略。 1. 数据采集的新纪元&#xff1a; 城市规划的核心在于数据的收集和分析。…

Mysql5.7.36主从同步实操

主库创建同步账户 #创建备份的账户 CREATE USER backup192.168.32.1 IDENTIFIED BY backup123; #给账户授予备份的权限 GRANT REPLICATION SLAVE ON *.* TO backup192.168.32.1; #刷新权限 FLUSH PRIVILEGES;停止主库 配置主库需要的备份参数 打开my.ini文件&#xff0c;配置…

Hive(一)

一、DDL 1、数据库操作 1&#xff09;、创建数据库 语法&#xff1a; CREATE DATABASE [IF NOT EXISTS] database_name [COMMENT database_comment] [LOCATION hdfs_path] [WITH DBPROPERTIES (property_nameproperty_value, ...)]; 案例&#xff1a; &#xff08;1&…

卷积神经网络——下篇【深度学习】【PyTorch】

文章目录 5、卷积神经网络5.10、⭐批量归一化5.10.1、理论部分5.10.2、代码部分 5.11、⭐残差网络&#xff08;ResNet&#xff09;5.11.1、理论部分5.11.2、代码部分 话题闲谈 5、卷积神经网络 5.10、⭐批量归一化 5.10.1、理论部分 批量归一化可以解决深层网络中梯度消失和…

Anaconda, Python, Jupyter和PyCharm介绍

目录 1 Anaconda, Python, Jupyter和PyCharm介绍 2 macOS通过Anaconda安装Python, Jupyter和PyCharm 3 使用终端创建虚拟环境并安装PyTorch 4 安装PyCharm并导入Anaconda虚拟环境 5 Windows操作系统下Anaconda与PyCharm安装 6 通过 Anaconda Navigator 创建 TensorFlow 虚…

静态代码扫描持续构建(Jenkins)

前提条件 已正确安装、配置Jenkins环境&#xff0c;并装有 Gradle 插件、HTML 插件、SVN 插件等。如下图所示&#xff1a; 已正确安装、配置android sdk&#xff0c;在cmd窗口输入命令“android -h”,回车 配置步骤 打开Jenkins&#xff0c;新建一个job&#xff0c;输入项目…

ABAP 定义复杂的数据结构

最近有个需求是实现ABAP数据类型与JASON类型的转换。想要创建个ABAP的数据类型来接JASON类型是个挺麻烦的事。例如下面这个JASON数据&#xff0c;是个很简单的数据结构。但对ABAP来说有4层了&#xff0c;就有点复杂了。 不过ABAP的数据类型也是支持直接定义数据结构的嵌套的。如…

Nginx特性应用及载装

Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少&#xff0c;并发能力强&#xff0c;事实上nginx的并发能力在同类型的网页服务器中表现较好&#xff0c;中国大陆使用nginx的网站有&#xff1a;网易、腾讯、阿里等。 …