UNIX 文件系統基本操作
本文示例源代碼或素材下載
引言
Unix® 中任何事物都是文件 的觀點意味著,您將始終會與文件和目錄打交道,無論您開發的是何種類型的應用程序。任何事物都存儲為文件,從數據到配置文件、甚至是設備,在對 UNIX 編程經過幾個小時的學習之后,stdio.h 系統 Header 中的函數將能夠為您提供很好的幫助。
一個時常困擾 UNIX 編程新手的問題是,如何瀏覽一個目錄,并對其中的文件、目錄和符號鏈接進行相應的處理。如何能夠獲取它們的列表,以及如何能夠確定它們究竟是什么?
請繼續閱讀本文,以學習如何使用 dirent.h 函數系列 (opendir()/readdir()/closedir()) 來讀取目錄中的條目,以及使用 stat() 函數來確定這些條目所對應的內容。
開始之前
本文中的示例代碼(請參見下載)使用 C/C++ 開發工具 (CDT) 在 Eclipse 3.1 中編寫,readdir_demo 項目是一個托管的 Make 項目,該項目通過使用 CDT 程序生成規則構建。您在這個項目中找不到 Makefile,但是它們非常簡單,如果需要在 Eclipse 之外編譯這些代碼,您可以很容易地生成相應的 Makefile。
如果您還沒有嘗試使用 Eclipse,那么您真的應該試一試。它是一個非常好的集成開發環境 (IDE),并且隨著發行版本的不斷更新,它變得更加完善。它來自于生命力頑強的 EMacS 以及基于 Makefile 的開發工具。請參閱本文結尾處的參考資料部分,其中提供了一些很好的 Eclipse 文章的鏈接。
讀取目錄條目
對于一個給定路徑的目錄,應該如何讀取其中的條目呢?您無法像操作文件那樣打開目錄(使用 open() 或 fopen() 函數),并且即便可以這樣做,所得到的數據可能是您正在使用的文件系統的專用格式,而對于不十分熟悉的程序員來說,直接訪問這些數據將使情況變得更糟。
dirent.h 函數,opendir()、readdir() 和 closedir(),它們正是您所需要的。這些函數的使用與用來對文件進行操作的 open/read/close 的習慣用法非常相似,但有一點除外:對于每個目錄條目,readdir() 函數一次返回一個指向特殊結構(struct dirent 類型)的指針。通常,對目錄進行瀏覽類似于清單 1 中所示的偽代碼。
清單 1. 讀取目錄中的內容
dir = opendir( "some/path/name" )entry = readdir( dir )while entry is not NULL:do_something_with( entry )entry = readdir( dir )closedir( dir )
在出現問題時,opendir() 和 readdir() 函數都會返回 NULL,并且將設置全局變量 errno 的值,以指出所出現的錯誤。如果 readdir() 返回 NULL,并且 errno 為 0(有時也稱為 EOK 或 ENOERROR),則表示沒有其他的目錄條目。
有一點需要注意,每個目錄都包含“.(對該目錄的引用)和“..(對該目錄的父目錄的引用)條目。根據您所進行的操作,可能需要忽略對這些條目的處理。
請注意,readdir() 不是線程安全的,因為所返回的結構是存儲在函數庫中的一個靜態變量。大多數現代的 Unix 系統都具有線程安全的 readdir_r(),如果您正在編寫線程代碼,可以使用這個函數作為替代。
struct dirent 中包含了哪些內容呢?
POSIX 1003.1 標準僅僅為 struct dirent 定義了一個必需的條目,即 char 數組 d_name。這是用標準的以 NULL 結尾的字符串表示的該條目的名稱。這個結構中任何其他內容都是特定于您的 UNIX 系統的。
的確如此,struct dirent 中其他所有內容 都是不可移植的。嚴格滿足一致性的系統不應該在其中包含任何其他的內容。如果您編寫了使用額外結構成員的代碼,那么您必須將其標記為不可移植的,并且包含一個完成相同任務的替換代碼路徑,如果您認為這樣做特別友好的話。
例如,許多 Unix 包含一個 d_type 成員和一些附加常量,這樣一來,您無需額外的 stat() 調用就可以檢查目錄條目的類型。除了減少另外的系統調用之外,這種不可移植的擴展還減少了從文件系統獲取更多元數據的開銷非常高的訪問操作。眾所周知,在大多數 UNIX 上,stat() 函數的執行速度非常慢。
獲取文件信息
除了獲取目錄中條目的名稱之外,您可能還需要一些附加信息,以確定下一步要進行的操作。至少,僅根據目錄條目的名稱,您無法辨別文件條目。
stat() 函數會將特定文件的相關信息填入 struct stat 結構中,如果您獲得的是文件描述符而不是文件名,那么作為替代,您可以使用 fstat() 函數。如果您想能夠檢測出符號鏈接,那么可以對文件名使用 lstat()。
與 readdir() 返回的 struct dirent 不同,struct stat 具有相當多的標準的、必需的成員:
st_mode——文件權限(用戶、其他用戶、組)和標志
st_ino——文件序列號
st_dev——文件設備號
st_nlink——文件連接計數
st_uid——所有者用戶 ID
st_gid——所有者組 ID
st_size——以字節表示的文件大小(針對普通文件)
st_atime——最后的訪問時間
st_mtime——最后的修改時間
st_ctime——文件的創建時間
對 st_mode 成員使用 S_*() 宏,這樣就可以找出您所處理的目錄條目的類型:
S_ISBLK(mode)——是否為塊特殊文件?(通常是某種基于塊的設備)
S_ISCHR(mode)——是否為字符特殊文件?(通常是某種基于字符的設備)
S_ISDIR(mode)——是否為目錄?
S_ISFIFO(mode)——是否為管道或 FIFO 特殊文件?
S_ISLNK(mode)——是否為符號鏈接?
S_ISREG(mode)——是否為普通文件?
眾所周知,在大多數文件系統上,stat() 函數的執行速度非常慢,所以如果您打算在將來再次使用該信息,可能需要對其進行緩存。
關于符號鏈接的說明
通常,您并不關心符號鏈接。如果對符號鏈接調用 stat(),那么您將獲取該鏈接所指向的文件的相關信息。這和用戶的體驗是一致的,因為控制與該文件交互的是目標文件的權限,而不是符號鏈接本身。
有些應用程序,如 ls 和備份程序,需要能夠顯示鏈接文件本身的相關信息,例如它所指向的文件。當您使用 lstat() 來代替 stat() 時,以及當您出于特定的目的而需要獲取符號鏈接本身的相關信息,而不是直接與其鏈接的文件打交道時,情況也是這樣的。
將其組合在一起
既然已經學習了如何使用 readdir() 和 stat() 來查找目錄中的條目,那么讓我們來看看演示這些函數的一些實際代碼。
這里所介紹的代碼將瀏覽命令行中指定的一個或多個目錄,并顯示在該目錄中找到的每個條目的相關信息。當它找到另一個目錄時,它會對該目錄進行同樣的處理。對于符號鏈接,將顯示其目標文件,并且還將顯示普通文件的大小。將忽略特殊文件。
如清單 2 所示,這個簡單的演示應用程序中包含了各種 Header 文件。程序的開始塊中包含了大多數程序中使用的標準部分,并且后面的四項是在該程序中使用 readdir() 和 stat() 所必需的。
清單 2. Header 和常量
#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <string.h>#include <limits.h>#include <sys/types.h>#include <sys/stat.h>#include <dirent.h>#include <unistd.h>
process_Directory() 函數(開始于清單 3,結束于清單 6)讀取了指定的目錄,并顯示了每個條目的相關信息。opendir() 返回的 DIR 指針與 fopen() 返回的 FILE 指針類似,它是一個用于跟蹤目錄流的操作系統特定的對象,您應該忽略其具體內容。
清單 3. 處理一個目錄
unsigned process_directory( char *theDir ){DIR *dir = NULL;struct dirent entry;struct dirent *entryPtr = NULL;int retval = 0;unsigned count = 0;char pathName[PATH_MAX + 1];/* Open the given directory, if you can. */ dir = opendir( theDir );if( dir == NULL ) {printf( "Error opening %s: %s", theDir, strerror( errno ) );return 0;}
在打開了指定的目錄之后,調用 readdir_r()(請參見清單 4)以獲取關于第一個條目的信息,隨后每次調用 readdir_r() 都將返回下一個條目,直到到達了目錄末尾,并且 entryPtr 被設置為 NULL。這里還使用了 strncmp() 來檢查“.和“..條目,以便略過它們。如果不略過它們,您將永遠都在處理類似“theDir/./././././././././.等這樣的目錄。
清單 4. 讀取一個目錄條目
retval = readdir_r( dir, &entry, &entryPtr );while( entryPtr != NULL ) {struct stat entryInfo;if( ( strncmp( entry.d_name, ".", PATH_MAX ) == 0 ) ||( strncmp( entry.d_name, "..", PATH_MAX ) == 0 ) ) {/* Short-circuit the . and .. entrIEs. */retval = readdir_r( dir, &entry, &entryPtr );continue;}
既然已經得到了目錄的條目名稱,那么您需要構造一個更加完整的路徑(請參見清單 5),然后調用 lstat() 以獲取該條目的相關信息。因為符號鏈接需要特殊的處理,所以這里使用了 lstat() 函數。您可以使用 readlink() 函數找到其目標文件。
如果該條目是一個目錄,那么對這個目錄遞歸地調用 process_Directory(),并將其中所找到的條目數加到運行總數中。如果該條目是一個文件,那么顯示其名稱和字節數(可在 struct stat 的 st_size 成員中找到)。
清單 5. 處理條目
(void)strncpy( pathName, theDir, PATH_MAX );(void)strncat( pathName, "/", PATH_MAX );(void)strncat( pathName, entry.d_name, PATH_MAX );if( lstat( pathName, &entryInfo ) == 0 ) {/* stat() succeeded, let's party */count++;if( S_ISDIR( entryInfo.st_mode ) ) {/* directory */printf( "processing %s/n", pathName );count += process_directory( pathName );} else if( S_ISREG( entryInfo.st_mode ) ) {/* regular file */printf( "t%s has %lld bytesn",pathName, (long long)entryInfo.st_size );} else if( S_ISLNK( entryInfo.st_mode ) ) {/* symbolic link */char targetName[PATH_MAX + 1];if( readlink( pathName, targetName, PATH_MAX ) != -1 ) {printf( "t%s -> %sn", pathName, targetName );} else {printf( "t%s -> (invalid symbolic link!)n",pathName );}}} else {printf( "Error statting %s: %sn", pathName, strerror(errno ) );}
在 while 循環的底部,讀取另一個目錄條目并對其進行處理。如果您完成了對目錄條目的處理,那么關閉當前打開的目錄,并返回經過處理的條目的數目。
清單 6. 讀取另一個條目
retval = readdir_r( dir, &entry, &entryPtr );}/* Close the Directory and return the number of entrIEs. */(void)closedir( dir );return count;}
最后,清單 7 顯示了該程序的 main() 函數,它只是對命令行中傳遞的每個參數調用了 process_directory() 函數。一個真正的程序應該具有使用方法消息,并且在用戶沒有指定任何參數時,提供某種形式的反饋信息,但我把這項內容作為練習留給讀者。
清單 7. 主線
/* readdir_demo main()** Run through the specified directories, and pass them* to process_directory().*/int main( int argc, char **argv ){int idx = 0;unsigned count = 0;for( idx = 1; idx < argc; idx++ ) {count += process_directory( argv[idx] );}return EXIT_SUCCESS;}
這就是整個程序。盡管包含了較多的文件,但處理目錄條目并不是十分困難。
結束語
使用 readdir() 和 stat() 函數瀏覽目錄中的條目并確定對其進行的額外處理,是非常簡單的,在您需要列舉目錄中的內容時,也可能會使用到這種處理方法。它是一種很實用的方法,但是對于一些沒有經驗的 Unix 開發人員來說,卻難以掌握。本文的目的是降低其難度,使得 UNIX 開發人員能夠充分利用這些有價值的函數。
