DOS程序员手册(五)

第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
",'A'+drive);
printf("Number Of clusters=%lu
",info.cluSters);
printf("SectOrS per Cluster=%lu
",infO.spc);
printf("Physical sector Size=%lu
",info.SecSize);
printf("DriVe Size=%lu Kb
",
(info.clusters*info.spc*info.secsize)/1024);
printf("
");
get_drvspace('A'+drive,&info);
printf("Drive %c: infOrmatiOn frOm function 36h
",'A'+drive);
printf("Number of cluSterS=%lu
",infO.ClUSterS);
printf("SectorS per cluSter=%lu
",info.Spc);
printf("PhySical sector size=%lu
",info.SecSize);
printf("Drive siZe=%lu Kb
",
 (info.clusters*info.spc*info.secsize)/1024);
printf("Available cluSters=%lu
",infO.avail);
printf("Available Space=%lu Kb
",
(info.avail*info.spc*info.secsize)/1024);
Printf("
");
}
/*Fetch the current drive code*/
unsigned int get_drive()
{
union REGS regs;
regs.h.ah=0x19;
intdos(&regs,&regs);
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(&regs,&regs,&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
",
*argv[1],avail,total);
else
printf("Free disk space on default drive is %lu Kb of %lu Kb
",
avail, tOtal);
}
    编写这个程序来为驱动器名而检查第一个命令行参数。如果没有提供第一个参数,程
序就假定它应该发现默认驱动器的信息。
get_free()函数确定它应检查哪一个驱动器,然后设置参数并进行对DOS的调用(见
列表8.4)。功能36h用于确定自由空间,但大部分磁盘信息已被舍弃,因为该函数的有限
目的并不需要它们。
列表8.4
/*get_free.c
Listing 8.4 of DOS PrOgrammer's Reference*/
#include<stdio.h>
#include<ctype.h>
#include<dos.h>
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(&regs,&regs,&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<stdio.h>
#include<dos.h>
#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,&regs,&regs,&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,&regs,&regs);
return ((code==3)?1:2);
}
 
183页
                 8.3 小    结
    这一章我们了解了磁盘的基本结构以及它的格式化方式。我们还了解到BIOS只知
道磁道、扇区和磁盘磁头,而不知道文件和目录。BIOS知道怎样安装磁盘的分区表和引导
记录,但它的知识就只有这些。
    所有与文件相关的磁盘操作都是DOS级的功能。DOS维护着磁盘的目录、文件和文
件分配表(FAT)这些表的结构和位置是以保存在引导记录(可引导的分区中的第一个
扇区)中的BIOS参数块(BPB)来给出的。
    有了磁盘的基本知识后,可以从基础的DOS调用来访问有关磁盘的信息(自由空间、
磁盘容量,等等)。经过本章的学习,就为开始学习第9章作好了准备。
 
         第9章     目录和文件
      对于编程来说,磁盘文件是最基本的内容。作为程序员,不论是为什么目的(商用、开
发、娱乐或科研)而编程,都必须涉及磁盘文件。于是大多数编程语言都提供了丰富的简单
方法来创建、打开、读、写关闭和删除文件。
      从高级语言来处理文件非常简单,以致我们通常不用考虑在DOS级别上的文件操
作。借助C、BASIC和Pascal所提供的文件处理功能,我们能安全地完成工作而没有太多
的麻烦。但如果采用汇编语言,则必须熟悉DOS的文件操作功能。
      即使不用汇编语言来编程,一些来自C、BASIC或Pascal函数库中的函数也并非是现
成可用的;某些类型的操作,除非使用DOS功能,否则不可能有效地完成它们。
      读者也许会问为什么本书只提及DOS功能——而不提BIOS。因为文件和目录超出
了BIOS的范围。BIOS对文件一无所知。它没将结构(除磁道和扇区以外)分配给磁盘。磁
盘文件结构在DOS的直接控制之下,DOS包括访问磁盘文件和目录所必需的所有功能。
      本章首先看看目录结构,然后讨论磁盘文件和文件功能。最后了解怎样利用最近掌握
的有关目录和文件的知识来建立一个程序,使读者能将某个特定文件安装在目录系统的
任何地方。
                      9.1磁盘目录
      目录第一次出现时,它们代表了磁盘操作系统对磁盘文件操纵方式的一个重要改进。
较早的操作系统(CP/M、TRSDOS、Apple DOS和无数其它的系统)都以相同的方式来处
理文件。在这些平面文件系统中,所有的磁盘文件都借助单一目录便可工作。在DOS 2.0
版出现之前,DOS也采用这种方式来处理文件。
      自DOS 2.0版开始,从UNIX和XENIX中借来了一个文件排序的概念。这种分级目
录方案允许对大量磁盘文件(通常是保存在硬盘上的,但并非总是如此)进行简易排序和
处理。在有分级目录的系统中,每个磁盘都有预先限定大小固定的根目录保存在磁盘的一
个已知位置上。像平面文件这个概念一样,根目录能包括一定数量的同样是可访问的文
件。
      如果要停留在单个根目录的限制之中,那么不会比以前更好。2.0版磁盘的根目录不
能与1.0版磁盘的目录区分开来。但分级目录系统让用户在根目录中创建特别的表项(叫
作子目录),子目录与那些除它们的文件空间包含另外的目录项以外的文件相似。像根目
录一样,依它们自己的行为,子目录也是目录。
 
185页
    但子目录又不像根目录,它没有任何大小的限制。如果必要,它们能扩展而容纳其它
的文件。更进一步,可以在于目录中创建子目录项,每一子目录各自包含一组该子目录管
理的文件。这种子目录的嵌套过程的唯一限制就是路径说明(从根目录到被访文件必须穿
过的目录所经过的路径)的最大大小为65个字符,这一限制使得不允许有超过32个层次
的嵌套。让我们先看看DOS追踪单一文件和目录所采用方法。
9.1.1根目录
    根目录在磁盘上有固定的位置和大小,它们是在磁盘格式化过程中,由FORMAT程
序确定的,根目录的大小和在磁盘上的位置记录在磁盘引导扇区的BIOS参数块中(参见
(第8章“磁盘”)。
   DOS1.0版中,根目录是磁盘中唯一的目录。 DOS2.0版开始支持了目录。正如读者
所知道的那样,子目录只是一种特殊类型的文件,它包括其它的目录项而不是平常的数
据。
    根目录的头两个表项被保留起来,作为BIOS和DOS核心系统文件表项,磁盘自举
程序在系统启动过程中要利用这些表项(参见第3章“动态的DOS”)。图9.1显示了包括
操作系统的典型根目录的第一个扇区。
图9.1根目录中的内容显示
186页
      根目录必须在一个明确定义的点上开始,这样对文件系统一无所知的程序才能找到
    它。内核和BIOS文件必须是储存在磁盘上的最开始的内容,以便在引导磁盘的过程中不
    必提供一个搜索例程而能将它们定位。自举装入程序假定这些文件处在根目录的最开始
    位置,并且至少BIOS文件(1.0版中的文件)是连续保存在磁盘上的。
    9.1.2目录项
      如果想了解DOS怎样跟踪文件和目录,那么绝对有必要了解目录项的结构。了解目
    录项以后,就能很容易地解释它们。
      每个目录项(32个字节的数据)包含文件的识别信息:文件名、扩展名、属性、大小磁
    盘上的起始位置,以及目录项最近更新的日期和时间。表9.1显示一个32字节的目录项
    的基本结构。
                  表9.1目录项的结构
        偏移值                  大小                      意义
        00h                      8个字节                文件名
        08h                      3个字节                扩展名
        0Bh                      字节                   文件属性
        0Ch                      10个字节               保留(未用)
        16h                      字                     最后更新的时间
        18h                      字                     最后更新的日期
        1Ah                      字                     起始的磁盘簇
        1Ch                      双字                   文件大小
      每个目录项都以这种方式格式化,文件信息的每一部分都存储在32字节项的固定偏
    移值上。目录项中的每个域会告知有关文件的独一无二的事情。除了DOS所保存的10个
    字节以外(在偏移值0Ch),本章要相对详细地讨论目录项中的其它各域。
        一、文件名(偏移值00h)
      该项的头8个字节是文件的根名(以ASCII文本串来保存)。现在读者能知道为什么
DOS中的文件名要限于8个字符——在目录项中只有这么多空间可用。
      在许多DOS程序中,可以使用超过8个字符的文件名,但DOS会删简它们,以适于8
个字符限制。如果根文件名没有8个字符长,它就在域中向左对齐并插入一些空格。 DOS
会将所有的文件名都以大写的ASCII字符保存起来。
      文件名域的第一个字节有许多特殊的意义(见表9.2)。
     00h代码可避免DOS对未使用的目录项的搜索。在搜索文件名时,遇上了该代码,它
就会被解释成“目录的结束”,并结束搜索过程。
      E5h字符(在1BM中显示为希腊字符Sigma)能用作文件名的第一个字符,但却存放
在目录项中,当作05h(为什么人们都想把它用在文件名中,这作为一个问题留给读者;但
    自动翻译能做到这一点)。
187页
                    表9.2文件名的头一个字节的特殊含义
      第一字节的值                  意    义
          00h                   项从未用过;后面没有项。
          05h                   文件名的第一字符其实是E5h。
          2Eh                   该项是当前子目录的别名,如果下一个字节是2Eh,则目录的
                                起始磁盘簇包含当前目录的父目录的目录项。
          E5h                   文件已被删除。
    E5h作为第一个字符表示已被删除的文件;在搜索的过程中,DOS会忽略这样的项;
或产生一个新文件时,DOS会再次使用这样的项。只有第一个字节变化了来表示一个被
删除的项;目录项其它部分仍保持不变。从理论上讲,可以把第一个字符变成有效的
AsCII字符,从而使删除的文件复活。但是,被删除的文件占据磁盘空间之前,如果另一个
文件已重新占用磁盘空间,那么就只能彻底删除这个文件了。
    2Eh项是一个句号。尽管只在子目录中发现它。但它表示的是当前目录的一个目录
项。如果下一个字节也是一个句号,那么该项会指向当前目录的父目录。每当使用DOS的
DIR命令,都可以看到每个子目录起始处的点(.)。和点点(..)项;当子目录存在和不能删
除时,就会自动产生这些项。
    任何对ERASE.的尝试都解释成对子目录的ERASE*.*,并且ERASE..变成了对
父目录的ERASE*.*。标准DOS会询问Are you sure(Y/N)?这没提供什么保护,因为
它没有指出将发生大量的文件破坏。在4.0版中,会给出信息明确地告诉你整个目录将被
删除。
    二、文件扩展名(偏移值08h)
    偏移值08h处开始的3个字符是文件类型或扩展名(也作为ASCII字符来保存)。如
果文件没有扩展名,或者扩展名少于3个字符,那么该扩展名就在域中向左对齐并且在名
字中插入一些空格。注意在文件名中的句号,它通常用作根目录和扩展名之间的界线,它
不与目录项中的文件名一起保存。 DOS假定目录项的第8和第9字节之间存在一个字句
号。
  三、文件属性(偏移值0Bh)
    文件属性(偏移值0Bh处的字节)指示目录项所代表的文件的类型和其可访问性。该
属性的每一位都表示了文件的一个特点或特性(见表9.3)。
    只读特性意味着该文件只能从其中读,而不能向它里面写。如果设置了这个位,该文
件就不能被删除。尽管这项预防措施能为文件提供一定的安全性,但文件还是能重新命名
进行修改的。
    其它的属性怎么样呢?隐藏起来的文件对DIR、COPY或许多其它的DOS命令都是
没用的。尽管DOS不能提供工具来完成这种工作,但许多第三方的实用程序还允许用户
在目录项中设置该隐藏位的,它使目录对于DIR也是不可见的。但可用CD命令来进入其
中,然后,就能看到该文件的内部了。
                                                                                          
188页
                      表9.3一个属性字节
                位                                  意义
                76543210
                .......x                          只读文件
                ......x.                          隐藏的文件
                .....x..                          系统文件
                ....x...                          卷标文件
                ...x....                          子目录文件
                ..X.....                          归档文件
                xx......                          保留(未用)
      系统这个词是特指DOS内核和BIOS文件的,但它也能用于其它文件。例如,可以标
记COMMAND.COM和其它的系统实用程序作为系统和最初由非技术方面的人员所使
用的系统中的隐藏文件;该过程储存了用户试图打扫磁盘“房间”时清除掉,但必须恢复的
文件。
      卷标则为所给的磁盘来识别包含该磁盘标记(或名字)的一个目录项。尽管有张磁盘
应该有一个卷标,但直到DOS4.0中才可在大多数磁盘中发挥作用时检查其标记。卷标
记将在本章后面更详细地介绍。
      子目录位表示该文件是一个包含其它目录项的特殊文件。子目录是从属于当前目录
的目录(目录和子目录将在本章后面详细讨论)。
      归档位(文件的状态位)在文件更新时设置。特别地,硬盘备份程序用该位来指示哪个
文件需要备份。
      四、上次更新的时间(偏移值16h)
      偏移值16h处开始的字(2个字节)是文件上次更新的时间。有时叫作文件的时间标
记,它的低字节保存在前面。
      当产生文件时就设置它的时间域,并且不论什么时候关闭该文件,其时间域就会得到
更新;但是只有向该文件中写入了信息,其时间域才会真正地更新。如果该文件只是被读、
拷贝(DOS COPY命令)或被重新命名(用DOS命令),那么该域应当不会更新。当更新时
间域时,新的时间由系统时钟来获得。
      表9.4显示时间域的各个位的含义。小时包含在5个位(24小时时钟)中,而分钟则
包含在6个位中。因为这只留下5个位来存放秒数,所以必须将数除以2。
注意因为秒数除以2,所以时间标记只对秒的偶数是精确的(在大多数应用程序中这种
        限制并不显著)。
      一个鲜为人知的有关文件时间域的事实是:在两个字节中的所有位都是0,则DIR命
令不会显示任何时间。但是如果秒域包括一个1并且其它位都是0,则文件时间显示为12
:00a(午夜)。一些软件发行者利用这一小技巧来帮助识别他们发布的磁盘的修正版本。
        
189页
                         表9.4时间域的编码
                  位
    FEDCBA98 7 6543210                  含义
    xxxxx... ........                        小时
    .....xxx xxx.....                        分钟
    ........ ...xxxxx                        2秒的增量               
    五、上次更新的日期(偏移值18h)
    偏移值18h处开始的字(2个字节)是文件上次更新的日期,有时也把这叫文件的时
间标记。它的低字节保存在前面。
    日期域与时间域想似——产生文件时就设置日期域,并且只有向文件中写信息后再
关闭文件时,该域才会更新。如果只是读、拷贝(用DOS COPY命令)、或重新命令(用DOS
REN命令)文件,则其日期域不会更新。日期域更新后,它对文件最近修改的日期进行编
码(通过系统时钟)。
表9.5显示了日期域的布局。年份保存在7个位中,月份保存在4个位中,而日期则
保存在5个位中。
                            表9.5日期域的编码
                    位
FEDCBA98 76543210           含义
    xxxxxxx. .......x       年计数(相对1980年)
    .......x xxx.....       月
    ........ ...xxxxx       日
注意年份是相对于1980年的。换句话说,它是相对于1980的差,而不是绝对值(如
1988)。例如1988年是以8来保存的,要识别绝对的年份,.只需将差值加上1980。年份占
据了7个位;因为7个位能代表的最大值是127,所以年可从1980(差为0)到2107(差为
127)。
六、起始磁盘簇号(偏移值1Ah)
偏移值1Ah处开始的字(2个字节)是文件的起始磁盘簇号,该字的低字节保存在前
面。
该字只给出了文件的起始点。要分配第二个簇和后面的簇,该域的值也用来计算与文
件分配表(FAT)的偏移值。通过FAT,文件所使用的任何其它簇号也都能定位(见第8章
有关FAT的详细讨论)。
七、文件大小(偏移值1Ch)
有4个字节的文件大小域包含了文件的精确长度,以字节来表示。于是DOS所能操
纵的最大大小是4294967295个字节。因为这个数目比现今最大的MS-DOS硬盘还要大
13倍,所以要达到这个限制数还有几年。
                                                                                                
190页
    一些高级语言(特别是BASIC的早期版本)以将这个值设置成与文件所占有的扇区
中的字节总数相等而著名。如果该文件包含520个字节并占有2个512个字节大小的扇
区,那么文件大小会设置成明显误导的1024。但通常这个项会精确的以字节来反映出文
件的大小。
9.1.3子目录
    前面已经提到,每个目录项都包含文件的一个属性字节。这个属性字节的第4位表示
它是作为子目录指针的目录项。
    子目录是一个文件,就像系统的其它文件一样。子目录项指向目录文件的起始簇号,
目录文件可包含16个项;可以搜索随后的文件簇来找到后面的项。图9.2显示了一个典
型子目录的数据。
                          图9.2一个子目录的数据显示
      子目录的结构确切地像根目录的结构,只有一个除外:在每个子目录的开始处,有两
个特殊的项,它们带有.和..文件名。
      第一项(.)指向当前目录;在这个目录项中的起始簇域指向当前子目录的第一簇。第
二项(..)在父目录中完成同样的工作;这个项的起始簇域指向父目录的第一簇。如果该簇
号为0,那么父目录就是根目录。这两个特殊的目录项都不能删除。相反,想这样做的话,
就会删除掉相应目录中的所有文件。
      不能像处理一般的文件那样来处理子目录。 DOS功能43h不能设置子目录的属性
位。子目录的系统位和隐藏位可被设置以使目录不能被正常地列出,但它的可访问性都不
能改变。 CHDIR还是能到达它那里。
9.1.4卷标
      卷标是一个目录项,在其中设置了文件属性字节的第三位。要解释这一项,所有必需
做的就是将它定位——文件名就是卷标。
      尽管在通常情况下卷标不被DOS 4.0版之前的大多数软件所使用,但要编写利用它
    们的软件,它们就极为有用。程序可用独一无二的卷标来当作磁盘识别者。程序记住哪些
 
191页
  磁盘正在使用以后,它能借助名字而不是一个通用的标题来激活某个特定的磁盘。
    例如,Macintosh能在单个驱动器系统中使用多张磁盘时做到了这一点。在Macintosh
便携式计算机中,显示了系统已识别的磁盘名字,并且如果需要其中的一张磁盘,可用名
字来进行请求。能告诉用户是否插入了错误盘的系统可以用名字来继续要求正确的磁盘。
    尽管大多数PC程序不能以这种方式来操纵磁盘,但现在它们还是能的——感谢卷
标记。产生磁盘标记的唯一方式是通过扩展的FCB功能,本章“扩展的文件控制块”一节
将讨论它们(有关所有FCB操作的DOS功能表请见本书结尾“DOS参考手册”一节)。
    DOS 4.0版发表后,卷标就在世界上传播开来。现在它拷贝到引导扇区中,还有根目
录中,并且DOS在一定程度上利用它来快速确定磁盘是否已改变(如果卷标没变,还要进
行更详细的检查)。
    尽管因为没有办法保证两个不同的磁盘不带有相同的卷标,所以增添了新的东西来
增加独一无二个性的可能性:卷系列号。当磁盘格式化时产生这个系列号,它以从系统时
钟获得的时间和日期标记为基础,并与标记一起保存在引导扇区中。卷标和系列号一起提
供高度可靠的磁盘变化的指示物,因为系列号每2秒种便会产生一次变化。
                    9.2什么是文件
    我们在各种事情中都用到了文件。简而言之,文件是一个用来保存信息的有组织的地
方。文件这个词也用来指设备。在UNIX的带领之下,DOS的2.0版也引入了文件句柄这
个概念。文件系统能分配独特的数字(叫作句柄)给设备如打印机、RS232端口、键盘和屏
幕。我们中的许多人也习惯于将这些设备看作特殊的物体,但文件句柄改变了这一点。
    使用文件句柄这个概念比许多人意识到的要有力一些。借助文件句柄,可以用同样的
技术来访问文件和设备。例如,编写一个涉及键盘和视频屏幕的程序(使用sTDIN和
sTDOUT),就不仅可以使输入重定向,以便输入来自文件;而且可以使输出重定向到一
个文件。同样的程序在每个实例中发挥作用——而用户只需改变输入输出所用的句柄。
                9.3 DOS处理文件的方式
    DOS提供了两种方式来处理文件:FCB方式或句柄功能方式。
    DOS 2.0发表之前,文件控制块(FCB)方式(旧的CP/M系统的派生物)是唯一的一
种访问文件的方式。 FCB功能是围绕程序员直接控制的文件控制块的存在而建立起来
的。
    我们不必掌握文件的所有细节来用于大多数的操作,如打开、关闭、读、写或保存一个
文件(重新命名或删除文件)。对于这些类型的文件操作,可以使用别的方式——句柄功
能。
    句柄功能只给程序员对文件信息的有限访问;DOS内在地控制着文件。程序员借助
某个特定的文件名(打开或创建文件)或利用一个文件句柄(读、写或关闭文件)来请求文
件操作。 DOS使用句柄查看文件的信息。
                                                                                              
192页
 句柄功能比FCB功能有许多的优越性:
      ·因为句柄功能更易于使用,所以它更易于防止错误或改正错误。
      ·句柄功能将保留与DOS及OS/2中变化的兼容性。
      ·它能利用DOS的分级目录结构。
      ·它能减少程序员大量的登记簿工作(登记薄登记文件位置和保存在FCB中的每
        样东西——是在DOS内核中完成的)。
    我们建议,无论什么时候,只要可能,都应利用句柄功能来访问文件。必须使用FCB
功能来创建磁盘卷标,但对于其它文件操作,句柄是处理文件的更好方式。
    不管是否使用FCB或句柄功能,DOS都会熟练地告诉用户它可能检测到的任何错
误。有关DOS错误代码的表,可以参看本书“DOS参考手册”一节中的内容(特别要看看
Int 21h的功能59h)。
9.3.1标准文件控制块
    所有FCB功能所使用的标准FCB,几乎直接来自原始的CP/M环境,它的36个字节
构成了11个域,这一点可从表9.6所显示的标准FCB的布局中看出来。
                              表9.6标准文件控制块的布局
偏移值      长度      意义                    备注
00h          1  驱动器说明            0=默认值,1=A,2=B,依此类推
01h          8    文件名                向左对齐的ASCII码;再插入空格
09h          3    扩展名                向左对齐的ASCII码;再插入空格
0Ch          2    当前块号
0Eh          2    记录大小              默认值80h字节,以及DOS OPEN或CREATE功能
10h          4    文件大小
14h          2    创建或更新日期        与目录表项的格式相同
    16h     2     创建或更新时间        与目录表项的格式相同
    18h      8    保留
    20h      1    当前记录号
    21h      4    随机记录号            如记录大小少于64个字节,则只用3个字节
      文件控制块由DOS所提供的信息所组成,有些直接来自文件目录项中的值。注意不
允许与FCB一起使用路径名。所有FCB功能都在当前目录的范围内操作。
      文件名、扩展名、文件大小以及上次更新的日期和时间都是对文件目录项的反映。其
他域要么被FCB功能初始化,要么被程序员修改来向DOS指示程序员希望的是什么。
9.3.2扩展的文件控制块
      扩展的FCB允许在FCB中包含其它的文件信息。 FCB的扩展部分由7个普通FCB
的起始处添加的7个字节(3个域)组成。
      检查一下FCB的第一个字节,DOS就会告知用户所使用的FCB类型。如果第一个字
节是FFh,DOS就假定它是一个扩展的FCB(标准FCB的第一个字节代表磁盘驱动器标
 
193页
识符。 FFh用作磁盘驱动器号,它是非法值)。
    所有DOS FCB功能都使用扩展的FCB。如果决定使用FCB功能,那么就应使用扩展
的FCB,以便只需追踪一个结构化的FCB区域。
    在扩展FCB中的大多数信息与标准FCB中的信息是相同的。如果将表9.6与表9.7
比较,表9.7详细介绍扩展FCB的布局。从这个比较可看出(从偏移值07h字节开始)两
者的布局是一致的。
                                表9.7扩展的文件控制块
偏移值    长度    意义                  备注
00h           1       FFh                           告诉DOS这是扩展FCB
01h        5    保留                由DOS使用,通常为OS
06h        1    属性字节            与目录项相同的意思
07h       1     驱动器说明          0=默认值,1=A,2=B,依此类推
08h        8    文件名              向左对齐的ASCII码;再加上空格
10h        3    扩展名              向左对齐的ASCII码;再加上空格
13h        2    当前块号
15h        2    记录大小            默认值80h字节,以及DOS OPEN或CREATE功能
17h        4    文件大小
1Bh        2    创建或更新的日期    与目录项的格式相同
1Dh        2    创建或更新的时间    与目录项的格式相同
1Fh        8    保留
27h        1    当前记录号          如果记录大小少于64个字节,就只用3个字节
28h       4     随机记录号
9.3.3基本的FCB文件处理
    要用FCB来成功地工作,可按下列基本的步骤来进行:
    1.将FCB的所有字节设置为0。
    2.获得文件名信息。要做到这一点,可能需要利用DOS的析取功能(29h)。
    3.打开(功能0Fh)或创建(功能16h)文件。
    4.如果记录大小域不是80h,就改变它。
    5.如果要进行随机访问操作,那么就设置记录号域。
    6.设置DTA地址(如果它还未设置)。
    7.执行适当的功能。
    8.完成以后,关闭文件
9.3.4什么时候使用FCB功能
    甚至在DOS 4.0版中,也是因为下列原因而合理地使用FCB功能的:
    ·使用FCB时,可以有数量不受限制的打开文件。
    · FCB提供了一条途径来产生磁盘的卷标。
 
194页
 · FCB保证访问文件的方法与DOS1.0版兼容。
    这第一点是真实的,因为我们有对与文件I/O相关的“内务管理”的全部控制(在最近
的DOS版本中,用户可在CONFIG.SYS文件中指定可同时打开多少个文件)。
    第二点是很重要的,且不论所使用的操作系统是哪个版本的。如果程序要产生磁盘的
卷标,那么就必须使用FCB。
    第三点可能是最重要的:FcB是唯一的证明与使用DOS1.0版的系统兼容性的途
径。如果肯定软件将用于DOS 2.0版或更迟的系统中,或者如果对较早系统的兼容性感
到满意,那么总是应该选择句柄功能。
9.3.5句柄功能
    前面已提到,句柄功能是编程技术中的一个提高。将它们引入DOS,就为文件提供了
与在UNIX中实现的相似的控制。事实上,可将UNIX应用程序(用C编写)移植进DOS。
这些应用程序运行起来就像是它们的UNIX副本。
    应该意识到句柄功能有下面两个基本特性:
    . 在句柄功能下,连续的和随机的访问文件之间没有区别。所有的文件都看作字节
      串,很像一个数组。对文件的这种看法对UNIX文件来说是标准的。
    .句柄是被DOS内部保存的。程序所需的唯一信息就是文件名和句柄号。
    并非所有的程序员都赞成这些特性是优越的。一些程序员反对不得不放弃FCB所提
供的控制;其它的程序员认为不提供随意的访问记录结构,这个系统会降低威力。
    哪种看法正确呢?都不对。对于简单的编程,也可以为了能轻松地使用,而利用句柄
功能。不考虑一些程序员的反对,程序也很少需要做它们自己的文件登记簿记录工作。随
机的文件访问还是有用的,但是必须用不同的方式来采用它。
    句柄只是保存了所有关打开的文件的相关信息的内部DOS表的一个指针。程序员
不必保存对信息的详细说明,他们使用FCB 功能时,也是这样的;相反,句柄功能放弃属
于DOS的所有登记簿功能。对于大多数编程应用程序来说,这个性能显著减轻了程序员
的负担。因为不必操纵或担心特殊的文件控制块,所以理论上,程序更简单、更易于调试并
且更易于保持与DOS的未来版本之间的兼容性。
9.3.6基本的句柄文件处理技术
    使用句柄的技术要比使用对应的FCB 功能的技术简单得多。因为系统操纵了基本的
细节,所以只用识别所需要的文件并让DOS完成余下的工作。在句柄文件处理方法中,可
以遵循下列步骤:
      1.产生一个ASCII文件名字符串。
    2.打开(Int 21h的功能3Dh)或创建(int21h的功能3Ch)文件(3.0版和4.0版提供
了另外的OPEN/CREATE功能;或参看参考手册一节)。
    3.在文件中设置文件指针(Int 21h的功能42h)。
    4.完成所需要的操作。
 
195页
 5.关闭文件
    ASCIIZ字符串只是一个以NUL字符(ASCII码0)作为结尾的ASCII文字串。在高
级语言中,如C中,字符串,通常作为ASCIIZ字符串来保存的。
    要设置文件大小,可在结尾处设置文件指针。然后完成0字节的书写。要想文件达到
精确的某个大小,就需要增加或移去一定的空间)。
9.3.7何时使用句柄功能
    某些例子的产生是由DOS 2.0版或更近的版本提供的改进功能而带来的,在这些例
子中必须使用文件句柄而不是FCB。例如,在下列情况下,必须使用文件句柄:
    ·无论什么时候使用路径名时
    ·无论何时I/O重定向和管道很重要时
    ·支持文件共享和死锁
    ·支持网络环境
    ·使用增强的错误报告
    ·为了能容易地访问文件中的任意地址。
    ·在程序控制之下设置文件大小
    我们建议:无论何时何地,只要可能,都使用文件句柄(但一定要用FCB去产生卷
标)。在所有的程序中使用文件句柄,就能马上获得未来环境的方便,在这样的环境中
FCB不会存在了。更重要的是,可以简化编程任务。
    通常,用高级语言如C或BASIC时,不会考虑下降到本章所讨论的内容的水平上。总
之,高级语言能提供优秀的文件控制操作。但使用者却要使用兼容的功能——句柄。最近
的C、BASIC和Pascal发行物都在它的文件访问例程中使用了句柄功能。
9.3.8练习:目录搜索
    要分析DOS目录功能的使用,让我们开发一个简单的能指出文件在分级目录系统的
位置的应用程序。必须知道的是文件名。本章后面所介绍的程序是用来在文件系统的任
何地方找到文件(给出其名字)。
    这个程序,叫作find.c ,它通过文件结构来搜索(C的循环特性允许向下搜索文件系
统而不使程序过分复杂)。该程序分析了文件句柄功能用来快速而自然地访问信息的方
式。
    这一简单程序的基本技术是对于命令行中的每个变量,从文件系统中搜索拥有那个
名字的所有文件。程序的执行如列表9.1所示。
    列表9.1 
          /* Find.c
          Listing 9.1 of DOS Programmer'S Reference*/
          #include<Stdio.h>
196页
     /* Prototypes */
          VOid depth_search(char*dir,char *name);
          void main(argc, argv) 
          /*    find.c
                This search program locates file names in the directory
                structure of a hard disk. It illustrates the use of the DOS
                directory functions from a high-level language.
          */
          int argc;
          char *argv[];
          {
                int i; 
                for(i=1; i<argc;  i++)
                      depth_search(" " ,  argv[i]);
          } 
      使用与要求的文件名相符的文件搜索过程,搜索例程depth_search()检查文件系统
中每个目录寻找指定文件。
      基本的算法是:
      检查当前目录,找与所期望的文件名相匹配的文件。
      在当前目录中定位每个子目录并继续搜索。
      每当进入这个循环例程,都会产生一个新的磁盘传输区域(DTA)。无论功能何时返
回,前面使用过的DTA又会变成当前的。不是必须只用一个DTA;可以拥有所希望的那
么多的DTA,这决定于解决问题需要的是什么。在这种情况下,这个问题能用下列程序来
得到最好的解决。
    列表9.2
        /* depsrch.C
            Listing 9.2 of DOS Programmer'S Reference*/
        #include<stdio.h>
        #include<dos.h> 
        #include<string.h>
        /* FIRST or NEXT search flags*/
        #define FIRST    0
        #define NEXT       1
        /* File attribute for search*/
        #define     FILE      0
        #define   DIR         16
        /* Prototypes */
        void depth_search(char *dir, Char * name);
        int search(char*fname, int flag, int type);
        void set_dta(char*ptr);
        int Streql(char *str1, char*str2);
        void depth_search(dir, name)
        char *dir, *name;
 
197页
   {
    char filename[256];    /*  File name to search for*/
        char dirname[256];      /* Directory name to search*/
              char dta[43];           /*  Disk transfer area     */
              int flag;                  /*  Search type flag      */
              sprintf(filename,"%s\\%s",dir,name) ; 
              /*Set the DTA to the local DTA buffer*/
              set_dta(dta);
             flag = FIRST;
              while(search(filename,flag,FILE)) { 
                    printf("DEPTH:FOUND: %s\\%s\n",dir,dta+30);
                    flag = NEXT;
              }
              sprintf(filename,"%s\\*." ,dir);
              flag = FIRST;
              while(search(filename,flag,DIR)){ 
                    flag = NEXT;
                    /* Specifically exclude “.” and “..” from searching
*/
      if(!streql(".",dta+30) && !streql("..", dta+30)){
             sprintf(dirname,"%s\\%s",dir,dta+30);
                          depth_Search(dirname, name);
                    } 
                    /* Return to local DTA buffer for next directory*/
                    set_dta(dta);
              } 
        } 
        void set_dta(ptr)
        char *ptr;
        { 
              union REGS regs;
              regs.h.ah = 26; 
              regs.x.dx=(int)ptr;
              intdos(&regs, &regs);
        } 
        int streql(str1, str2) 
        char *str1, *str2;
        {
              return (strcmp(str1, str2)==0);
        } 
    该程序控制了对目录结构的搜索,但利用DOS找到第一个文件的功能(4Eh),根据
特定的搜索准则来定位文件。每当找到一个文件,DOS都用文件信息来更新DTA。从文
件的目录项获得的信息用在depth_search()中来打印文件名(或访问目录)。
    列表9.3显示例程search(),它与DOS功能之间有相互作用。
    列表9.3
        /* search.c
            Listing 9.3 of DOS Programmer's Reference*/
        #include<stdio.h> 
        #include<dos.h> 
198页
            #define  FALSE    0
            #define    TRUE !FALSE 
             int search(fname,flag,type) 
            char*fname;
            int flag;
            int type;
            { 
                  union REGS regs;
                  regs.h.ah = 0x4e+flag; 
                  regs.x.cx=type;
                  regs.x.dx=(int)fname;
                intdos(&regs,&regs);
                if(regs.x.cflag==1)
                        return (FALSE);
                  return (TRUE);
                                  } 
      注意在该例程中给每个功能调用设置了相同的寄存器,不管是在寻找过程中文件的
第一次出现,还是在寻找过程中文件的又一次出现,它都是这样。唯一的差异存在于AH
中的设置(4Eh用于发现第一次,4Fh则用于发现下一次)。尽管所有的设置信息只为
FindFirstFile功能(4Eh)而需要,但它不会制止FIndNextFile功能(4Fh)的操作。有关这两
个功能的更详细情况,请参考“DOS参考手册”一节。
      当搜索到达分级系统的另一级时,必须从前一级来保留DTA以继续搜索下列例程,
set_dta()可标记一个缓冲区来作为当前DTA(该例程包含在列表9.2中):
        void set_dta(ptr)
        char * ptr;
              {
              union REGS regs;
              regs.h.ah=26;
              regs.x.dx=(int)ptr;
              intdos(&regs,&regs);
        } 
      所保存的例程streql()分析了用C功能进行编程的便利。在这个例程中,strcmp()(标
准的C库函数用来比较两个字符串)在字符串相同时返回0。但有时,特别是在建立一个
程序的早期开发阶段,产生你真正希望的那种助记提醒信号的函数是便利的。例如,根据
两个字符串是否等同,streql()返回TRUE或FALSE。利用streql()函数,可以使逻辑更清
楚并且能将精力集中到需要解决的问题上。通过限定streql()宏可以从该例程中挤出一点
速度:
      define streql(x,y)(strcmp(x,y)==0)
 
199页
    使用这个宏可以消除一个功能调用的内部开销。那么为什么不作呢?完全可以作——
依据程序员的意图而定。将streql()定义为一个函数,就可以把它包含进一个库中,并且
在需要它的时候,连接程序能肯定它在那里。如果将它定义为一个宏,必须在程序或一个
包含文件中定义这个宏,以使它有用。一种方法易于开发,一种则能消除一些开销。
    对于大多数程序,选择使用哪种形式取决于程序员。一些程序员喜欢函数;其他人则
喜欢宏。程序开发过程中唯一的关键因素是明确性。需要使各样东西都尽可能地清楚以
使开发问题变得最小。
    streql()例程的代码表如下(该例程包含在列表9.2中):
      int streql(str1,str2)
      char *  str1,
              * str2;
        {
                return (strcmp(str1, str2)==0);
        }
    既然已详细地介绍了find.c 的各个部分,那么让我们把这个程序用于测试驱动器,下
面的测试运行搜索了autoexec.*文件的所有发生的情形:
        C>find  autoexec.*
        DEPTH:FOUND:AUTOEXEC.BAT 
        DEPTH:FOUND:AUTOEXEC.DV 
        DEPTH:FOUND:AUTOEXEC.BAK 
        DEPTh:FOUND:AUTOEXEC.WIN
      DEPTH:FOUND:BINLOTUSINSTALLAUTOEXEC.BAT 
      DEPTH:FOUND:SYSAUTOEXEC.DV
      DEPTH:FOUND:SYSAUTOEXEC.OLD
      DEPTH:FOUND:SYSAUTOEXEC.BAT
                      9.4小      结
    从本章我们了解了文件访问由下列两种方法之一来处理:文件控制块(FCB)或文件
句柄。FCB是较老的形式,但总是能与DOS 1.0版兼容。磁盘卷标也只可用FCB来书写,
但其它各种文件访问都能由文件句柄功能来完成。
    句柄功能在DOS 2.0版引入的分级目录结构中工作。它们也能使程序设计变得简单
得多,因为它们让操作系统完成文件的登记薄工作。
    基于我们已经了解的知识,我们准备开始学习下一章“程序和内存管理”,它涉及到程
序执行的一些知识。
 

第10章程序和内存管理

    为了“使作业做完”,程序员已开发了许多装置和设备来从程序中获得内存所能包含

的更多信息。程序的连接、覆盖和其它技术长期以来一直是程序员技巧的主要成分。例如,

我们第一个使用的系统是只有32K内存的IBM 7040;有些程序没有覆盖技术就不能运

行(参见本节覆盖技术的讨论)。

    特殊化的技术能转变成调试时讨厌的东西,使程序高度地不可移植。因为大的系统可

以有增加的内存和处理机速度,所以计算机程序员已开发了一些技术来从其它程序内部

处理程序。命令外壳,如COMMAND.COM就采用这些技术来按需要执行程序。

    在一个DOS系统中,可以使用COMMAND.COM所使用的相同技术来从一个程序

的内部控制另一个程序的执行(这种能力与在UNIX系统中提供的性能相似)。这种操纵

程序功能的方式是相对清楚的。系统的重要模式能以单一标准的程序的形式存在。每个

这样的程序都能作为一个独立的实体而完整地测试和调试。UNIX程序员已经发现这项

技术对于程序开发是极为有效的。

    本章先介绍有多少内存可发挥作用,当需要时怎样获得更多内存以及怎样让DOS保

存不需要的内存。当在高级语言中工作时,编译程序管理着这个过程。而在汇编语言中,

就应该明确要对内存做些什么。本章还要看看扩展内存和扩充内存并检查一下能对它们

做什么。

    然后讨论程序执行——一个进程(父进程)怎样执行另一个进程(子进程)然后重新获

得控制。下面给出了一个简单的实例来说明这个过程的工作方式。

    最后解释一种特殊程序—TSR(终止和驻留)。TSR在许多方面与“通常的”程序

(应用程序如字处理程序和电子表格)不同。最明显的是TSR停止后,它们仍留在内存中,

并且不会被覆盖。TSR能以许多途径从另一个程序获得控制,其中最重要的是涉及一个

中断(见第11章“中断处理程序”)。本章将视野放在要遵循的程序上。

    然后,让我们继续看看内存管理。

                              〈程序覆盖〉

      程序覆盖是在内存中没有稳定地保持住的一部分代码或数据。该程序可能

    以许多二元图象的形式存在,其中每个图象都操纵一些特异的功能。主程序段操

纵了整个坐标,并且常常拥有覆盖所需的功能。

      在一个有覆盖的典型程序中,主菜单和普通功能在主覆盖块中,后者总是保

 

204页

存在内存中。无论程序何时需要一个子菜单,覆盖就安装在内存中的一个拥有覆

盖代码的区域内。然后主模式将控制传递给覆盖块,子菜单及其功能就能操作

了。

      用户结束了菜单,返回主菜单时,控制就传递回一级覆盖。因为子菜单的覆

盖在内存中不再需要,所以其内存空间能被另一个覆盖所使用(子菜单的程序代

码被新代码覆盖)。

                    10.1内存的工作方式

      基本的PC机或兼容机都有一个拥有1M内存的地址空间(记住一兆的字节是

1024K)。第3章“动态的DOS”指出该内存中只有640K能用于程序使用。剩下的384K分

配给ROM BIOS、显示适配器和盒式磁盘。

      但是较低的640K内存不只用于用户程序。其中,1024个字节的中断向量表由处理器

保存。只有中断向量在这些位置上,才能硬连线到处理器中(见第11章)。然后是BIOS和

DOS参数表、DOS内核、系统驱动程序,最后是命令处理程序的驻留部分。所有这些内存

都来自内存的用户区域。

      要计算失去的内存数量是很困难的,因为它要依据所使用的系统和巳安装的驱动程

序而定。安装的第一批用户程序(在内存图中)是TSR如Borland的SidekicK(10.1)是

已安装的TSR所使用的内存图示)。必须从有效空间中减去它们的内存使用。如果增加一

个与DESQview相似的窗口包,就只有大约350K内存可能从640K的起点上保存下来

——并且没有真正地启动程序去做什么!

      启动之后剩下的空间(通常用于安装Sidekick,大约500K到550K)是空着的,并且可

以使用。这个空间叫作暂用程序区域(TPA),这个名称也用在CP/M系统的等价区域上。

这个名称是合适的,因为用户程序在这个区域中暂驻——它们来或者去都是依据在系统

中工作的用户的需要而定的。

      PC机产生时,机器的内存640K的限制对于一个工作区域看起来是有限的。当时,具

备640K内存的PC是标准的;那些拥有128K内存及少数拥有256K内存的人是令人嫉

妒的。随着需求的增长,640K限制变成了一条非常有趣的笑话——它成了重要的系统限

制。Murphy法则的变化用于计算机内存:“不论你有多少,你总是需要更多的”。就成了一

个大笑话。

      有了80286芯片之后,PC系统就可能拥有16M的内存(有人说:“万事大吉”,其它人

则说:“现在我们有了一个更重要的系统”)。但DOS不能以相应的操作来利用这个内存。

超过1M限制的内存,已知是扩展内存,它对于80286处理器和后来的模型都是有用的,

DOS却不能使用它;要访问这种内存,80286芯片必须在保护方式中工作,DOS做不到这

一点。在DOS下面,只有特别编写的、能够打开CPU方式的程序才能使用扩展内存。

 

205页

    图10.1已安装的TSR所使用的内存

                                〈保护方式〉

        80286和80386处理器所具有的保护方式能访问控制多任务操作的特殊处

    理器功能。实地址方式提供8088/8086处理器只用来访问1M内存的同样的环

    境。将这个处理器移到保护方式中(通常需在一个操作系统中进行),该系统能控

    制多个程序在内存中的操作以及从一个任务到另一个任务的切换。

      本章不会详细介绍保护方式的性能,因为DOS不使用这种方式。要了解保

    护方式的更多信息,可查阅有关80286/80386 汇编语言程序设计方面的书籍。

    DOS在处理器的实地址方式中运行;于是1M以上的内存对于那些通过普通的DOS

功能来运行的程序来说是不可访问的。即使当一个系统有额外内存时,用户也不能有效地

使用它。

    尽管一些程序如DESQview能将它们的一些操作码插进使用80286扩展内存(像磁

盘一样)的RAM——磁盘驱动程序内,该内存对于大多数程序还是不能访问的(程序必

须在访问内存并完全返回实地址方式之前将处理器切换到保护方式)。 RAM磁盘在扩展

内存中操作时,它们也不必那么快,因为处理器从实地址方式切换到保护方式并返回来获

 

206页

得数据——这个过程可能相对较慢。在80286处理器上,换到硬盘可能更快一些。

      自从产生了个人计算机的AT型号以来,BIOS提供了两个功能来帮助程序确定有多

少内存是有用的(Int 15h,功能88h),然后将数据块移向或离开1M标记以上的扩展内存

(Int 15h,功能87M。这些例程的使用会带来严重的问题,因为没有对扩展的内存的空间

进行管理。一个程序能很容易地在另一个程序(或一个RAM——磁盘驱动程序)保存在

扩展内存空间中的数据上进行写操作;没有什么能说明这个问题或报告这个错误。

      扩充内存的第一次出现,是作为一个联合工作组提出的。该工作组由Louts和Intel

在1985年春季的COMDEX展示会介绍给人们的(3.0版本),它提供了一条途径,允许访

问8M那么大的内存,而不需要在处理器方式中进行特别的移动。扩充内存使处理器能通

过16K页来访问额外的内存,该页能映射进640K与1M之间的内存空间里未用的区域

之中。4个16K页映射进64K框架,其位置由安装板时用户的系统决定(见第3章“动态

的DOS”,那里深入地讨论了扩充内存)。

      扩充内存管理(EMM)使扩充内存活动起来像一个带有句柄的文件。当程序在扩充内

存中请求空间时,EMM就将这个空间放在一边,并返回能用来获得对该空间进行访问的

一个独一无二的“句柄”。

      要想从扩充内存中获得16K页,可以使用Int 67h来调用EMM,并告诉它将该页放

进页框架之中。然后就能直接从程序中定位此内存。图10.2显示了所发生的事情。

                           图10.2扩充内存的访问

      将下列任意一行加到CONFIG.SYS文件中就能安装EMM.SYS(扩充内存管理器

驱动程序):

          DRIVER=EMM.SYS

          DRIVER=EMM386.SYS

      这些驱动程序运行起来部分像一个真实的驱动程序(真实的驱动程序在第12章“设

备驱动程序”中介绍;目前,就先认为扩充内存管理器驱动程序不能正常工作)。不能像对

待其它驱动程序那样去访问EMM.SYS或EMM386.SYS,而是通过Int 67h来访问它的

 

207页

功能。可参看“DOS参考手册”一节中有关Int 67h各种功能的详细列表。

    EMM.SYS包括以下这些有用功能:

    ·报告扩充内存的状态

    ·在扩充内存中分配页

    ·在扩充内存中释放分配页

      ·诊断

      ·多任务支持

    ·将物理页映到扩充内存中,变成分配给程序的逻辑页。

    这种内存分页技术长期以来用于计算机中。当有扩充内存可用时,没有理由不去利用

它。

    扩充内存产生后不久,Microsoft宣布它支持该标准的3.2版本,其中包括了对于多

任务操作系统有用的设备。3.2版本变成了LIM EMS(Lotus-Intel-Microsoft扩充内存规

范)。Ashton-Tate, Ast Research和Quandram注意到这种标准的局限性:只能将一个16K

页映射到内存的一个未使用区域之中。那么为什么不将来自TPA的内存的大区域映射

进去呢?这就引导人们开发了增强的扩充内存规范(EEMS)。

    用增强的扩充内存就能将一个完整的程序移到扩充内存区域中并代替另一个程序。

这种功能意味着一台PC机能变成多任务的系统。1988年产生的LIM4.0,除了包含旧的

LIM 3.2以外,还包括EEMS。所有涉及扩充内存技术的公司都在一定程度上支持这个新

的LIM,尽管并非所有的公司都执行了该标准中要求的所有功能。

    只要我们使用基本的PC机,就必须学会与分配给我们的640K的空间区域一起生

活。当然,新的机器型号突破了这个限制。

                    10.2内存管理

    DOS之下的内存管理涉及到TPA中的自由区域。要保持程序和未来出现的操作系

统之间的兼容性,就应该将DOS调用用于所有的内存分配和释放的请求之中,

    尽管当前我们能在内存中使用各种技巧,但当移向多任务系统时,这些使用了特殊技

巧的程序会停止运行。了解了在DOS局限性下工作,就能编写出更便利的程序。遗憾的

是,一些技巧(如直接访问视频显示内存)是DOS编程的主要成分。没有它们,系统不会敏

感到能给用户提供专业程序所必需的“感觉”类型。尽管这些花招使程序没有那么方便,但

有时要编写好程序,失去一点便利性也是必要的。

    而在内存分配中却没有这些技巧。甚至一个小的错误也会使该系统死锁。让我们先

看看DOS控制TPA中内存的方式。

    TPA安排在一个叫做“内存场(memory arena)”的结构之中。 DOS维护着这个被称为

场项的内存块的链表,每一项都有它自己的特殊控制块,叫做“场头(arena header)”(第3

章讨论这些题目,表3.3显示了场头的布局)。3个DOS功能(Int 29h,功能48h、49h和

4Ah)用于请求或释放内存:“DOS参考手册”一节将详细讨论每个功能。

 

208页

 场链(见图10.3)将每个内存块链接成内存块表。不论两个空白内存块何时碰到了一

起,它们都结合成单一的较大的,且在链中只有一个场头的块。

                                    图10.3内存分配链

    请求内存时,DOS搜索内存场链来分配一个能满足请求的块。要分配这些块,DOS采

用了最先适配策略,其中它利用了第一个够大的能满足需要的块。如果该块所含内存超过

了需要,就将它分成两部分,多余的内存作为一个独立内存块放回到链表中。

    DOS之所以使用这个策略,是因为它是通常使用中最有效的。从DOS 3.0版开始,可

将分配策略变成下列替代方法之一:

      ·最佳适配在这个策略中,要搜索整个内存并且用与要求最紧密匹配的内存块来

          填满它。

      ·最后适配使用链中能满足分配需要的最后一个块(该块带有最高的内存地址)。

    内存分配策略不需改变,因为最有效的方法已经在使用(用DOS 3.0版进行的非常

有限的测试,看起来是指示DOS在它安装任何程序时都继续使用最先适配策略,也不

管设置了哪种方法。这个结果并不是最后的——因为随着所涉及的变化因素的增多,就需

要更多的测试来提供有限的回答。一个问题是DOS的装入程序开始时,总是要试着获得

所有保留的RAM)。

    无论DOS何时得到一个分配请求,它都会检查内存场链表,看看是否存在什么问题。

如果在写内存时,冲掉了这个场头,或者损坏了这个表,那么程序就会废弃并且看到这样

一条信息:Memory Allocation Error(内存分配出错)。

 

209页

10.2.1压缩程序内存

    当COM程序启动时,系统会将所有内存都分配给它,因为DOS生来就是一个单用

户的操作系统,每次只能运行一个程序。装入程序一开始就请求65,535个段(比可能得到

的要多)来找到有多少可用的段,然后它再要求精确的数目。

    EXE程序也分配了所有内存,但在这里如果愿意,就可以减少所分配的内存。EXE程

序的程序头有2个参数:MINALLOC和MAXALLOC。前者是程序运行所需的最小的内

存分配。如果一个块有这个最小数量的内存,那么这个块有效时,该程序就能运行。

    但DOS总是在可能的情况下,努力将一个带有MAXALLOC内存字节的块分配给

程序。 Microsoft的连接程序,总是设置MINALLOC为0,MAXALLOC为FFFFh(1048560

个字节),除非用户告诉它使用别的值。无论何时,在没有特异化这些值的情况下连结一个

EXE程序,都要保证它获得所有有效的内存。

    注意Turbo C和Microsoft C都在启动时自动释放多余的内存。但Turbo Pascal像

Microsoft的连接程序一样不能自动地完成这件事。在Turbo Pascal 4.0版和5.0版中(它

们能产生EXE文件),所有有效内存都被默认值所使用,将最高区域管理成一个堆。但是

如果想自己控制它,有一个编译程序指令,就能设置大小。另外,内部变量(在手册中已清

楚地公开了)精确告知使用内存的方式。

    旧的Turbo Pascal版本能产生COM文件,这些版本之所以能限制程序所用内存数量

的唯一方式,是因为在编译过程中为堆设置一个最大值的容量。由于内存分配的方式,以

及程序之上的堆栈和堆,所以不存在有效的方法来确定Pascal COM程序结束的地方。

    BASIC也向程序员提出了这一问题,因为没有有效的方式来确定程序在内存中的结

束地址,也没有办法启动一个程序,除非访问EXE功能,特殊的MEMSET功能让程序设

置内存上限,但该功能有意允许BASIC程序安装汇编语言支持例程而不是提供其它程序

的执行。CHAIN和RUN(BASIC语句用来执行其它BASIC程序)用一个新程序代替业巳

存在的程序,而不是保持当前程序的状态。

    在高级语言中,C和Turbo Pascal的较新版本都适于动态的内存分配工作。C给内存

分配和释放提供了较好的功能;从来不必为此目的而访问DOS功能。最近的Turbo Pascal

版本综合了传统(或标准)Pascal的许多扩展名,并以略有不同的名字提供了像C一样的

功能。

    只有以汇编语言编写的程序必须明确地将内存在启动处返回到内存池中。列表10.1

显示了这样做的方法。

    列表10.1

;Freemem.asm

            mov sp,offset stack          ; Move stack to safe area

            mov ah,4Ah                   ; Setblock

            mov bx,280h                   ;Retain 10K of space

            int 21h

            jc    alloc_error            ; Allocation error

210页

;            [MORE PROGRAM CODE]

      dw 64 dup(?)

     stack  equ $

10.2.2获得更多的内存

      如果程序需要额外的内存,它可以用修改内存分配功能(Int 21h,功能4Ah)来请求

额外的内存。作为回报,如果内存是可用的,进位标志会清零,AX寄存器含有内存基的段

地址,如果没有足够的内存来满足请求,就会设置进位标志,AX含有一个出错标志(7=

控制块破坏,8=内存不够),寄存器BX拥有最大有效块的大小。

      C和Turbo Pascal的较新版本都提供有用的函数以获得额外的内存并释放内存。旧

    的Turbo Pascal版本(V4之前的)请求另外内存的能力最小或不存在,除非使用特殊的编

译程序开关。BASIC未提供方法来改变给BASIC程序的内存分配,也没有提供方法来确

    定从哪里进入内存,去限制内存使用。

      只有在汇编语言中才必须直接访问DOS分配功能。在C和TurboPascal的较新版本

    中,由库分配例程去访问DOS功能。列表10.2提供了一种方法来获得内存,并确定如果

    禁止第一次尝试,将留下多少内存。

      列表10.2

            ;Getmem.asm

            getmem: mov         ah,48h                    ;Allocate memory

                      mov       bx,bufsize               ;16K memory

                      int      21h

                      jc         nomem                    ;Cannot allocate

                      mov        bufSeg,ax                ;Save pointer

                      jmp        pgm                       ;Continue program

            nomem :   Cmp       aX,8

                      jnz        quit                      ;Major alloc error

                      mov        bufsize,bx                ;Save buffer size

                      mov        ah,48h                  ;Allocate memory

                      int      21h

                jC    quit                 ;Still cannot allocate

              pgm:

          ;                 [MAIN PART OF PROGRAM]

          done:     mov     ah,49h                ;De·allocate memory

              mov      es,bufseg     ;Point to buffer segment

                          int        21h

          quit :            [EXIT CODE GOES HERE]

          bufsize dW      400h

          bufseg dw          0

      在没有访问DOS有关内存分配的请求的情况下,许多内存分配和释放的功能对于用

C或Turbo Pascal的较新的版本编写的程序都是有用的。使用这些有效的功能很有意思,

因为它们可明显简化操作,并将它保持在语言系统的控制之下。Turbo Pascal V4.0以前

的版本在堆中拥有所有可用的内存,并且提供了程序空间的分配和释放功能。这些Pascal

 

211页

分配和释放例程不会从内存场中获得或返回空间。BASIC不请求也不返回什么。

    使用BASIC和Pascal(Turbo Pascal 4.0之前)的程序员能用编译程序开关来人工限

制内存,然后用DOS功能调用来请求释放另外的内存。但这个练习会带来混乱和内存处

理不力。必须有动态内存处理的程序,应使用C来编写,或用新的Turbo Pascal,或者干脆

用汇编语言来编写。

                    10.3扩充内存

    或迟或早(当在PC上用完内存并已添加所有必要的芯片来使程序达到640K上限

时),用户可能要购买一块扩充内存板。有了扩充内存,就能对大量数据存储(EMS)或多

任务区域(EEMS)进行快速而有效的访问。

10.3.1确定扩充内存的有效性

    要确定是否已安装了扩充内存,可使用下列方法之一:

    ·试着打开文件EMMXXXX0(设备驱动程序的推荐名)。如果能成功能地打开,那

      么就有驱动程序存在或存在有相同名字的文件。要看看是否有驱动程序,可用

      IOCTL功能来给出一个“获得输出状态”的请求。驱动程序返回FFh;文件就返回

      00h。关闭该文件,以便重新使用这个句柄。

    ·考察在Int 67h向量位置的地址。该地址是驱动程序的中断入口点。如果有EMM.

      SYS(或一个可替代的驱动程序),所给的段地址就是该驱动程序的基地址;该段

      地址中的偏移值00Ah处,驱动程序名作为驱动程序头的一部分而出现。尽管该

      过程比打开文件的过程(打开和关闭的开销是显著的)要快,该方法要依赖程序去

      访问其正常内存范围以外的内存。

    列表10.3和列表10.4给出了两个用C编写的独立的例程,检查EMS驱动程序是

否存在。

列表10.3

        /*emmtest.c

        Listing 10.3 of DOS Programmer's Reference*/

      #include <dos.h>

      int emmtest()

      {

          union REGS regs;

          struct SREGS sregs;

          short int result;

          unsigned int handle;

          regs.h.ah=0x3d;           /*Open file*/

          regs.h.al=0;               /*Read mode only*/

          regs.X.dX=FP_OFF("EMMxxxX0");/*File name*/

          sregS.dS=FP_SEG("EMMXXxx0");/*Set the DS register*/

          intdosx(&regs,&regs,&Sregs);

          handle=regs.x.ax;          /*File handle*/

212页

              /*If opened OK,then close the file*/

              if(result=(regs.x.cflag==0)){

                    regs.h.ah=0x3e;

                    regs.x.bX=handle;

                    intdoS(&regs,&regs);

              }

             return (result);

        }

    列表10.3用打开文件的方法去确定EMM是否已安装,它不会去检查IOCTL调用,

看看该方法返回了一个文件或是一个驱动程序。大多数情况下,这不是一个问题。但为了

安全起见,应该向该功能增加一个IOCTL调用的检查。

    注意:用Borland C++编译该函数,要在if()语句中产生一个警告,该语句设置re-

sult。这个警告能安全地忽略,因为所期望的效果是设置功能返回值为TRUE或FALSE,

根据测试结果而定,编译程序虽抱怨它但仍能处理它。

    列表10.4对于某些不习惯涉及远(far)指针的C程序员,它变成了一个真正的问题。

当在PC机上工作时,必须意识到各种指针之间的区别:在一个段内进行指向操作的指针

(near指针);能够指向内存、所给定的段和偏移值地址的任何地方的指针(far指针);以及

能指向操作内存任何位置的指针,就像内存没有划分成段一样(huge指针)。不论何时使

用指针变量,都必须谨慎并且应一直保持与它们之间的匹配;否则意外的结果会阻止程序

运行。

    列表10.4

          /*emmchk.c

          LiSting 10.4 of DOS Programmer's Reference*/

        #define FALSE 0

        #define TRUE    !FALSE

          #include<Stdlib.h>

          #include<Stdio.h>

          #include<dos.h>

          #include<alloc.h>

          int emmchk()

          {

            union REGS regs;

            struct SREGS sregs;

            char far*emptr,far*nameptr,far*fmptr;

            nameptr="EMMxxxx0";

            regs.h.ah=0x35;           /*Get Interrupt vector*/

            regs.h.al=0x67;              /* Get it for the EMM */

            intdosx(&regs,&regs,&sregs);

            /*Make a FAR pointer to access the driver*/

            emptr=MK_FP(sregs.es,0);

            fmptr=farmalloc(sregs.es);

            if((emptr=fmptr)==NULL{

                  printf("Unable to allocate far pointer.\n");

                  exit(0);

            }

213页

        /*Return TRUE if they are the same for eight CharacterS*/

        return (farcmp(emptr+10,nameptr,8));

}

int farcmp(Str1,str2,n)

       char far*str1,

            far*str2;

        int n;

{

        while(*str2 && n>0){

              if(*str1!=*str2)return(FALSE);

                  n--;

                  str1++;Str2++;

          }

              return (TRUE);

    }

    一些书籍介绍扩充内存的方式会产生一个更为严重的错误。因为许多扩充内存方面

的讨论都错误地解释说Int 67h处的指针指向驱动程序的开始,并且所给的在Int 67h向

量上的内存地址的10个字节是名字EMMXXX0。马上就会知道这种说法不会是真的;中

断向量在被调用时会指向控制传递的位置,但驱动程序的起始字节是驱动程序头的一部

分,它不能是可执行的代码(这个问题将在第12章里讨论)。

     该向量提供了地址的段部分,但在段内部,在绝对偏移值000Ah处能找到这个名字,

因为驱动程序的内存总是在段边界上对齐(只能分配多个完整的段)。任何已装入的驱动

程序基地址的段地址都要规范化——即段地址要调整,直到偏移值为0。

     列表10.5是一个用两种方法来检验扩充内存是否存在的小程序,它还显示每种方法

返回的内容。                                      

 列表10.5

/*testemm.C

      Listing 10.5 of DOS programmer'S Reference*/

#include<Stdio.h>

VOid main()

          int emmchk(void);

          int emmteSt(void);

          if(emmchk())

                printf("MEM:Expanded memory is present\n");

          else

                printf("MEM:Expanded memory is NOT present\n");

          if(emmteSt())

                printf("OPEN:Expanded memory is present\n");

          else

                printf("OPEN:Expanded memory is NOT present\n");

}

10.3.2使用扩充内存

    知道有扩充内存可用时,就可以使用扩充内存功能(与Int 67h紧密相关)来获得并使

                                                                                         

214页

用内存(附录部分的EMS一节将详细介绍扩充内存的功能)。列表10.6给出了一个使用

Int 67h的内存功能的一个简单实例。

列表10.6

/*emstest.c

Listing 10.6 of DOS Programmer's Reference*/

#include<stdio.h>

#include<Stdlib.h>

#include<string.h>

#include<dos.h>

#define FALSE 0

#define TRUE !FALSE

#define EMM 0X67

Char far*emmbase;

void main()

{

unsigned int emmhandle;

char teststr[80];

int i;

int emmtest(void);

int emmok(void);

unsigned int emmalloc(int n);

unsigned int emmmap(unsigned int handle,int phys,int page);

void emmmove(int page,char*str,int n);

void emmget(int page,char*str,int n);

unsigned int emmclose(unsigned int handle);

/*Is there any expanded memory?*/

if(!emmtest()){

printf("Expanded memory is NOT present ");

printf("Cannot run this program ");

exit(0);

}

 /*Is the expanded memory manager functional?*/

if(!emmok()){

printf("Expanded memory manager NOT available ");

printf("cannOt run this program ");

exit(0);

}

/*Get ten pages of expanded memory for the demo.*/

if((emmhandle=emmalloc(10))<0){

printf("There are not enough pages available ");

printf("cannot run this program ");

exit(0);

}

/*Write the test string into each of the ten pages.*/

for(i=0;i<10;i++){

Sprintf(teststr,"This info is in EMS page %d ",i);

emmmap(emmhandle,i,0);

emmmove(0,teststr,strlen(teststr)+1);

}

 

215页

/*Now read them back in and recover the test string.*/

fOr(i=0;i<10;i++){

 emmmap(emmhandle,i,0);

emmget(0,teststr,strlen(teststr)+1);

printf("Reading from block %d:%s",i,teStstr);

}

/*Finally,release the expanded memory*/

emmclose(emmhandle);

}

int emmtest()

{

union REGS regs;

 struct SREGS sregs;

short int result;

unSigned int handle;

regs.h.ah=0x3d;/*open file*/

regS.h.al=0;/*Read mOde Only*/

regS.x.dx=(int)"EMMXXXX0"; /*File name*/

sregS.ds=_DS; /*Set the DS register*/

intdosx(&regs,&regs,&sregs);

handle=regS.x.ax;/*File handle*/

if(result=(regs.x.cflag==0)){

regs.h.ah=0x3e;

regs.x.bx=handle;

intdos(&regs,&regs);

}

return (result);

}

int emmok()

{

union REGS regs;

regs.h.ah=0x46;/*Get manager status*/

int86(EMM,&regS,&regs);

if(regs.h.ah!=0)return (FALSE);

regs.h.ah=0x41;/*Get page frame segment*/

int86(EMM,&regS,&regs);

if(regs.h.ah!=0)return (FALSE);

emmbase=MK_FP(regs.x.bx,0);

return (TRUE);

}

unsigned int emmalloc(n)

int n;

{

union REGS regs;

regs.h.ah=0x43; /*Get handle and allocate memory*/

regs.x.bx=n;

int86(EMM,&regs,&regs);

if(regs.h.ah!=0)

return (-1);

return (regs.x.dx);

}

unsigned int emmmap(handle,phys,page)

unsigned int handle;

 

216页

              int phys,page;

          {

              union REGS regs;

              regS.h.ah=0x44;        /*Map memory*/

              regs.h.al=page;

              regS.x.bX=phys;

              regS.x.dX=handle;

              int86(EMM,&regS,&regs);

              return (regS.h.ah==0);

          }

          vOid emmmOve(page,str,n)

              int page,n;

              Char *str;

          {

              char far*ptr;

              ptr=emmbaSe+page*16384;

              while(n-->0)

                    *ptr++=*Str++;

          }

          void emmget(page,str,n)

              int page,n;

              char*str;

          {

              char far*ptr;

              ptr=emmbase+page*16384;

              while(n-->0)

                    *Str++=*ptr++;

          }

          unSigned int emmclose(handle)

              UnSigned int       handle;

          {    

            union REGS regs;

              regs.h.ah=0x45;         /*Release handle*/

              regs.x.dX=handle;

              int86(EMM,&regS,&regs);

              return (regs.h.ah==0);

          }

      该程序分析了扩充内存的基本操作。如下所示:

      ·检验扩充内存是否存在。

      ·然后看看扩充内存管理程序是否在正常工作。

      ·试着分配10页扩充内存。

      ·每次将一页映射到页框架中,并将一个测试字符串写进每一页中。

      ·将这些页映射回页框架中,然后读出并打印测试字符串。

      这些是扩充内存上的最基本操作。它们使你能在多个程序中利用内存区。可利用这

类技术将电子表格或数据库放进扩充内存:要作到这一点,就要在页框架中限定一个值数

组,并用它们的指针填满它们。记住:要用far指针或大指针来访问页,因为这个区域将超

出当前段。

 (未完待续)

原文地址:https://www.cnblogs.com/Chaobs/p/3838517.html