世界上第一台磁盘存储设备来自IBM,没错,那个蓝色巨人,几十年前科技的风向标,此设备用磁头来读写数据,用盘片来存储数据,以后的硬盘都是以这样的模式发展。这个磁头可以直接移动到盘片上的任何一块存储单元,从而实现了随机存储。虽然它的总容量只有5MB,但是限于当时的制造工艺水平,一共使用了50个直径为24英寸的盘片,摞起来的体积相当于冰箱那么大。在这些盘片表面都涂有用于存储数据的磁性物质,它们摞起来被固定在一起,在电机的带动下,绕着同一个轴旋转。
1968年由IBM首次提出了“温彻斯特/Winchester”技术,这项技术的精髓是“镀磁盘片在密封空间中高速自转,磁头悬浮在盘片上方,固定在磁头臂上沿盘片径向移动”。磁头不与盘片接触也不应该接触,长期的摩擦会销毁一切数据,盘片高速旋转会产生气流,磁头在这种气流下像飞碟一样悬浮,这样就保证了不会与盘片有摩擦。磁头被固定在磁头臂上,它能沿盘片径向移动,由于磁头和盘片各自的运动,再加上如此近的距离,所以哪怕一点灰尘都会造成磁盘的损伤。于是,磁头、盘片被密封在了一个盒子里。今天的硬盘依然是这样的结构。
当时这样的结构只是一种设想,这种设想在1973年被IBM蓝色巨人实现了,IBM 3340,容量60MB,由两个30MB的存储单元拼合而成。从此硬盘技术的发展便有了成形的结构基础。
硬盘发展到今天,这几十年来都是靠容量不断提升才打败竞争对手存活下来的。可是速度一直是其最大的敌人,家用电脑10年前硬盘主流转速是7200转/分钟,今天依然如此。硬盘的随机存取是靠磁头臂不断移动实现的,磁头臂移动到目标位置的时间称为寻道时间,如果存储的数据不连续,这一块那一片的,磁头就得不断调整位置,这是机械式硬盘不可避免的,这便是硬盘的瓶颈所在,所以一般的硬盘都将寻道时间作为重要参数。
在众多竞争对象当中,也有一款顽强地活了下来,它就是SSD固态硬盘,人家也有几十年的历史了,没想到吧,直到前几年SSD硬盘还是一种非常贵的设备,当时一般只有128G或者256G的固态硬盘用作启动盘,提高运行速度,而且三星这些固态硬盘大厂时不时失火跑水,固态硬盘和内存条这种电子产品都有涨价的时候,被人们戏称理财产品,但是长江存储等国产厂商攻克存储技术后,固态硬盘和内存就白菜价了,国外的那些大厂也不失火了,也不跑水了,虽然目前三星的固态依然贵,但是早已没有了之前的傲慢。有些核心技术不掌握在自己手中,始终就是任人宰割的羔羊。有些跑远了,回来回来,我们这章只讲硬盘,
讲硬盘就离不开这张图,目前主流个人电脑硬盘上的转速是7200转/分钟(这个数字几十年没边了)。每个盘片分上下两面,每面都存储数据,每个盘面都各由一个磁头来读取数据,故一个盘片上对应2个磁头。由于盘面与磁头是一一对应的关系,故用磁头号来表示盘面。磁头编号从上到下以0开始计数,所以用磁头0代表第一个盘面。磁头不会自己在盘片上移动,它需要被固定在右边的磁头臂上,在磁头臂的带动下,沿着盘片边缘向圆心的方向来回摆动,注意,摆动的轨迹是个弧,并不是绝对径向地直来直去。一方面这是因为磁头臂是由步进电机驱动的,磁头臂一端是步进电机主轴,另一端是磁头。步进电机每次都会转动一个角度,所以带动磁头臂在“画圆”,而磁头位于磁头臂的另一端,所以也跟着呈钟摆运动,运动轨迹是个弧线,并不是直线。所以,图3-28中磁头臂中标注了“类似于”径向运动,这就是“类似”的原因。另一方面,磁头读取数据也不需要做直来直去的移动,能否找到数据,只跟它最终落点有关,和中间路径形状是没关系的。所以,一方面盘片的自转,另一方面磁头的摆动,这两种动作的合成,使磁头能够读取盘片任意位置的数据。
盘片表面是用于存储数据的磁性介质,为了更有效管理磁盘,这些磁性介质也被“格式化”成易于管理的格局,即将整个盘面划分为多个同心环,以圆心画扇形,扇形与每个同心环相交的弧状区域作为最基本的数据存储单元。这个同心环就称为磁道,而同心环上的弧状区域是扇形的一部分,故称之为扇区,它作为我们向硬盘存储数据的最基本单位,大小是512字节。我们写入的数据最终是写进了磁道上的扇区中。注意啦,我上面说的磁道是个环,不是线,很多教科书上介绍磁道时都简单画了个圆圈,这容易让人误解磁道是条线,线上可无法存储数据,“环”是有横截面的,数据就存储这些“面积”中。磁头臂带动磁头在盘片上方移动,就是在找磁道的位置,盘片高速自转,就是在磁道内定位扇区。
磁道的编号和磁头一样也是从0开始的。相同编号的磁道组成的管状区域就称为柱面。图3-28中,两个盘片上编号相同的磁道,它们之间用灰色直线连接起来的部分,很像柱子的弧形表面,所以称柱面。如果盘片非常多的话,“柱面”就显得非常形象了。为什么会有柱面这个概念呢,我一个盘一个盘的存放不好嘛,这个是为了减少寻道时间的,磁头在磁道间跳转,跳转所需要的时间称为寻道时间,如果待写入的数据小于一个磁道的剩余容量,将来再读出来的时候,磁头只定位到该磁道就行了。这时候的寻道只是一次。如果待写入的数据要占用多个磁道时,除了写的时候要变换磁头到新磁道,将来读出来的时候也需要变换磁道,也就是需要多次寻道才能完成数据的完整读写。既然寻道对机械式硬盘速度影响较大,那原则上就尽量减少寻道次数,柱面中的磁道是相同编号,编号相同则意味着磁道在盘面上的位置相同,要定位到同一柱面中的磁道,所有磁头位置都一样,于是磁头不用再移动了。是不是很赞呢?
按照这种想法写数据:当0面上的某磁道空间不足时,其他数据写入第1面相同编号的磁道上。若新磁道空间还是不足,再写第2面相同编号的磁道上,直到同一柱面上的磁道(所有盘面上的编号相同的磁道)都不够用时才会写到新的柱面上。所以,在这一点上,盘面越多,硬盘越快。
各磁道扇区都是以1为起始编号的,并且只限于本磁道内有效,所以各个磁道间的扇区编号都相同,下限都是以0起,上限就不一定了,取决于各厂商的工艺,不过大多数情况下一个磁道中有63个扇区。
磁头如何找到扇区呢,就算是按照编号查找也得有个对比吧,实际上扇区是有自己的头部的,头部之后才是存储的数据,头部之中包含了扇区自身的信息,所以用磁头号磁道号扇区号3个信息就可以定位到唯一的一个扇区。
硬盘控制器同硬盘的关系,如同显卡和显示器一样,它们都是专门驱动外部设备的模块电路,CPU只同它们说话,由它们将CPU的话转译给外部设备。这是它们的共同点,但不同的是显卡和显示器是分开的,硬盘控制器和硬盘是连接在一起的,但是实际上很早之前他们也是分开的,后来业界的几个老大合作开发出一种新的接口,这样才将硬盘和硬盘控制器整合在一起,为了突显“整合”之意,硬盘控制器和硬盘终于在一起了,这种接口便称为集成设备电路(Integrated Drive Electronics,IDE)。随着IDE接口标准的影响力越来越广泛,全球标准化协议将此接口使用的技术规范归纳成为全球硬盘标准,这样就产生了ATA(Advanced Technology Attachment),计算机发展非常快,新老交替的现象层出不穷,以至于后辈的出现常常把前辈的名字都给改了。这不,前几年刚出道的硬盘串行接口(Serial ATA,SATA),由于其是串行,所以之前的ATA接口只好称为并行ATA,即(Parallel ATA,PATA)。
PATA这种接口我们现在已经用的非常少了,她的每个接口可以挂载两块硬盘,一块主盘一块从盘,主盘从盘的分工很明显,很多工作都要以主盘为主,比如系统就要装在主盘上。到后来系统兼容性越来越好,以至区别不明显了。一个主板支持这样的4块IDE(PATA)硬盘,所以主板上提供两个IDE插槽。这两个接口也是以0为起始编号的,一个称为IDE0,另一个称为IDE1。过按ATA的说法,这两个插槽称为通道,IDE0叫作Primary通道,IDE1叫作Secondary通道。
SATA这种接口我们现在用的比较多,一个主机可以链接多少SATA硬盘取决于主板上有多少SATA接口,就没有PATA那种限制了,即使主板上安装的是SATA硬盘,它也兼容PATA的编程接口,向上兼容是计算机能源源不断向前发展的根基。所以,后面给出的端口号也将按照PATA这两个通道来分组给出。
Nvme这种接口是一种基于PCIe的高速接口协议,专门用于连接固态硬盘。相比于SATA和SAS,NVMe具有更高的数据传输速率和更低的延迟,能够更好地发挥固态硬盘的性能优势。这种协议比较新所以优势是链接速度比较快,现在的固态硬盘传输速度理论上甚至在7000MB/s
硬盘的控制器属于IO接口,所以他的控制也是读写端口,端口就是位于IO控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器。我们只讲一些可能会用到的东西,其实硬盘是很复杂的,毕竟是一个非常精密的元器件,她的手册都非常的长。
端口可以被分为两组,Command Block registers 和 Control Block registers,第二组中的寄存器已经被精简了很多了,而且基本用不到,所以我们只讲第一组。
端口是按照通道给出的,也就是说,大家不要像我当初那样误以为端口是直接针对某块硬盘的,不是这样的,一个通道上的主、从两块硬盘都用这些端口号。要想操作某通道上的某块硬盘,需要单独指定。瞧,上面有个叫device的寄存器,顾名思义,指的就是驱动器设备,也就是和硬盘相关。不过此寄存器是8位的,一个通道上就两块硬盘,指定哪一个硬盘只用1位就够了,寄存器可是很宝贝的资源,不能浪费,所以此寄存器是个杂项,很多设置都需集中在此寄存器中了,其中的第4位,便是指定通道上的主或从硬盘,0为主盘,1为从盘。
端口用途在读硬盘和写硬盘时还是有点区别的,比如拿Primary通道上的0x1F1端口来说,读操作时,若读取失败,里面存储的是失败状态信息,所以称为error寄存器,并且0x1F2端口中存储未读的扇区数。写操作时就变成了feauture寄存器,此寄存器用于写命令的参数。这一切都是为了省钱,一个寄存器可以复用。
Data寄存器在名字上我们就知道它是负责管理数据的,数据的读写还是越快越好,所以此寄存器较其他寄存器宽一些,16位(已经很不错了,表中其他寄存器都是8位的)。在读硬盘时,硬盘准备好的数据后,硬盘控制器将其放在内部的缓冲区中,不断读此寄存器便是读出缓冲区中的全部数据。在写硬盘时,我们要把数据源源不断地输送到此端口,数据便被存入缓冲区里,硬盘控制器发现这个缓冲区中有数据了,便将此处的数据写入相应的扇区中。
读硬盘时,端口0x171或0x1F1的寄存器名字叫Error寄存器,只在读取硬盘失败时有用,里面才会记录失败的信息,在写硬盘时,此寄存器有了别的用途,所以有了新的名字,叫Feature 寄存器。有些命令需要指定额外参数,这些参数就写在Feature寄存器中。
Sector count寄存器用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减1,所以如果中间失败了,此寄存器中的值便是尚未完成的扇区。这是8位寄存器,最大值为255,若指定为0,则表示要操作256个扇区。
硬盘中的扇区在物理上是使用 柱面-磁头-扇区 来定位的,简称CHS,但是每次我们都要先算出来在那个柱面,那个扇区,太麻烦了,我们希望磁盘中扇区从0开始依次递增编号,不用考虑扇区所在的物理结构。其实我在描述需求时已经说出了LBA的定义,这是一种逻辑上为扇区址的方法,全称为逻辑块地址(Logical Block Address)。LBA有两种,一种是LBA28,用28位比特来描述一个扇区的地址。最大寻址范围是2的28次方等于268435456个扇区,每个扇区是512字节,最大支持128GB。我们这里为图简单,采用LBA28模式。由于128GB已经不能满足日益增长的存储需求,硬盘越来越大了,得有相匹配的寻址方法与之配套,于是要介绍的另外一种是LBA48,用48位比特来描述一个扇区的地址,最大可寻址范围是2的48次方,等于281474976710656个扇区,乘以512字节后,最大支持131072TB,即128PB。
可是我们才三个LBA寄存器,一共24位,不够28位怎么办,和device借一点,device寄存器的低4位用来存储LBA地址的第24~27位,第4位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘。第6位用来设置是否启用LBA方式,1代表启用LBA模式,0代表启用CHS模式。另外的两位:第5位和第7位是固定为1的,称为MBS位,
在读硬盘时,端口0x1F7或0x177的寄存器名称是Status,它是8位宽度的寄存器,用来给出硬盘的状态信息。第0位是ERR位,如果此位为1,表示命令出错了,具体原因可见error寄存器。第3位是data request位,如果此位为1,表示硬盘已经把数据准备好了,主机现在可以把数据读出来。第6位是DRDY,表示硬盘就绪,此位是在对硬盘诊断时用的,表示硬盘检测正常,可以继续执行一些命令。第7位是BSY位,表示硬盘是否繁忙,如果为1表示硬盘正忙着,此寄存器中的其他位都无效。另外的4位暂不关注。
在写硬盘时,端口0x1F7或0x177的寄存器名称是command,和上面说过的error和feature寄存器情况一样,只是用途变了,所以换了个名字表示新的用途,它和status寄存器是同一个。此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了,0xEC,即硬盘识别。0x20,即读扇区。0x30,即写扇区。
1、先选择通道,往该通道的sector count寄存器中写入待操作的扇区数。
2、往该通道上的三个LBA寄存器写入扇区起始地址的低24位
3、往device寄存器中写入LBA地址的24~27位,并置第6位为1,使其为LBA模式,设置第4位选择主盘或者从盘
4、往该通道上的command寄存器写入操作命令。
5、读取该通道上的status寄存器,判断硬盘工作是否完成。
6、如果以上步骤是读硬盘,进入下一个步骤。否则,完工。
7、将硬盘数据读出。
如果硬盘已经完成了自己的工作,他已经准备好了数据,一般常用的数据传送方式有以下五种
1、无条件传送
2、查询传送
3、中断传送
4、直接存储器存取(DMA)
5、IO处理机传送
对于第一种方式,数据源设备一定是随时准备好了数据,CPU随时取随时拿都没问题,如寄存器、内存就是类似这样的设备,CPU取数据时不用提前打招呼。那硬盘就不行了,他很慢。
对于第二种方式,是指传输之前,由程序先去检测设备的状态。数据源设备在一定的条件下才能传送数据,这类设备通常是低速设备,比CPU慢很多。CPU需要数据时,先检查该设备的状态,如果状态为“准备好了可以发送”,CPU再去获取数据。硬盘有status寄存器,里面保存了工作状态,所以对硬盘可以用此方式来获取数据。
对于第三种方式,是如果数据源设备将数据准备好后再通知CPU来取,这样效率就高了。通知CPU可以采用中断的方式,当数据源设备准备好数据后,它通过发中断来通知CPU来拿数据,避免了CPU轮询机制,效率较高。
对于第四种方式,也是针对第三种的缺陷,第三种中断的处理方式需要保存现场,压栈这些操作,DMA控制器可以完全解放CPU,不让CPU参与传输,完全由数据源设备和内存直接传输。CPU直接到内存中拿数据就好了。
对于第五种方式,CPU还需要完成数据交换,组合,校验等工作,干脆一不做二不休,再引入一个硬件吧。于是,I/O处理机诞生啦,听名字就知道它专门用于处理IO,并且它其实是一种处理器,只不过用的是另一套擅长IO的指令系统,随时可以处理数据。IO处理机之前会被集合在南桥芯片下,但是最新的一下处理器会集合有南桥和北桥的功能,只能说CPU的集成度越来越高了。
rd_disk_m_16:
mov esi,eax ;备份eax,也就是LBA地址
mov di,cx ;备份cx,也就是待读入的扇区数
;1、设置要读取的扇区数
mov dx,0x1f2 ;要写入的端口号要放在dx中
mov al,cl
out dx,al
mov eax,esi ;恢复eax
;2、将LBA地址存入0x1f3-0x1f6
mov dx,0x1f3 ;LBA地址7-0位
out dx,al
mov cl,8 ;右移操作数
shr eax,cl ;右移8位
mov dx,0x1f4 ;写入端口 LBA 15-8
out dx,al
shr eax,cl ;写入端口 LBA 13-16
mov dx,0x1f5
out dx,al
shr eax,cl ;只需要LBA地址的24-27位
and al,0x0f ;将al的前四位清零
or al,0xe0 ;将al的前四位设置为1110,即表示LBA模式
mov dx,0x1f6 ;写入到端口
out dx,al
;3、向0x1f7端口写入读命令0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;4、检测硬盘状态
.not_ready:
;统一端口,写时表示命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ; 第4位为1表示硬盘已经准备好数据传输
; 第7位位1表示硬盘忙
; 如果硬盘准备好了,那么al的结果是0x08,否则位0x80
cmp al,0x08 ; 下面这两行汇编可以表示为,如果结果不是0x80那么跳转
jnz .not_ready
;5、从0x1f0端口读数据
mov ax,di ; di为要读取的扇区数,这里我们只读取一个扇区,
;一个扇区512字节,每次读取一个子,所以需要256次读取
mov dx,256
mul dx ; 乘法指令,被乘数隐藏在ax中
mov cx,ax ; cx中是循环次数
mov dx,0x1f0
.go_on_ready:
in ax,dx ; 数据寄存器是16位的,所以这里每次读取两个字节
mov [bx],ax ;
add bx,2 ;
loop .go_on_ready ;
ret
0x13
读写硬盘该段汇编代码如下:
_start:
mov edx, 0 ; 初始化 EDX 寄存器,用于指定设备号和扇区号
mov ecx, 0 ; 初始化 ECX 寄存器,用于指定磁道号
mov ebx, 0 ; 初始化 EBX 寄存器,用于指定目标地址
mov ah, 2 ; AH 寄存器设置为 2,表示读取指定扇区数据
mov al, 1 ; AL 寄存器设置为 1,表示读取一个扇区
push edx ; 将扇区号压到栈中
mov dword [esp+4], ebx ; 将目标地址压到栈中
mov ecx, 0 ; 磁道号设置为 0
mov edx, 0 ; 设备号为 0 硬盘
mov ebx, 0x80 ; 通过设置 LBA 模式(7位),EBX 寄存器的高一位为 1
int 0x13 ; 调用 BIOS 中断读取扇区数据
jc error ; 如果 CF 标志被设置,表示读取失败,跳转到 error 标签处
jmp $ ; 跳转回当前位置
error:
; 处理错误
该代码使用 BIOS 中断 0x13
读取 SATA 硬盘 0 头 0 道 0 扇区的数据,然后将数据存储到目标地址(EBX 寄存器中指定的内存地址)中。通过 AH
寄存器设置为 2
,表示读取指定扇区数据,而 AL
寄存器设置为 1
,表示只读取一个扇区。将扇区号压到栈中,再将目标地址压到栈中,使用 0x80
的值将 EBX
寄存器的高一位设置为 1
,以启用 LBA 模式。如果操作失败,将跳转到 error
标签处处理错误。
标地址(EBX 寄存器中指定的内存地址)中。通过 AH
寄存器设置为 2
,表示读取指定扇区数据,而 AL
寄存器设置为 1
,表示只读取一个扇区。将扇区号压到栈中,再将目标地址压到栈中,使用 0x80
的值将 EBX
寄存器的高一位设置为 1
,以启用 LBA 模式。如果操作失败,将跳转到 error
标签处处理错误。