第8章磁 盤
學習編程語言,經常是從基本的輸入和輸出入手的(正如第五、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的規則,因此這些磁盤能被任意地閱讀,這就使許多人相信新版本會在引導
扇區的第3、四和第五字節中尋找「魔術字頭」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的各個類型。
1、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頁
三、若是原來的簇號(在這種狀況下是2)是偶數;就獲取該字的低12位;不然就獲取
高12位。
2、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對策之間的惟一差
異就是如何得到更大磁盤容量的方法。
3、更多的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藉助整個的簇來分配文件空間。因而無論文件實際有多
大,一個文件的最小磁盤應用單位就是一個簇。一個字節的文件可能佔用5十二、102四、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<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(®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<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,®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章做好了準備。
第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),本章要相對詳細地討論目錄項中的其它各域。
1、文件名(偏移值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版中,會給出信息明確地告訴你整個目錄將被
刪除。
2、文件擴展名(偏移值08h)
偏移值08h處開始的3個字符是文件類型或擴展名(也做爲ASCII字符來保存)。如
果文件沒有擴展名,或者擴展名少於3個字符,那麼該擴展名就在域中向左對齊而且在名
字中插入一些空格。注意在文件名中的句號,它一般用做根目錄和擴展名之間的界線,它
不與目錄項中的文件名一塊兒保存。 DOS假定目錄項的第8和第9字節之間存在一個字句
號。
3、文件屬性(偏移值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中才可在大多數磁盤中發揮做用時檢查其標記。卷標
記將在本章後面更詳細地介紹。
子目錄位表示該文件是一個包含其它目錄項的特殊文件。子目錄是從屬於當前目錄
的目錄(目錄和子目錄將在本章後面詳細討論)。
歸檔位(文件的狀態位)在文件更新時設置。特別地,硬盤備份程序用該位來指示哪一個
文件須要備份。
4、上次更新的時間(偏移值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秒的增量
5、上次更新的日期(偏移值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)。
6、起始磁盤簇號(偏移值1Ah)
偏移值1Ah處開始的字(2個字節)是文件的起始磁盤簇號,該字的低字節保存在前
面。
該字只給出了文件的起始點。要分配第二個簇和後面的簇,該域的值也用來計算與文
件分配表(FAT)的偏移值。經過FAT,文件所使用的任何其它簇號也都能定位(見第8章
有關FAT的詳細討論)。
7、文件大小(偏移值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:\BIN\LOTUS\INSTALL\AUTOEXEC.BAT
DEPTH:FOUND:\SYS\AUTOEXEC.DV
DEPTH:FOUND:\SYS\AUTOEXEC.OLD
DEPTH:FOUND:\SYS\AUTOEXEC.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\n");
printf("Cannot run this program\n");
exit(0);
}
/*Is the expanded memory manager functional?*/
if(!emmok()){
printf("Expanded memory manager NOT available\n");
printf("cannOt run this program\n");
exit(0);
}
/*Get ten pages of expanded memory for the demo.*/
if((emmhandle=emmalloc(10))<0){
printf("There are not enough pages available\n");
printf("cannot run this program\n");
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\n",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(®s,®s,&sregs);
handle=regS.x.ax;/*File handle*/
if(result=(regs.x.cflag==0)){
regs.h.ah=0x3e;
regs.x.bx=handle;
intdos(®s,®s);
}
return (result);
}
int emmok()
{
union REGS regs;
regs.h.ah=0x46;/*Get manager status*/
int86(EMM,®S,®s);
if(regs.h.ah!=0)return (FALSE);
regs.h.ah=0x41;/*Get page frame segment*/
int86(EMM,®S,®s);
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,®s,®s);
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指針或大指針來訪問頁,由於這個區域將超
出當前段。
(未完待續)