Linux 檔案訪問原語(轉)

ba發表於2007-08-11
Linux 檔案訪問原語(轉)[@more@]經授權自 1999 年春季號 Linux 雜誌重印。Linux 雜誌版權所有,由 Infostrata Communications 出版。
POSIX API 最重要的一個抽象概念就是檔案。儘管幾乎所有的作業系統都將檔案用於永久性儲存器,但所有 Unix 版本透過檔案抽象概念提供對大多數系統資源的訪問。

更具體地說,這意味著 Linux 使用相同的一組系統呼叫來提供對裝置(例如軟盤和磁帶裝置)、網路資源(最常見的是 TCP/IP 連線)、系統終端,甚至核心狀態資訊的訪問。感謝無所不在的系統呼叫,嫻熟地使用與檔案相關的呼叫對於每個 Linux 程式設計師來說都很重要。讓我們仔細檢視一下檔案 API 背後的一些基本概念,並描述最重要的檔案相關係統呼叫。

Linux 提供許多不同種類的檔案。最常見的型別就簡稱為常規檔案,它儲存大量用於以後訪問的資訊。您所使用的絕大部分檔案 -- 例如可執行檔案(如 /bin/vi)、資料檔案(如 /etc/passwd)和系統二進位制檔案(如 /lib/libc.so.6)-- 都是常規檔案。它們通常駐留在磁碟上的某處,但我們稍後會發現,並不一定都是這種情況。

另一種檔案型別是目錄,它包含了一個其它檔案及其位置的列表。使用 ls 命令列出目錄中的檔案時,它開啟該目錄的檔案,並列印出它所包含的所有檔案的資訊。

其它檔案型別包括塊裝置(表示檔案系統快取記憶體的裝置,例如硬碟驅動器)、字元裝置(表示非快取記憶體的裝置,例如磁帶驅動器、滑鼠和系統終端)、管道和套接字(允許程式相互之間對話),以及符號連結(允許檔案在目錄層次結構中有多個名稱)。

大多數檔案都有一個或多個引用它們的符號名。這些符號名是一組由 / 字元定界的字串,並向核心標識檔案。它們是 Linux 使用者所熟悉的路徑名;例如,路徑名 /home/ewt/article 引用的是我手提電腦中包含這篇文章文字的檔案。沒有兩個檔案可以共享相同的名稱(但單一檔案可以有多個名稱),因此路徑名唯一地標識單一檔案。

程式可以訪問的每個檔案都由一個小的非負整數標識,稱為“檔案描述符”。檔案描述符由開啟檔案的系統呼叫建立,並由從當前程式建立的新子程式繼承。就是說,當程式啟動了一個新程式時,原始程式的開啟檔案通常是由新程式繼承的。

按照約定,大多數程式保留前三個檔案描述符(0、1 和 2)用於特殊目的 -- 訪問所謂的標準輸出、標準輸出和標準錯誤流。檔案描述符 0 是標準輸入,這裡許多程式都將從外部世界接收輸入。檔案描述符 1 是標準輸出。大多數程式在這裡顯示正常的輸出。對於與錯誤情況相關的輸出,使用檔案描述符 2(標準錯誤)。

任何習慣使用 Linux shell 的人都曾看到過標準輸入、輸出和錯誤檔案描述符的使用。通常,shell 執行命令時帶檔案描述符 0、1 和 2,都是指 shell 的終端。當使用 > 字元指示 shell 將一個程式的輸出傳送給另一個程式時,shell 在呼叫新程式之前開啟該檔案作為檔案描述符 1。這將導致程式將它的輸出傳送給指定的檔案而不是使用者終端;其妙處是,對於程式本身,這是透明的!

與之類似," 而不是 > 將標準錯誤重定向)。這種型別的檔案重定向是 Linux 命令列最強大的特性之一。

使用任何與檔案相關的系統呼叫之前,程式應該包括 和 ;它們為最普遍的檔案例程提供了函式原型和常數。在下面的示例程式碼中,我們假設每個程式開始處都有



#include
#include


首先,讓我們瞭解如何讀寫檔案。憑直覺就可以知道,read() 和 write() 系統呼叫是執行這些操作的最常用方法。這兩種系統呼叫將有三個自變數:要訪問的檔案描述符、指向要讀寫的資訊的指標以及應該讀寫的字元數。返回成功讀寫的字元數。清單 1 說明了一個簡單的程式,它從標準輸入(檔案描述符 0)中讀取一行,並將它寫入標準輸出(檔案描述符 1):


清單 1:

void main(void) {
char buf[100];
int num;

num = read(0, buf, sizeof(buf));
write(1, "I got: ", 7); /* Length of "I got: " is 7! */
write(1, buf, num);
}



關於這個處理有兩個值得注意的問題。首先,我們要求 read() 返回 100 個字元,但如果我們執行這個程式,只有在使用者按下了 "enter" 鍵以後才能獲得輸入。許多檔案操作都根據最佳效果工作:它們嘗試返回程式要求的所有資訊,但只有部分能夠成功。預設情況下,終端配置成一旦存在 " " 或新行符(透過按 "enter" 鍵產生)時,就從 read() 呼叫返回。這實際上非常方便,因為大多數使用者都希望程式無論如何都是面向行的。但常規資料檔案並非如此,如果依靠它就可能產生不可預料的結果。

另一個要注意的問題是我們不必在顯示輸出後寫一個 。read() 呼叫給了我們來自使用者的 ,只將那個 透過 write() 寫回標準輸出。如果您希望在沒有新行符的情況下看到發生的事件,嘗試將最後一行改為



write(1, buf, num - 1);


有關這個簡單示例的最後一點:buf 絕對不包含實際的 C 字串。C 字串由標記字串結束的單一 字元終止。因為 read() 不將 新增到緩衝區的結尾,在 read() 上使用 strlen()(或任何其它 C 字串函式)將可能鑄成大錯!這種行為可以讓 read() 和 write() 對包括 字元的資料處理,而這對於一般字串函式來說是不可能的。

read() 和 write() 系統呼叫可以對絕大多數檔案起作用。但它們不對目錄起作用,目錄應該透過特殊函式(例如 readdir())來訪問。另外,read() 和 write() 對於某些型別的套接字也不起作用。

某些檔案,例如常規檔案和塊裝置檔案,使用檔案指標的概念。它指定在檔案中,下一個 read() 呼叫從哪裡讀取,下一個 write() 呼叫從哪裡寫入。read() 或 write() 後,檔案指標隨著已處理的字元數(在內部,透過核心)增加。這樣,使用單一迴圈就可以方便地讀取檔案中的所有資料。清單 2 就是示例:


清單 2:

char buffer[1024];

while ((num = read(0, buffer, 1024))) {
printf("got some data ");
}



這個迴圈將讀取標準輸入上的所有資料,自動在每次讀取後增加核心的內部檔案指標。當檔案指標處於檔案結尾時,read() 將返回 0 並退出迴圈。某些檔案(例如字元裝置 -- 終端就是很好的一例)本身沒有檔案指標,所以對於這一點,該程式將繼續執行,直到使用者提供檔案結束標記(透過按 "Ctrl-D")為止。

到現在為止,我們已經知道如何讀寫檔案了,下一步要學習如何開啟一個新檔案。開啟不同型別的檔案有不同方法;我們將在這裡討論的方法是透過路徑名開啟在檔案系統中表示的檔案;包括常規檔案、目錄、裝置檔案和指定的管道。某些套接字檔案有路徑名,那些必須透過替代方法開啟。

撇開放棄權利的,open() 系統呼叫可以讓程式訪問大多數系統檔案。open() 是個不尋常的系統呼叫,因為它獲取兩個或者三個自變數:



int open(const char *
pathname,
int flags);


或者,



int open(const char *
pathname,
int flags,
int perm);


第一種形式更普遍一些;它開啟一個已存在的檔案。第二種格式應該在需要建立檔案時使用。第三個自變數指定應該給予新檔案的訪問許可權。

open() 的第一個引數是以正常 C 字串表示的全路徑名(即以 終止)。第二個引數指定檔案應該如何開啟,幷包括邏輯“與”操作的一個或多個以下標誌:

O_RDONLY:檔案可以只讀

O_RDWR:檔案可以讀寫

O_APPEND:檔案可以讀或附加

O_CREAT:如果檔案還不存在則應該建立

O_EXCL:如果檔案已存在,失敗而不是建立它(只應該使用 O_CREAT)

O_TRUNC:如果檔案已存在,從中除去所有資料(與建立新檔案類似)

open() 的第三個引數只在使用 O_CREAT 時需要;它指定了以數字表示的檔案許可權(格式與 chown 命令的數值許可權自變數的格式相同。為 open() 指定的許可權受使用者的 umask 影響,後者允許使用者指定一系列新檔案應該獲得的預設許可權。大多數建立檔案的程式都使用第三個自變數 0666 呼叫 open(),可以讓使用者透過 umask 來控制程式的預設許可權。(大多數 shell 的 umask 命令都可以更改它。)

例如,清單 3 顯示瞭如何為進行讀寫開啟檔案、如果它不存在則建立,以及廢棄其中的資料:


清單 3:

int fd;
fd = open("myfile", O_RDWR | O_CREAT | O_TRUNC, 0666)
if (fd < 0) {
/* Some error occurred */
/* ... */
}




open() 返回引用檔案的檔案描述符。回憶一下,檔案描述符總是 >= 0。如果 open() 返回了一個負值,就表示發生了錯誤,全域性變數錯誤號包含了描述問題的 Unix 錯誤程式碼。open() 總儘量返回最小數,如果沒有使用檔案描述符 0,open() 將總返回 0。

程式帶檔案結束時,它應該透過 close() 系統呼叫關閉它,該系統呼叫的格式為:



int close(int fd);


close 的檔案描述符是傳遞給 close() 的唯一自變數,在成功情況下返回 0。儘管 close() 失敗的情況比較少見,但如果檔案描述符引用的是遠端伺服器上的檔案,系統無法正確清空它的快取記憶體,close() 就可能真的失敗。程式終止時,核心自動關閉所有還在開啟的檔案。

最後的一個常見檔案操作是移動檔案指標。這(自然)只對帶檔案指標的檔案有意義,如果嘗試在不恰當的檔案上嘗試該操作就會返回錯誤。lseek() 系統呼叫用於以下目的:



off_t lseek(int fd, off_t pos, int whence);


off_t 型別是表達 longint (long 就是 lseek 中 "l" 的來歷)的一種別緻方法。lseek() 返回相對於檔案開始處檔案指標的最終位置,如果有錯誤,則返回 -1。這個系統呼叫希望被移動的檔案指標所屬的檔案描述符作為第一個自變數,將它移動到檔案中的位置作為第二個自變數。最後一個自變數描述檔案指標的移動方式。

SEEK_SET 將它移動到從檔案開始算起的 pos 位元組。

SEEK_END 將它移動到從檔案結尾算起的 pos 位元組。

SEEK_CUR 從它當前位置開始向檔案結尾移動 pos 位元組。

open()、close()、write()、read() 和 lseek() 的組合為 Linux 提供了基本的檔案訪問 API。雖然還有許多其它操縱檔案的函式,但這裡描述的是最常用的。

大多數程式設計師都使用熟悉的 ANSI C 庫檔案函式,例如 fopen() 和 fread(),而不是在此描述的低階系統呼叫。可以預見到,fopen() 和 fread() 是在使用者級別庫中這些系統呼叫的基礎上實現的。仍然會經常看到低階系統呼叫的使用,特別是在更復雜的程式中。透過熟悉這些例程和介面,您就可以成為一個真正的 Unix 駭客了。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-947708/,如需轉載,請註明出處,否則將追究法律責任。

相關文章