天天看點

UNIX 檔案系統基本操作

引言

UNIX® 中任何事物都是檔案 的觀點意味着,您将始終會與檔案和目錄打交道,無論您開發的是何種類型的應用程式。任何事物都存儲為檔案,從資料到配置檔案、甚至是裝置,在對 UNIX 程式設計經過幾個小時的學習之後,stdio.h 系統 Header 中的函數将能夠為您提供很好的幫助。

一個時常困擾 UNIX 程式設計新手的問題是,如何浏覽一個目錄,并對其中的檔案、目錄和符号連結進行相應的處理。如何能夠擷取它們的清單,以及如何能夠确定它們究竟是什麼?

請繼續閱讀本文,以學習如何使用 dirent.h 函數系列 (

opendir()/readdir()/closedir()

) 來讀取目錄中的條目,以及使用

stat()

函數來确定這些條目所對應的内容。

UNIX 檔案系統基本操作
UNIX 檔案系統基本操作
UNIX 檔案系統基本操作

開始之前

本文中的示例代碼(請參見下載下傳) 使用 C/C++ 開發工具 (CDT) 在 Eclipse 3.1 中編寫,readdir_demo 項目是一個托管的 Make 項目,該項目通過使用 CDT 程式生成規則建構。您在這個項目中找不到 Makefile,但是它們非常簡單,如果需要在 Eclipse 之外編譯這些代碼,您可以很容易地生成相應的 Makefile。

如果您還沒有嘗試使用 Eclipse,那麼您真的應該試一試。它是一個非常好的內建開發環境 (IDE),并且随着發行版本的不斷更新,它變得更加完善。它來自于生命力頑強的 EMACS 以及基于 Makefile 的開發工具。請參閱本文結尾處的參考資料部分,其中提供了一些很好的 Eclipse 文章的連結。

UNIX 檔案系統基本操作
UNIX 檔案系統基本操作

讀取目錄條目

對于一個給定路徑的目錄,應該如何讀取其中的條目呢?您無法像操作檔案那樣打開目錄(使用

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()

,如果您正在編寫線程代碼,可以使用這個函數作為替代。

UNIX 檔案系統基本操作
UNIX 檔案系統基本操作

struct dirent 中包含了哪些内容呢?

POSIX 1003.1 标準僅僅為

struct dirent

定義了一個必需的條目,即

char

數組

d_name

。這是用标準的以 NULL 結尾的字元串表示的該條目的名稱。這個結構中任何其他内容都是特定于您的 UNIX 系統的。

的确如此,

struct dirent

中其他所有内容 都是不可移植的。嚴格滿足一緻性的系統不應該在其中包含任何其他的内容。如果您編寫了使用額外結構成員的代碼,那麼您必須将其标記為不可移植的,并且包含一個完成相同任務的替換代碼路徑,如果您認為這樣做特别友好的話。

例如,許多 UNIX 包含一個

d_type

成員和一些附加常量,這樣一來,您無需額外的

stat()

調用就可以檢查目錄條目的類型。除了減少另外的系統調用之外,這種不可移植的擴充還減少了從檔案系統擷取更多中繼資料的開銷非常高的通路操作。衆所周知,在大多數 UNIX 上,

stat()

函數的執行速度非常慢。

UNIX 檔案系統基本操作
UNIX 檔案系統基本操作
UNIX 檔案系統基本操作

擷取檔案資訊

除了擷取目錄中條目的名稱之外,您可能還需要一些附加資訊,以确定下一步要進行的操作。至少,僅根據目錄條目的名稱,您無法辨識檔案條目。

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()

時,以及當您出于特定的目的而需要擷取符号連結本身的相關資訊,而不是直接與其連結的檔案打交道時,情況也是這樣的。

UNIX 檔案系統基本操作
UNIX 檔案系統基本操作
UNIX 檔案系統基本操作

将其組合在一起

既然已經學習了如何使用

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 bytes/n",
                    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 -> %s/n", pathName, targetName );
                } else {
                    printf( "/t%s -> (invalid symbolic link!)/n",
 pathName );
                }
            }
        } else {
            printf( "Error statting %s: %s/n", 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;
}
      

這就是整個程式。盡管包含了較多的檔案,但處理目錄條目并不是十分困難。

UNIX 檔案系統基本操作
UNIX 檔案系統基本操作

結束語

使用

readdir()

stat()

函數浏覽目錄中的條目并确定對其進行的額外處理,是非常簡單的,在您需要列舉目錄中的内容時,也可能會使用到這種處理方法。它是一種很實用的方法,但是對 于一些沒有經驗的 UNIX 開發人員來說,卻難以掌握。本文的目的是降低其難度,使得 UNIX 開發人員能夠充分利用這些有價值的函數。

繼續閱讀