第8章磁 盘 学习编程语言,常常是从基本的输入和输出入手的(正如第5、6和第7章曾介绍的一 样)。到目前为止,我们不仅学习了怎样输入和输出数据,还学习了如何进行数据操作。 在开始编写重要程序之前,需要先了解文件(第9章是有关“目录和文件”的介绍),因 为大多数程序都与不同的类型的文件一起工作。有些则直接与磁盘和目录结构一起工 作。要预先了解第9章的一些基本原理,让我们先看看磁盘。 首先要了解基本磁盘技术的工作方式。软盘和硬盘的容量虽不同,但通过DOS功能 进行访问的途径却是相似的。在任何磁盘上,用来打开或关闭文件、读或写文件,以及访问 目录的功能调用却是相同的,通过这一章的学习,我们会了解磁道、扇区和簇的概念,以及 它们在创建程序过程中的重要性。 然后,用所学的有关磁盘工作的知识,就可创建一个基本的磁道格式化功能,可用来 对磁盘重新格式化,但使用这个功能要格外小心;一不注意就会出错并导致关键磁盘受 损。 8.1磁盘的内部结构 但凡使用过PC机的人都会与磁盘打交道。不论拥有什么样的系统,都要在磁盘上保 存信息。既然磁盘在个人计算机的操作中如此重要,人们似乎应该迫切了解它的工作方式 才是,但是事实并非如此。如果打算在磁盘上工作(即使只是察看磁盘结构或它们所保存 的数据),就应该懂得磁盘的工作方式。 可以将磁盘看作文件的集合—就好像它是一个文件柜一样,而不是放在公文包里 的一支笔。每张磁盘都拥有许多文件,用户可以直接访问任何文件所处的“文件夹”—目 录。 在格式化过程中,操作系统将常见的文件结构装入新磁盘中,DOS产生一个文件索 引(目录),并确弃磁盘上文件地址的方式(文件分配表,FAT)。 DOS记录有关磁盘布局 (引导记录)的信息,其中包括一个启动程序,甚至在没有系统文件的磁盘上也是这样。 基本上,磁盘的每一面都是磁性覆盖的表面。这个表面是由读/写磁头通过旋转磁盘 来得到磁化的。双面磁盘有两个记录表面;单面则只有一个(尽管两面都被磁化,但只有一 面能达到质量标准)。硬盘有典型的2到4个磁盘或盘片,它的两边都有记录表面。 在任何磁盘驱动器中,都有特殊的步进电机驱动读/写磁头沿着磁盘表面移动。这种 电机已精确地定义了停止处叫作步长,这些地方是磁头停住的场所。每个停止点都定义了 160页 一条磁道,数据就是被记录在磁道上,大多数硬盘都有一个多盘片系统,在该系统内磁头 能沿着所有的磁盘移动。我们将对应干步进电机所驱动的每个单步的磁道(在所有磁盘 上)都叫作柱面。 FORMAT程序将磁道划分成512字节大小的扇区,以便产生更便于管理的磁盘片 段;在软盘上每磁道有8个或9个扇区,硬盘上每磁道有17个扇区。 DOS分配给文件的空间是以簇为单位来计算的,每簇包括2到8个扇区,依磁盘的 类型而定。当文件需要另外的磁盘空间时,操作系统就将另外一个或多个簇分配给它。图 8.1显示了一个典型磁盘的布局。 : 图8.1磁盘示意图(图中标明了磁道、扇区和簇) 磁盘划分为以下5个重要区域: ·分区表 ·引导记录 ·文件分配表(FAT) ·目录 ·数据空间 我们将在以后的小节中介绍这些内容。 8.1.1分区表 几乎每个磁盘都有一个主记录保存在柱面(磁道)0、磁头(面)0和扇区1的地方(少 数没有主记录的磁盘可能不具备此状态;但用户不会用它们的)。该主记录负责读取和解 释其结尾处的磁盘分区表。然后控制被传递给目前能引导的硬盘分区的引导记录,正如分 区表所显示的那样。如果磁盘没有这样的主记录,它的位置就会被引导记录所代替,这将 在下节中讨论。 分区表说明了硬盘是怎样划分的。为了能被程序如FDISK识别,分区表必须遵从标 准的布局。在一个硬盘上可以有4个分区,每个分区都有一个相应的选项。图8.2显示了 161页 来自COMPAQ Deskpro 286的主引导记录的信息。注意分区表信息储存在扇区结尾。分 区1的选项为默认值01BEh,分区2为01CEh,分区3为01DEh,而分区4为01EEh。扇区 的最后两个字节(即默认值01FEh处,紧随分区表的字节)是这种情况下扇区的标志字 —AA55h。 图8.2硬盘主引导记录、磁盘分区表 注意图8.2所示的分区表信息中,只有两个选项填满了—该硬盘只有两个分区。分 区表的每个项长16字节。表8.1使用从图8.2的分区表中获得的样本值来说明每个分区 表项的布局。 表8.1硬盘分区表项的分布 字节 域长度 样本值 意 义 00h 字节 80h 引导指示符 00h=不能引导 80h=可以引导 01h 字节 01h 起始磁头 字节 01h 起始扇区(位0~5;位6~7是 02h 柱面值的位“8和9”) 03h 字节 00h 起始柱面(低8位) 162页 字节 域长度 样本值 意 义 04h 字节 04h 系统ID 00h=未知 01h=DOS,12位FAT 04h=DOS,16位FAT 05h=DOS,扩展的磁盘,16位FAT 05h 字节 04h 结束磁头 06h 字节 51h(11) 结束扇区(位0-5;其它的两位是柱面值的位 “8和9”) 07h 字节 E9h(1E9) 结束柱面(低8位) 08h 双字 00000011 第一个分区扇区 0Ch 双字 0000A2A1 分区中的扇区数 注意保存在分区表中的信息含义。信息的大部分是说明每个分区的边界,而两个域、 引导指示符以及系统ID则是我们特别感兴趣的内容。引导指示符是告知分区是否可引 导。4个可能的分区中只有一个能标记为可引导的。系统ID则用来标记分区的类型。表 8.1指出许多可能的系统ID值,但是各种其它的操作系统( 如XENIX、UNIX和Pick)则 需要扩大可能的系统ID表。 因为分区表不仅要被DOS识别,还要被别的操作系统识别,所以它的格式在不同的 DOS版本中不易改变(或者在不同的操作系统中);任何对分区表格式的破坏,都会减少 这种软件的商业销售机会。 在系统引导过程中,BIOS查寻磁盘的第一个扇区以便持续引导过程。对于软盘,这就 是引导扇区(见下一节)。对于硬盘;这就是主记录(在主记录内,分配分区表,并且BIOS 确定(通过引导指示符域)哪个分区是能引导的。安装好要引导的分区之后,就将控制传递 给分区的引导扇区,而且象对于软盘那样的磁盘继续进行引导工作。 磁盘分区技术在硬盘上建立起一系列的逻辑磁盘。每个逻辑磁盘运行起来就象一个 小型的磁盘驱动器(并由磁盘驱动器指定一个驱动器字母)。这样单个的硬盘能将所有的 逻辑驱动器用于一个操作系统,或者每个逻辑驱动器能拥有不同的操作系统。一些系统通 常在一个分区中有MS-DOS 而在另一个分区之中则装有XENIX—就象是有两台计 算机而只需一台的价格。 通常当使用容量大于32M的硬盘时,分区是必须的。 DOS 4.X以前的大多数版本都 限于32M或在某个分区中小于32M。通过使用多个分区,就可以利用160M那么大的磁 盘。一些商业化的实用程序借助特殊的磁盘驱动程序能够有效地消除32M的限制。但是, 因为没有驱动程序时,磁盘通常不能使用,所以如果要运行别的操作系统或从软盘进行引 导的话,这种限制就会带来问题。 DOS 4.0版本的一个重要特性,就是它能在一个硬盘的大小范围内去掉了这种限制。 它允许扇区大小达到32位,并允许FAT达到64个扇区,这样就将磁盘容量限制扩大到 了上吉(千兆)字节区域中。 163页 因此,当使用大的硬盘和DOS 4版本时,不需要进行分区。但是如果用户喜欢传统的 方式来组织系统或者系统中保存了多个操作系统,那么也可以进行分区。 8.1.2引导记录 当系统在为可引导的磁盘分区确定安装引导记录的地址时,BIOS就将引导记录装入 内存中。图8.3显示了软盘的典型引导扇区。注意它们在不同的DOS版本之间所存在的 差异。 在这4个版本中,引导扇区都是以跳到自举装入例程的起始点来开始的,该例程将系 统“自举”而进入操作过程。在装入了小的自举程序后,再回过头来装入较大的操作系统 (见第3章,“动态的DOS”,在那里,更详细地讨论了DOS的装入程序)。 在3个字节的跳转指令后面,紧跟的是8个字节的系统名字域,该域指定用来格式化 磁盘的系统所属的制造厂家(有些制造厂家未在这里放置一个名字)。该域后面紧跟BIOS 参数块(BPB),它提供了表8.2所列举的信息。BPB的格式和内容说明,在不同的引导扇 区之间有些差异;每一个重要的版本改进,都要增加更多的数据。表8.2中的样本值来自 图8.3中的引导扇区(所有这些值都来自360K的双面双密软盘)。 在表8.2中,特别要注意的是,样本值是怎样试图保持自身与旧版本的兼容性的,它 甚至在这点上达到极点:当有可能在每个磁盘上拥有多于65535个扇区时,它为此提供了 两个不同的域以用于扇区总数目这一栏。遗憾的是,并非所有的制造厂商都在旧版本中将 保留区单独放置,所以当安装二个新版本时,磁盘会与不能读取的版本一起被格式化。 (A)2.0版磁盘的引导扇区布局 图8.3软盘的引导扇区 164页 (B)MS-DOS3.2版的引导扇区布局 (C)IBM DOS4.01的引导扇区布局 图8.3(续) 165页 (D)MS-DOS5.0的引导扇区布局 图8.3(续) 表8.2 BIOS参数块(BPB)的布局 字节(偏移值) 域长度 样本值 含义 00h 字 0200 每个扇区内的字节数 02h 字节 02 每个簇内的扇区数 03h 字 0001 保存扇区的数目(从扇区0开始) 05h 字节 02 FAT的数目 06h 字 0070 根目录项的最大数 08h 字 02D0 扇区总数(或在V3中如果大于65535则为0) 0Ah 字节 FD 媒体描述符 08h 字 0002 每个FAT的扇区数 0Dh 字 0009 每个磁道的扇区数 0Fh 字 0002 磁头数 11h 双字 00000000 隐藏扇区的数目 15h 11个字节 — 保留(V3之前) V3 BPB 扩展 15h 双字 00000000 在08h的字=0时的扇区数 19h 7个字节 — 保留(8P3外部引导记录区) V4 引导记录扩展 19h 字节 00 物理驱动器号 1Ah 字节 00 保留 166页 字节(偏移值) 域长度 样本值 含义 1Bh 字节 29 扩展引导记录的特征字节 1Ch 双字 203D10CC 卷序列号(来自日期/时间) 20h 11个字节 NONAME 卷标记 2Bh 8个字节 FAT12 保留 在表8.2中,特别要注意的是,样本值是怎样试图保持自身与旧版本的兼容性的,它 甚至在这点上达到极点:当有可能在每个磁盘上拥有多于65535个扇区时,它为此提供了 两个不同的域以用于扇区总数目这一栏。遗憾的是,并非所有的制造厂商都在旧版本中将 保留区单独放置,所以当安装一个新版本时,磁盘会与不能读取的版本一起被格式化。 V2之后的DOS版本,BPB对于自举程序的操作是很关键的,因为该程序必须知道这 些参数,才能去发现并安装操作系统的BIOS和内核。 V3之前,安装者假定BPB的ROM BIOS版本能用于引导,并假定安装了DOS后将 应用媒体码;于是,他们不利用引导扇区中的数据。结果,一些公司(特别是Tandy和 HeathZenith)忽略了用DOS2格式化的软盘所提供的BPB数据。 在V3之前,这些磁盘工作良好,但在新版本中它们变得不能阅读了,因为新的DOS 把自举程序的代码看作正确的磁盘参数而阅读这些参数。因为IBM的V2产生的磁盘的 确遵循BPB的规则,所以这些磁盘能被任意地阅读,这就使许多人相信新版本会在引导 扇区的第三、四和第五字节中寻找“魔术字头”IBM;而实际寻找的却是紧跟第八字节 OEM名字区域后的数据。 从V3到V4的过程中,也有相似的情形出现。但是在这种情况下,DOS的IBM版本 的确是在BPB的起始位置寻找魔术起始字头;如果在那里并不是IBM这几个字母(即使 发现了MS-DOS),它会报告一个未知的媒体出错。不知道这种行为的理由;但只要将第3 ~10字节的内容改变成“IBM V2.0”或“IBM V3.0”就能使磁盘被系统接受。 8.1.3文件分配表(FAT) DOS利用文件分配表(FAT)来管理磁盘的数据区。 FAT向DOS指示每个文件所拥 有的磁盘部分。由于FAT具有关键作用,所以DOS通常在磁盘上依次保存了它的两个拷 贝。初始FAT(第一个)改变以后,DOS会小心地更新第二个拷贝。 在磁盘上,FAT紧跟引导记录。因为引导记录只有一个扇区长(扇区0),所以FAT 从扇区)开始。 FAT的长度(在扇区中)由引导记录BPB指定,FAT的拷贝也是这样。 我们很有兴趣地注意到:DOS本身的命令没有一个使用了FAT的第二个拷贝。如果 原始FAT受到一定程度的损坏,就必须使用某个独立的实用程序(不由DOS提供,甚至 也不是来自Microsoft或IBM),来作用于FAT的第二个拷贝,以便发现损坏的磁盘文件。 但是实际上,能够影响第一个拷贝的话,它同时或稍后也能破坏另一个拷贝,这使第二个 FAT拷贝的实用性值得怀疑。 每个FAT包含一系列项,长12或16位,可记录每个簇在磁盘驱动器上的状态。如果 使用12位项,那么其中的每两个项就会被包裹在表中的3个连续的字节中(即24位含有 167页 两项)。这样可使每个FAT所需的空间最小化。 簇是能够分配使用的磁盘空间的最小位,它总是包括一个或多个连续的逻辑扇区(它 们不必在磁盘的相同表面上,第一磁道的第一表面的第一扇区就是0逻辑扇区,然后扇 区、表面、磁道各自递增来记数)。 在一个簇中扇区数总是2的乘方,这样可以简化簇数与逻辑扇区数之间的转换。软盘 通常使用两个扇区大小的簇(1024个字节);第一个硬盘使用8个扇区的簇,但用户们发 现储存许多小文件时4096个字这个最小的分配单位是很浪费的。V3发表后,大硬盘的簇 大小减小到4个扇区。对于每个磁盘,簇大小是包含在BPB中的一个关键项目(见表 8.2)。 在FAT内,每一表项都与磁盘上的一个簇精确对应。与0簇对应的表项拥有媒体码, 1簇对应的则总是填满1的位(十六进制FFFh或FFFFh)。能用于数据的第一簇的编号 为簇2。 任何能用于分配的簇,它在FAT中对应的项均为0。将第一个簇分配给文件时,它在 FAT中的项就变成FFFh或FFFFh,以便指示出该簇是文件中的最后一个簇。簇号还被 记录在文件的目录项中(第9章讨论“目录和文件”)。当分配一个新簇时,FFFh/FFFFh 项就移向FAT中新簇的项,并且新项的簇号就填上了前面已使用的FFFh/FFFFh值。 以这种方式,FAT将所有分配给文件的簇都链接起来,而不管这些簇碰巧在磁盘的 什么位置上。 特殊码指示某个簇是否遭到损伤,以及若受损伤,则是什么样的。值FF7h~FFEh (FFF7h至FFFEh是用于16位FAT表的)就用于这个目的。 “32M界限”长期以来一直是DOS的一个著名特性;但在V4中却消失了。在了解这 种改变对FAT编码和其它磁盘参数产生的影响之前,让我们先看看这个界限是在哪里 产生的。 尽管起初选择来作为FAT项的大小是形成这一界限的原因之一,但真正的界限却 是所有I/O例程中使用的最多只有512个字节的扇区大小和16位的扇区号值限制所带 来的结果。因为16不能超出65535,所以在一个卷中,65535就变成了最大的可能的扇区 号。在512个字节的扇区大小情况下,结果是卷大小的最大值变成了33553920个字节,或 32M。 vs之前的DOS版本都限于只使用12位FAT项;磁盘所能包含的最大簇数是2的12次方 (或4096)。4096个可能的FAT项值之中的9个用来表示簇的状态,所以只有4087个簇 号可用。最大的FAT占用了6144个字节,或12512个字节的扇区。 注意这些4087个簇所能代表的实际磁盘大小的最大值完全依赖于所选择的簇大小。对 于大小只有单个扇区的簇,那么就只有2092544(40870512)个字节。这个数字能 满足所有常用的软盘(没有人能解释2个扇区的簇怎样变成了标准的簇)。但它甚 至对于最小的硬盘也是不能用的。 将簇大小增加到8个扇区就会相应地将大小限制提高到16740352个字节,足够用于 原始的XT型号的10兆字节驱动器。将簇大小加倍成每簇16个扇区,它所带来的是8192 168页 个字节的最大分配单位(甚至对于1个字节的文件也会如此)会将FAT容量发展到32M 的界限。但这种空间浪费使用户很不满意。 簇大小的进一步增加能在FAT中扩展该界限,但在卷中却不能。只有扇区大小的增 加(或者一个表面的增加,即第三方驱动程序所采用的方式)能够突破卷大小的限制。扇区 大小还会受到所用的控制卡硬件的限制。 3.0版中浪费问题能被避免,在第3版里允许DOS使用另外编码的FAT。如果磁盘 驱动器足够大,而能产生超过408)个簇,即簇大小为8个扇区(比17兆字节大),DOS V3 就变到4扇区那样的簇大小并使用一个16位的FAT项。16位FAT允许簇号最大值为 65527。FAT中有了这么多簇,就能使用2个扇区的簇而不超出32M。 但是这样大小的FAT对于每个拷贝需要131072个字节,或者说,对于FAT对,在使 用512个字节的标准扇区大小情况下,需要512个扇区。要将系统开销保持到尽可能低的 水平,DOS设计者选择了FAT项的限制数为16384,于是就保持了32个兆字节的限制和 4个扇区的簇,并将FAT空间减少到128个扇区。 随着时间的推移,目前有许多磁盘的容量都大大地超过了32M的界限(本书就是在 一个80M硬盘的系统中写成的,该硬盘被分成3个26M的逻辑驱动器)。于是,在4.0版 中,扇区号可以是16或32位JAT中的字节数可以与16位项一起增大到最大值,这就 将磁盘空量的限制提高到了128M,每个族有4个扇区;或者提高到256M,每个族的大小 仍为8个扇区。但超过300M的驱动器已经在作广告;谁知道将来会出现什么? 磁盘格式化时,FORMAT程序确定使用哪个编码方案。如果磁盘大小指示它能充分 地被12位FAT来表示,那么就使用12位FAT,否则就使用16位的FAT。如果卷的大小 超出32M,那也可以使用32位扇区号(否则,旧的16位扇区号依然保留)。下面让我们来 看看FAT的各个类型。 一、12位FAT 12位FAT所带来的表比16位FAT要小25%。这可能要归因于采用了12位FAT。 在3个字节中拥有2个12位数。图8.4显示了12位FAT的样本扇区。 请注意文件分配表的组成。在这个例子中,头两个FAT项(头3个字节)包含有系统 信息。第0簇和第1簇的数据区就不能被FAT访问。紧跟着的1/2个字节(12位,第2簇 的FAT项)后面是第3簇的FAT项,依此类推。注意默认值0103h上的3个字节,它们是 第2和第3簇的FAT项。用下列公式(所有值都是十六进制的)可将034000分成2个独 立的FAT项: 第1项=((第2字节AND 0F)*1000)+第1字节 第2项=((第3字节*10)+(第2字节ANDF0)/10) 于是第2簇的FAT项如下所示: 第2FAT项=((40 AND OF)* 1000)+03=((0)* 1000)+03 =03 第2FAT项=((40 AND OF)* 1000)+03 169页 图8.4一个FAT样本 第3簇的FAT项如下 第3FAT项=00* 10+((40 AND F0)/10)=0+(40/10)=04 每个FAT项都指向文件所占有的下一个簇。于是,FAT项就形成了一个链;如果该 链中的“链结”放在一起,那么该链就表示某个特定文件所占有的簇。 但是FAT项的一些值并不代表某个后续簇号;相反,这些值代表该簇的一个状态。 表8.3总结了FAT项的可能代码。 表8.3 12位FAT分配字节 分类 代码 自由的可分配的项 0 文件的一部分(下一簇的指针) 2—FF6 坏簇 FF7 簇链的结束 FF8—FFF 使用图8.3所示的12位FAT,让我们来看看一条簇链。文件的目录项指向文件所占 用的第一个簇(第9章讨论目录项)。因此,IBMBIO.COM(长22100个字节)的目录项指 向开始簇号:2。如果看看第2簇的项,会看到第3簇的指针—并且第3簇指向第4簇 (记住你只是在作算术)。第4簇指向第5簇,后者以指向第6簇,依此类推直到第18h簇。 这里,FAT项为FFFh,表明已到簇链的终端。 将第2簇作为起点,可以用下列方法找到下一簇: 1.簇号乘以2并记下结果。 2.在所得到的偏移值上取得一个字。 170页 3、如果原来的簇号(在这种情况下是2)是偶数;就获取该字的低12位;否则就获取 高12位。 二、16位FAT DOS V3的发表,为较大的硬盘和使用每项16位(2个字节)的FAT提供了支持。图 8.5显示了来自16位FAT的一个样本扇区。 图8.5一个16位的FAT样本 对这个文件分配表的解释要比12位FAT直接多了。开始的两项(4个字节)用于系 统信息;每个后续项占有2个字节。注意这两个字节位于缺省值0104h(即第2簇的FAT 项)。这里的值(0003h)指向第3簇的项。 表8.4 16位FAT分配字节 分类 代码 自由的可分配的项 0 文件的一部分(下一簇的指针) 2—FFF6 坏簇 FFF7 簇链的结尾 FFF8FFFF 与12位的版本一样,它的每个FAT项也指向文件所占有的另一个簇。于是FAT项 便形成了一条链,当链的所有“链结”放在一起时,链就指示某个特定文件所占有的簇。象 12位FAT那样,FAT项的其它值不代表一个后续簇号。它们代表簇的状态。表8.4总结 了FAT项的可能代码。 让我们来看看一条簇链,它使用16位FAT(如图8.5所示)。文件的目录项指向文件 所占有的第一个簇;在图8.5中,IBMBIO.COM的目录项(长度为22100个字节)指向起 始簇号2。第2簇的项指向第3簇。第3簇指向第4簇,后者指向第5簇,依此类推,直到 171页 第0Ch簇。在0Ch簇上FAT项为FFFFh,这指示出已到达簇链结尾。 在4.0版中获得的更大磁盘容量是通过允许FAT达到最大的容量而不是削减它来 得到的。尽管3.0版和4.0版的扇区号大小可能改变,但它们的FAT对策之间的唯一差 异就是如何获得更大磁盘容量的方法。 三、更多的FAT信息 当DOS为文件请求空间时,空间就会以一个或多个簇为单位来分配给这个文件。由 12位和16位FAT的有关讨论可以知道,一个文件的簇是链接在一起的,每个FAT项都 给出了下一个项的簇号(见图8.6)。 图8.6 FAT策链接 FAT保留了第0项和第1项的空间,但未使用它们。 FAT的第一个字节用作磁盘标 识(ID),有助于识别磁盘的格式(见表8.5)。因为为了系统而保留了0簇和第1簇,所以 第2簇是能被分配的第1个簇。 表8.5一些可能的FATID字节值 值 磁盘特征 F0 不能识别 F8 固定的磁盘 F9 双面,15扇区/磁道 F9 双面,9扇区/磁道(720K) FC 单面,9扇区/磁道 FD 双面,9扇区/磁道(360K) FE 单面,8扇区/磁道 FF 双面,8扇区/磁道 172页 从FAT的讨论中可知,DOS借助整个的簇来分配文件空间。于是不管文件实际有多 大,一个文件的最小磁盘应用单位就是一个簇。一个字节的文件可能占用512、1024、2048 或者更多字节的磁盘空间,具体决定于每个簇的扇区个数。 由对软盘的BPB编码可知,样本磁盘有2个FAT表。无论何时磁盘操作在磁盘上分 配或释放所分配的空间时,两个FAT都将自动地更新。当磁盘被第一次访问时,DOS会 比较两个FAT,看看它们是否稳定。尽管可以有2个以上的FAT,它们连续存放在磁盘 上,但大多数磁盘只有2个FAT。 最后的FAT到达根目录后,每个项都有32个字节。 BPB给出目录的大小,以便确定 文件区域从哪里开始(紧跟在根目录之后)。 既然已知道从哪里找到磁盘上的内容,那么让我们再来看看DOS提供了哪些功能来 操纵它们。 8.2利用磁盘功能 因为文件系统(包括所有刚刚讨论过的表)是DOS的一个结构,所以没有BIOS功能 能用于DOS文件系统。对于BIOS,磁盘只是一系列的扇区,从0扇区开始,连续递增到最 高数目的扇区。BIOS了解磁道、扇区和磁盘磁头,但不了解文件、FAT和目录。接下来所 有将要使用的功能都是面向DOS的功能。 8.2.1驱动器信息 可利用DOS功能调用来获取磁盘驱动器方面的信息。drvinfo.c程序分析了怎样获得 和显示这个信息(见列表8.1)。注意该程序没有将4.0版的新的32位扇区的可能性考虑 进来;4.0版的《技术参考手册》也没有指出驱动器信息要注意这种变化。 列表8.1 /*dRvinfo.c Listing 8.1 of DOS Programmer'S Reference*/ #include<Stdio.h> #include<dOS.h> #include<Ctype.h> #include "drvinfo.h" /*Prototypes*/ void get_drvinfo(char drv,struct drvinfo*info); unsigned int get_drive(void); void get_drvspace(char drv,struCt drvinfo*info); void main() { int drive; drive = get_drive(); printf("\n\n"); printf("Current Drive Code=%u(%c:)\n",drive,'A'+drive); printf("\n"); get_drvinfo('A'+drive,&info); 173页 printf("Drive %c: infOrmation from funCtion 1Ch\n",'A'+drive); printf("Number Of clusters=%lu\n",info.cluSters); printf("SectOrS per Cluster=%lu\n",infO.spc); printf("Physical sector Size=%lu\n",info.SecSize); printf("DriVe Size=%lu Kb\n", (info.clusters*info.spc*info.secsize)/1024); printf("\n"); get_drvspace('A'+drive,&info); printf("Drive %c: infOrmatiOn frOm function 36h\n",'A'+drive); printf("Number of cluSterS=%lu\n",infO.ClUSterS); printf("SectorS per cluSter=%lu\n",info.Spc); printf("PhySical sector size=%lu\n",info.SecSize); printf("Drive siZe=%lu Kb\n", (info.clusters*info.spc*info.secsize)/1024); printf("Available cluSters=%lu\n",infO.avail); printf("Available Space=%lu Kb\n", (info.avail*info.spc*info.secsize)/1024); Printf("\n"); } /*Fetch the current drive code*/ unsigned int get_drive() { union REGS regs; regs.h.ah=0x19; intdos(®s,®s); return (regs.h.al); } /*Fetch drive information using function 1Ch*/ vOid get_drvinfo(drv,info) char drv; Struct drvinfO*infO; { union REGS regs; Struct SREGS segs; int dn; /*Converts drive letter to internal representation*/ drv=toupper(drv); dn=drv-'A'+1; /*Set up and call DOS*/ regS.h.ah=0x1c; regS.h.dl=dn; intdosx(®s,®s,&segs); infO->Spc=regS.h.al; info->fatseg=segs.ds; infO->fatoff=regS.X.bx; infO->SeCSize=regS.x.cx; infO->CluSterS=regS.x.dx; } /*Fetch drive information using function 36h*/ void get_drvspace(drv,info) 174页 char drv; Struct drvinfo*info; { union REGS regs; struct SREGS segs; int dn; /*Converts drive letter to internal representation*/ drv=toupper(drv); dn=drv-'A'+1; /*Set up and make the DOS call*/ regs.h.ah=0x36; regs.h.dl=dn; intdosx(&regs,&regs,&segs); info->spc=regs.x.ax; info->avail=regs.x.bx; info->secsiZe=regs.x.cx; info->clusters=regs.x.dx; } 为了获取有关驱动器的信息,drvinfo.c调用了下列几个子例程: get_drive(),获取当前驱动器号 get_drvinfo(),获得驱动器的一般信息 get_drvspace(),获得其它信息 所有这些信息都放在drvinfo结构中,正如drvinfo.h所定义的那样,见列表8.2。 列表8.2 /* drvinfo.h Listing 8.2 of DOS Programmer's Reference*/ struct drvinfo{ unsigned long spc; /*Sectors per cluster*/ unsigned long avail; /*Available clusters*/ unsigned long fatseg;/*FAT segment of ID byte*/ unsigned long fatoff;/*FAT offset of ID byte*/ unsigned long secsize;/*Physical sector size*/ unsigned long clusters;/*Number of clusters*/ char fatid; /*FAT ID byte*/ }info; 通过定义一个结构来容纳有关磁盘的信息,就可以保证在需要这些信息时,这些信息 总是有逻辑地组织在一起的。 注意列表8.1中的drvinfo.c程序包括两个子例程调用;这些调用说明了同样的信息 可用多种途径来获取。 获取驱动器信息是一个简单的过程——可以调用DOS服务中断(int 21h)。在C语言 中该调用由intdos()函数来执行,在pascal中则由Msdos来完成。DOS服务将驱动器代码 返回到Al寄存器中。 175页 get_drive()函数不解释驱动器代码;它只是将代码返回给drvinfo.c。但是请求特定驱 动器信息的过程要比请求驱动器号的过程复杂一些。信息返回到段寄存器中,就象返回到 通用寄存器一样。要访问段寄存器,必须使用intdosx()函数,正如在get_drvinfo()和get_ drvspace()函数中所做的那样。 在这些函数调用了DOS(intdosx函数调用)之后,该函数就会将从寄存器中返回的信 息保存在函数调用所传递的info结构中。处理象这样结构中的信息,能使没有寄存器或 Dos调用方面知识的人,通过调用该函数的程序来访问它。稍稍再做一点工作,这些函数 就可以包含在那些不知道怎么处理DOS的程序员的函数库之中。 get_drvinfo()和get_drvspace()是编程实践中的很好例子——将执行细节隐藏在函 数之中。这种情况下,用户就通过所熟悉的驱动器字母(A、B、C等)标准格式来确定正确 的驱动器代码。 允许驱动器名作为一个字母来传递,这是一条途径,它可以隐藏如下事实,即在DOS 和BIOS例程中,驱动器标志并不总是稳定的。有些例程使用0来表示A驱动器,但有些则 用0来表示默认的驱动器。 get_drvspace()所返回的信息可能比get_drvinfo()返回的信息在程序中用得更多。例 如,用get_drvspace()可以利用DOS功能36h所返回的下述信息来确定磁盘上有多少自 由空间可用。 寄存器 内含物 AX 每个簇的扇区数 BX 有效扇区数 CX 每扇区的字节数 DX 每个驱动器的簇数 要计算驱动器上的自由空间,可利用下列公式: BX*AX*CX 下列公式则计算驱动器容量的总数: DX*AX*CX 如果只想确定磁盘上有多少自由空间,可以编写函数get_free())来仅仅返回这方面 的信息。列表8.3中的free.c程序调用了get_free()来获得这方面的信息,它为得到可用 的和整个的磁盘空间,而使用了驱动器名和指向整数的指针。 列表8.3 /*free.c Listing 8.3 of DOS Programmer's Reference*/ #include<Stdio.h> /*Prototypes*/ void get_free(char drv,unsigned long*avail,unsigned long*total); void main(argc,argv) int argc; char*argv[]; 176页 { unsigned long avail,total; get_free(*argv[1],&avail,&total); if(*argv[1]) printf("Free diSk space on drive %c: iS %lu Kb Of %lu Kb\n", *argv[1],avail,total); else printf("Free disk space on default drive is %lu Kb of %lu Kb\n", avail, tOtal); } 编写这个程序来为驱动器名而检查第一个命令行参数。如果没有提供第一个参数,程 序就假定它应该发现默认驱动器的信息。 get_free()函数确定它应检查哪一个驱动器,然后设置参数并进行对DOS的调用(见 列表8.4)。功能36h用于确定自由空间,但大部分磁盘信息已被舍弃,因为该函数的有限 目的并不需要它们。 列表8.4 /*get_free.c Listing 8.4 of DOS PrOgrammer's Reference*/ #include #include #include void get_free(drv,avail,total) char drv; unsigned long*avail,*tOtal; { union REGS regs; struct SREGS segs; int dn; unsigned long SectCluster; unsigned long AvailCluster; unsigned long BytesSector; unsigned long Clusters; /*Determines the drive and sets the drive number*/ if(drv){ drv=toupper(drv); dn=drv-'A'+1; }else{ dn=0; } /*Sets up and makes the DOS function call*/ regs.h.ah=0x36; regs.h.dl=dn; intdosx(®s,®s,&segs); SectCluster=regs.x.ax; AvailCluster=regs.x.bx; BytesSector=regs.x.cx; Clusters=regs.x.dx; *avail=(SectCluster*BytesSector/1024)*AvailCluSter; *total=(SectCluster*BytesSectOr/1024)*Clusters; } 177页 在BASIC中,也能执行同样的操作,参看列表8.5中的free.bas程序。 列表8.5 'free.bas $include"REGNAMES.INC" def fnchkspc(drv) 'determine the free space from Int 21h,FunCtion 36 reg %ax, &h3600 reg %dx,drv call interrupt &h21 fnchkspc=reg(%bX)*(reg(%ax)*reg(%CX)/1024) end def def fnsize(drV) 'determine the space from Int 21h, Function 36h reg %ax , &h3600 reg %dx,drv call interrupt &h21 fnsize=reg(%dX)*(reg(%aX)*reg(%CX)/1024) end def 'MAIN PROGRAM input"Drive:",dV$ dv$=left$(dV$,1) drive=int((instr("AaBbCcDdEeFfGg",dV$)+1)/2) print "Free Space on Drive";dV$;"Is";fnchkspc(drive);"K" print "Drive Capacity Is";fnsize(drive);"K" end 如果想建立自己的确实有用的实用程序,为什么不建立一个这样的程序,将一组文件 拷贝到一张软盘或一套软盘上?这类应用程序可检查用户想拷贝的下一个文件的大小。如 果磁盘上有足够的空间,该实用程序就拷贝那个文件;否则此实用程序就提示需要另一张 软盘。这样的程序就象下面这个样子: FOR i=1 To number_of_arguments TOP: size=size of file i space=get space on target drive IF size>Space THEN ask for another floppy disk wait for user to press a key GOTO TOP: ELSE copy file i to floppy ENDIF NEXT i 尽管还不知道怎样去做各种必需的事情来使这个程序实用,但在第9章“目录和文件” 和第10章“程序和内存管理”中要讨论文件大小和执行别的操作的程序。然后就可以建立 这个程序。 8.2.2格式化磁盘 磁盘格式化是大多数人不必自己完成的一项简单而又危险的任务。与DOS一起发表 178页 的基本FORMAT程序是许多有用的磁盘格式化程序中的一个。特殊的格式化程序对于 那些想进行更快和更复杂格式化的人们来说是有用的(通常作为实用程序如PC Tools的 部分)。 开发者在编写磁盘格式化程序之前应该仔细计划一下;如果不能正确操纵,这些程序 会抹去关键的磁盘系统并破坏辛苦工作的成果。这一节将介绍一些基本的格式化技术和 访问存在于系统中的访问格式化例程的途径。记住:如果在这里犯错误,那就会将系统置 于危险的境地!当测试一个格式化程序时,请遵守下列简单的预防措施: ·无论什么时候有可能的话,在“只有软盘”的系统中测试格式化程序(没有硬盘)。所 使用的系统盘应该是当发生错误时能够承受得起损失的盘。 ·如果必须在包括硬盘的系统中进行测试运行,若有可能就应使硬盘失效(例如,可 以借助于拨掉硬盘控制卡来达到)。 ·确保有自己系统的当前备份(用户应该总是有一个当前备份!) 好的编程实践说明:必须认识到错误的合理性;测试必须努力地预见到和提供所有可 能的错误。如果小心一点,那么在测试程序时,将不会发生什么事情。 BIOs提供了Int 13h的功能05h来格式化磁盘磁道。它涉及的概念很简单,这个功能 也易于使用。现在让我们来看看怎样使用这个功能来格式化磁盘。 如果用一个未格式化的磁盘来开始测试(或者如果清除一个旧盘),那么必须使用Int 13h,功能05h来格式化磁盘,一个磁道接一个磁道地进行,如下所示: 从0到最后的每个磁道 给磁道建立好调用所需的参数 调用磁道格式化例程 这样就能正确地格式化磁盘,并且BIOS例程能读它——但它不是一张DOS盘。回 忆本章前面的内容,DOS要求磁盘有一定的结构;引导扇区、FAT和根目录。这个格式化 过程没有提供一个这样的结构。 要产生DOS能接收的磁盘,必须不仅给磁盘一个基本格式,而且要象下述那样对磁 盘结构进行初始化: 从0到最后的每个磁道 给磁道建立调用所需的参数 调用磁道格式化例程 将引导扇区写到磁盘上 将FAT信息写到磁盘上 将根目录信息写到磁盘上 只需将零写到FAT和磁盘目录区中,就能操纵上面的最后两步。FAT中的零项表示 该簇是空白的,正准备着被重新分配。磁盘目录中的零表示目录项从未使用过。然后,除了 引导扇区,格式化过程可以相当简单:格式化磁道、写引导扇区,然后将FAT和根目录区 域设置成零。 让我们编写一个例程来格式化磁道。在了解了怎样格式化磁道之后,只需“走过”所有 179页 的磁道就能格式化一张磁盘。 可用表8.6中所示的寄存器设置来调用BIOS int 13h的功能05h。 表8.6 BIOS Int 13h的功能05h的寄存器设置 寄存器 意义 AH 05h(功能代码) ES:BX 指向磁道地址域表的指针 CH 磁道数 DH 磁头数 DL 驱动器数 磁道地址表是格式化操作的中心。它限定了逻辑磁盘扇区在物理磁道上的顺序。每个 磁盘扇区在这个表中都以4个字节的项来表示,该项为磁道上的每个扇区作标记。可用这 个表来分配逻辑扇区号,所按的顺序与物理扇区在磁盘上的顺序不同(该过程称为交叉 (interleaving))。 磁道数会依据所使用的磁盘类型而改变:5.25英寸的磁盘(360K,双面、双密度)每面 有40个磁道;3.5英寸磁盘(720K,双面、双密度)每面有80个磁道。磁头数(软盘的)应为0 或1。 驱动器号(nt)指示出你想在哪个物理驱动器上工作。(软盘驱动器从0数起的数来指 定:A盘驱动器为0,B盘为1,依次类推。硬盘则略有不同:C盘驱动器常是80h。) 在一张未格式化的磁盘上,磁道是磁性表面的非结构空白部分。格式化过程通过磁 化,在磁道上产生“储存箱”来将某个结构强加给磁盘(见图8.7)。信息就储存在这些箱中, 它们叫作扇区。 图8.7磁盘磁道 可将这些储存箱按它们出现的物理顺序围绕磁道放置,但这种方法有缺点。 想想从一系列磁盘扇区来读取数据,假设将一个磁盘扇区拷贝到内存中,然后迅速回 来读下一个扇区。这没有问题:只需告诉磁盘控制卡想要读哪个扇区,控制卡就会正确地 180页 定位在那个扇区。但是,如果这两个扇区在磁盘上是紧挨着的,那么对第二个扇区的请求 将在该读/写磁头已经经过该扇区的开始部分之后才会产生,要正确地访问该扇区,就必 须等待磁盘旋转一圈(对于每分钟300转的软盘,为五分一秒)的时间。 如果要一个扇区一个扇区地读取一整张磁盘,那些五分之一秒加起来会超过2分钟, 这是该程序用来等待某个特定扇区旋转到读/写磁头下所花费的时间。在进行大量磁盘 I/O的应用程序中,时间的开销会迅速地增加。如果做一些工作来消除这个问题,就可以 显著地改进这类应用程序。 要避免等待磁盘扇区的一条途径,就是将扇区交叉起来,所以,连续的逻辑扇区在物 理上是分开的,也就是说,要在磁道上间隔命名,逻辑扇区号。图8.8图示出某文件的一段9 个扇区的内容在一个磁道上是如何交替存放的。 图8.8存储文件的扇区 在IBM的默认设置值不能适应现代仪器之后,交叉因子已显著地提高了硬盘的性 能。对于大多数系统,3或3以下的交叉因子可以在硬盘驱动器上提供最好的结果。许多共 享件程序在系统中测试这个因子,并推荐最好的因子供使用,然后,按使用者的要求改变 此因子。 磁道地址表让用户指定(给BIO功能)磁道上的每个物理扇区对应哪个逻辑扇区号。 指定每个扇区的大小之后,就可以改变磁道的每个扇区大小。从磁道地址表而来的信息保 存在磁盘上,使磁盘控制卡能找到某个特定扇区,并考虑特别的表和找出磁盘的布局。当 使用磁道地址表来建立该布局时,可产生磁道交叉信息,把它作为磁盘逻辑结构中的一个 永久部分。 磁道地址表是一系列代表磁道、磁头、逻辑扇区号和代码大小的4个字节的项(每个对 应磁道上的一个扇区)。表8.7列举了允许的代码大小。 181页 表8.7代码大小表 代码 扇区大小(以字节表示) 0 128 1 256 2 512 4 1024 磁道地址表所提供的信息放进控制卡为每个扇区所写入的扇区头之中。当控制卡读 磁盘时,它用这些扇区头来定位期望的数据。表中的每一项为一个头提供数据。头就写在 扇区的数据区域前面。头的开始和结尾都由叫作地址记号的特殊的代码来识别;控制卡自 动地处理这些操作。 磁道地址表中的项总是依物理扇区号而安排在磁盘上。每个物理扇区都有一个逻辑 扇区号(按所希望的任何顺序排列)以便能执行交叉。该表还设置磁道上的扇区访问顺序; PC机读磁盘时,它通过访问扇区的头来确定已请求了哪个扇区。要在每个磁道写入多于 或少于9个扇区,就必须改变表中的项数和大小代码,以便所有扇区加到一起所限定的字 节总数不会超出4,608。用稍多一点的字节可获得更大的扇区,而小一些的扇区则允许较 少的字节;扇区的头也占用了一些空间。 一张360K、双面、双密度、每磁道有9个扇区的磁盘,它的第三磁道的磁道地址表看起 来就象图8.8那样。图中逻辑扇区号对应于下列物理扇区: 物理扇区 123456789 逻辑扇区 162738495 表8.8磁盘的出错状态位 ——出错代码—— 十六进制值 二进制值 意 义 76543210 01 .. . . . . . 1 命令错 02 .. . . . . 1. 坏的扇区地址标记 03 .. . . . . 11 写保护出错 04 .. . . . 1. . 坏扇区/未找到扇区 08 .. . . 1.. . DMA过速 09 .. . . 1. . 1 DMA出错 10 .. . 1.. . . 磁盘读中的CRC错 20 .. 1.. . . . 控制卡出故障 40 .1.. .. . . 寻道失败 80 1.. . . . . . 超时 在格式化过程中要指示错误,可在从BIOs功能返回的地方设置一个进位标志。如果 设置了进位标志,则AH就会包含出错代码。表8.8显示了错误代码各个位的含意。如果有 错误发生,程序应该立即调用BIOS int 13h的功能00h(它能再设置磁盘),并适当地处理 那个错误。 fmt_trk()函数是用C语言来执行磁道格式化过程的一个实例(见列表8.6)。该函数 182页 假定用户正在标准的PC驱动器中格式化一张360K、DSDD(双面双密)磁盘。该函数只提 供简单的出错查找;它还假定调用者例程去处理与用户之间的交互。如果成功,则返回0, 其它的值都表示出错。 列表8.6 /*fmt_trk.c Listing 8.6 of DOS Programmer's Reference*/ #include #include #define DISK 0X13 /*PrOtotypeS*/ int fmt_error(char code); fmt_trk(dSk,trk,head) int dsk; int trk; int head; { union REGS regs; struct SREGS sregs; char trktbl[36]; int i; for(i=0; i<9,i++){ trktbl[i*4]=trk; trktbl[i*4+1]=head; trktbl[i*4+2]=i; tpktbl[i*4+3]=2; } regs.h.ah=0x05; regs.h.ch=trk; regs.h.dh=head; regs.h.dl=dsk; regS.x.bx=FP_OFF(trktbl); sregs.es=FP_SEG(trktbl); int86x(DISK,®s,®s,&sregs); if(regs.x.cflag) return (fmt_error(regs.h.ah)); return (0); } int fmt_error(code) /*This routine returns an error code of 1 to indicate that a write-protect error (which can be recoverable) occurred. All other errors are assumed to be nonrecoverable and thus are lumped together. */ char code; { union REGS regs; regs.h.ah=0; int86(DISK,®s,®s); return ((code==3)?1:2); } 183页 8.3 小 结 这一章我们了解了磁盘的基本结构以及它的格式化方式。我们还了解到BIOS只知 道磁道、扇区和磁盘磁头,而不知道文件和目录。BIOS知道怎样安装磁盘的分区表和引导 记录,但它的知识就只有这些。 所有与文件相关的磁盘操作都是DOS级的功能。DOS维护着磁盘的目录、文件和文 件分配表(FAT)这些表的结构和位置是以保存在引导记录(可引导的分区中的第一个 扇区)中的BIOS参数块(BPB)来给出的。 有了磁盘的基本知识后,可以从基础的DOS调用来访问有关磁盘的信息(自由空间、 磁盘容量,等等)。经过本章的学习,就为开始学习第9章作好了准备。