1. 程式人生 > >UNIX環境高階程式設計學習之路(二)----檔案和目錄

UNIX環境高階程式設計學習之路(二)----檔案和目錄

對於UNIX環境程式設計,工作中經常會用到相關知識,作為學習UNIX環境程式設計的經典書籍--UNIX環境高階程式設計,是每個UNIX程式設計人員必看的經典書籍之一,為了將相關知識重新進行學習,以系統的整合所學知識,遂以博文形式作為總結。

一、概述

本章將描述檔案系統的其他特徵和檔案的性質。將從stat函式開始,諸葛說明stat結構的每一個成員以瞭解檔案的所有屬性。在此過程中我們將說明修改這些屬性的各個函式(更改所有者、更改許可權等),還將更詳細的檢視UNIX檔案系統的結構以及符號連結。

二、stat、fstat和lstat函式

#include<sys/stat.h>
int stat(const char *restrict pathname,struct stat *restrict buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *restrict pathname,struct stat *restrict buf);
三個函式的返回值:若成功則返回0,若出錯則返回-1
    一旦給出pathname,stat函式就返回與此命名檔案相關的資訊結構。fstat函式獲取已在filedes上開啟的有關資訊。lstat函式類似於stat,但是當命名的檔案是一個符號連結時,lstat返回該符號連結的有關資訊,而不是由符號連結引用檔案的資訊。     第二個引數是buf指標,它指向一個我們必須提供的結構。這些函式填寫由buf指向的結構。該結構的實際定義可能隨實現有所不同,但其基本形式是: struct stat {     mode_t st_mode;     /* file type & mode (permissions) */
    ino_t      st_ino;        /* i-node number (serial number) */
    dev_t     st_dev;       /* device number (file system) */     dev_t     st_rdev;      /* device number for special files */     nlink_t    st_nlink;     /* number of links */
    uid_t      st_uid;        /* user ID of owner */
    gid_t      st_gid;        /* group ID of group */
    off_t       st_size;      /* size in bytes, for regular files */
    time_t    st_atime;    /* time of access */
    time_t    st_mtime;   /* time of last modification */
    time_t    st_ctmie;    /* time of last file status change */
    blksize_t st_blksize; /* best I/O block size */
    blkcnt    st_blocks;   /*number of disk blocks allocated */
}; 使用stat函式最多的是ls -l命令,用其可以獲得有關一個檔案的所有資訊。 三、檔案型別 至今已經介紹了兩種不同的檔案型別---普通檔案和目錄。UNIX系統的大多數檔案是普通檔案或目錄,但是也有另外一些檔案型別。檔案型別包括如下: (1)普通檔案(regular file)這種檔案包含了某種形式的資料。至於這種資料是文字還是二進位制資料對於UNIX核心而言並無區別。對普通檔案內容的解釋由處理該檔案的應用程式進行。 (2)目錄檔案(directory file)這種檔案包含了其他檔案的名字以及指向與這些檔案有關資訊的指標。對一個目錄檔案具有讀許可權的任一程序都可以讀該目錄的內容,但只有核心可以直接寫目錄檔案。 (3)塊特殊檔案(block special file)這種檔案型別提供對裝置(例如磁碟)帶緩衝的訪問,每次訪問以固定長度為單位來進行。 (4)字元特殊檔案(character special file)這種檔案型別提供對裝置不帶緩衝的訪問,每次訪問長度可變。系統中的所有裝置要麼是字元特殊檔案,要麼是塊特殊檔案。 (5)FIFO,這種型別檔案用於程序間通訊,有時也將其稱為命名管道(named pipe) (6)套接字(socket)這種檔案型別用於程序間的網路通訊。套接字也可以用於在一臺宿主機上程序間的非網路通訊。 (7)符號連結(symbolic link)這種檔案型別指向另一個檔案。     檔案型別資訊包含在st_mode成員,可以用下表的巨集確定檔案型別。

POSIX.1允許實現將程序間通訊(IPC)物件(例如,訊息佇列和訊號量等)表示為檔案。

以下程式碼為:對每一個命令列引數列印檔案型別
#include <stdio.h> //for printf
#include <stdlib.h> //for exit
#include <sys/stat.h> //for S_ISREG()/S_ISDIR() and so on
int main(int argc,char *argv[])
{
    char *ptr;
    struct stat buf;
    int i;
    
    for(i=1; i<argc; i++)
    {
        printf("argv[%d]:%s: ",i,argv[i]);    
        if(lstat(argv[i],&buf) < 0)
        {
            perror("lstat error");
            continue;    
        }
        if(S_ISREG(buf.st_mode))
         ptr = "regular";
        else if(S_ISDIR(buf.st_mode))
         ptr = "directory";
        else if(S_ISCHR(buf.st_mode))
         ptr = "character special";
        else if(S_ISBLK(buf.st_mode))
         ptr = "block special";
        else if(S_ISFIFO(buf.st_mode))
         ptr = "fifo";
        else if(S_ISLNK(buf.st_mode))
         ptr = "symbolic link";
        else if(S_ISSOCK(buf.st_mode))
         ptr = "socket";
        else
            ptr = "***unknown mode***";
        
        printf("%s\n",ptr);
    }
    exit(0);
}
程式事例輸出:./a.out /etc/passwd /etc /dev/initctl /dev/log /dev/tty /dev/scsi/host0/bus0/target0/lun0/cd /dev/cdrom /etc/passwd:regular /etc:directory /dev/initctl:fifo /dev/log:socket /dev/tty:character special /dev/scsi/host0/bus0/target0/lun0/cd:block special
/dev/cdrom:symbolic link 四、設定使用者ID和設定組ID 與一個程序相關的ID有6個或者更多,如下表:

* 實際使用者ID和實際組ID標識我們究竟是誰。這兩個欄位在登入時取自口令檔案中的登陸項。通常,在一個登入會話間這些值並不改變,但是超級使用者程序有方法改變它們; * 有效使用者ID,有效阻ID以及附加組ID決定了我們的檔案訪問許可權; * 儲存的設定使用者ID和儲存的設定組ID在執行一個程式時包含了有效使用者ID和有效阻ID的副本。     通常,有效使用者ID等於實際使用者ID,有效組ID等於實際組ID。 每個檔案都有一個所有者和組所有者,所有者和組所有者分別用st_uid成員和st_gid成員來表示。      當執行一個程式檔案時,程序的有效使用者ID通常就是實際使用者ID,有效組ID通常就是實際組ID。但是可以在檔案模式字(st_mode)中設定一個特殊標誌,其含義是“當執行此檔案時,將程序的有效使用者ID設定為檔案所有者的使用者ID(st_uid)”。與此相類似,在檔案模式字中可以設定另外一位,它使得將執行此檔案的程序的有效組ID設定為檔案的組所有者ID(st_gid)。在檔案模式字中的這兩位被稱為設定使用者ID(set-user-ID)位和設定組ID(set-group-ID)位。     例如,若檔案所有者是超級使用者,並且設定了該檔案的設定使用者ID位,然後當該程式由一個程序執行時,則改程序就有超級使用者特權。不管執行此檔案的程序的實際使用者ID是什麼,都進行這種處理。 關於實際使用者ID,有效使用者ID,檔案所有者,有如下通俗的解釋: 實際使用者ID:有的文章中將其稱為真實使用者ID,這個ID就是我們登陸unix系統時的身份ID。 有效使用者ID:定義了操作者的許可權。有效使用者ID是程序的屬性,決定了該程序對檔案的訪問許可權。 檔案的訪問許可權包括讀寫和執行。判斷某個程序對檔案有何許可權時,核心會將非超級使用者程序的有效ID與檔案的所有者ID進行比較,當然,也可能需要比較有效組ID,這關係到具體的許可權測試方法,先不在這裡說明。而超級使用者建立的程序是允許訪問整個檔案系統的。它的有效ID等於0。不過,這裡還有一點需要說明的是,僅僅有合適的有效ID,還不一定就能獲得所有或者部分許可權。你需要得到被訪問檔案的允許,這就是檔案訪問許可權位(使用者讀、使用者寫、組讀等)的責任了。 檔案的所有者ID建立檔案是由某使用者的程序實現的吧?所以在建立新檔案的時候,就將該程序的有效ID作為該檔案的所有者ID了。APUE裡面有時又將檔案的所有者ID稱為“檔案的使用者ID”。也就是該檔案是由哪個使用者建立的,可用通過ls -l來檢視到。 五、檔案訪問許可權 st_mode值也包括了針對檔案的訪問許可權位,當提及檔案時,指的是前面提到的任何型別的檔案。所有檔案型別(目錄、字元特別檔案等)都有訪問許可權。

* 我們用名字開啟任一型別的檔案時,對該名字中包含的每一個目錄,包括它可能隱藏的當前工作目錄都應具有執行許可權。所以,對應目錄的執行權位常被稱為搜尋位。     例如,開啟檔案/usr/include/stdio.h,需要對/、/usr和/usr/include具有執行許可權。然後,需要具有對該檔案本身的適當許可權,這取決於以何種模式開啟它(只讀、讀寫等)。     如果當前目錄是/usr/include,那麼為了開啟檔案stdio.h,需要對該工作目錄的執行許可權。這是隱含當前工作目錄的一個例項,開啟stdio.h檔案與開啟./stdio.h作用相同。     注意,對於目錄的讀許可權和執行許可權是不同的。讀許可權允許我們讀目錄,獲得在該目錄中所有檔案的列表。當一個目錄是我們要訪問的路徑名的衣蛾組成部分時,對該目錄的執行許可權使我們可以通過該目錄。 * 為了在open函式中對一個檔案制定O_TRUNC標誌,必須對該檔案有寫許可權。 * 為了在一個目錄中建立一個新檔案,必須對該目錄具有寫許可權和執行許可權。 * 為了刪除一個現有檔案,必須對包含該檔案的目錄具有寫許可權和執行許可權。對該檔案本身則不需要有讀、寫許可權。 如果用6個exec函式中的任何一個執行某個檔案,都必須對該檔案具有執行許可權。該檔案還必須是一個普通檔案。
六、新檔案和目錄的所有權 新檔案的使用者ID設定為程序的有效使用者ID。關於組ID,POSIX.1允許實現選擇下列之一作為新檔案的組ID。 (1)新檔案的組ID可以是程序的有效組ID。 (2)新檔案的組ID可以是他所在的目錄的組ID。     使用POSIX.1所允許的第二個選項(繼承目錄的組ID)使得在某個目錄下建立的檔案和目錄都具有該目錄的組ID。於是檔案和目錄的組所有權從該點向下傳遞
七、access函式 當用open函式開啟一個檔案時,核心以程序的有效使用者ID和有效組ID為基礎執行其訪問許可權測試。有時,程序也希望按其實際使用者ID和實際組ID來測試其訪問能力。access函式是按實際使用者ID和實際組ID進行訪問許可權測試的。
#include<unistd.h>
int access(const char *pathname, int mode);
返回值:若成功則返回0,瑞出錯則返回-1;
其中,mode是下表所列常量的按位或。

如下測試程式顯示了access函式的使用方法。
#include <stdio.h> //for printf
#include <stdlib.h> //for exit
#include <unistd.h> //for access
#include <fcntl.h> //for open
int main(int argc,char *argv[])
{
    if(argc != 2)
    {
        printf("usage:./a.out ");
        exit(1);    
    }
    if(access(argv[1],R_OK) < 0)
    {
        printf("access error for %s\n",argv[1]);
        exit(1);    
    }
    else
    {
        printf("read access OK\n");    
    }
    if(open(argv[1],O_RDONLY) < 0)
    {
        printf("open error for %s\n",argv[1]);
        exit(1);    
    }
    else
    {
        printf("open for reading OK\n");    
    }
    exit(0);
}
設定使用者ID程式可以確定實際使用者不能讀某個指定檔案,而open函式卻能開啟該檔案。

八、umask函式

umask函式為程序設定檔案模式建立遮蔽字,並返回以前的值。

#include<sys/stat.h>
mode_t umask(mode_t cmask);
返回值:以前的檔案模式建立遮蔽字,該函式是沒有出錯返回的函式。
其中,引數cmask是9個訪問許可權位常量(S_IRUSR、S_IWUSR)中的若干個按位“或”構成的。在程序建立一個 新檔案或者新目錄時,就一定會使用檔案模式建立遮蔽字。對弈任何在檔案模式建立遮蔽字中為1 的位,在檔案mode中的相應位則一定關閉。 如下程式建立了兩個檔案,建立第一個時,umask值為0,建立第二個時,umask值禁止所有組或其他使用者的訪問許可權。
#include "apue.h"
#include<fcntl.h>
#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IROTH)
int main(void)
{
        mode_t mask=umask(0);
        if(creat("foo",RWRWRW)<0)
           err_sys("create error for foo");
        umask(S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
        if(creat("bar",RWRWRW)<0)
           err_sys("creat error for bar");
        exit(0);
}
執行結構為:

UNIX系統的大多數使用者從不處理他們的umask值。通常在登入時,由shell的啟動檔案設定一次,然後從不改變。儘管如此,當編寫建立新檔案的程式時,如果我們想確保指定的訪問許可權位已經啟用,那麼必須在程序執行時修改umask值。例如,如果我們想確保任何使用者都能讀檔案,則應將umask設定為0。否則,當我們的程序執行時,有效的umask值可能關閉該許可權位。 更改程序的檔案模式建立遮蔽字並不影響其父程序(通常是shell)的遮蔽字。所有shell都有內建的umask命令,我們可以用命令設定或列印當前檔案模式建立遮蔽字。     使用者可以設定umask值以控制他們所創檔案的預設許可權。該值表示成八進位制數,一位代表一種要遮蔽的許可權,設定了相應位後,塔索對應的許可權就會被拒絕。如: 002:阻止其他使用者寫你的檔案 022:阻止同組成員和其他使用者寫你的檔案 027:阻止同組成員寫你的檔案以及其他使用者讀、寫或者執行你的檔案。
Single UNIX Specification要求shell支援符號形式的umask命令。與八進位制格式不同,符號格式指定許可的許可權而非拒絕的許可權。下面比較了兩種格式的命令。


九、chmod、fchmod函式

這兩個函式使我們可以更改現有檔案的訪問許可權

#include<sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int filedes, mode_t mode);
返回值:若成功則返回0,若出錯則返回-1。
為了改變一個檔案的許可權位,程序的有效使用者ID必須等於檔案的所有者ID,或者該程序必須具有超級使用者許可權。      引數mode是如下變所示常量的某種按位或運算構成的。

如下程式修改foo檔案和bar檔案模式

#include "apue.h"
int 
main(void)
{
        struct stat     statbuf;
        /* turn on set-group-ID and turn off group-execute */
        if(stat("foo", &statbuf) < 0)
                err_sys("stat error for foo");
        if(chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
                err_sys("chmod error for foo");
        /* set absolute mode to "rw-r--r--" */
        if(chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0)
                err_sys("chmod error for bar");
        exit(0);
}
statbuf.st_mode & ~S_IXGRP 是把組執行許可權設定為0,其它許可權不變。 程式執行後,這兩個檔案的最終狀態為:

chmod函式更新的只是i節點最近一次被更改的時間,系統預設,ls -l列出的是最後檔案內容的時間。

十、粘住位

S_ISVTX位被稱為粘住位(sticky bit)。如果一個可執行程式檔案的這一位被設定了,那麼在該程式第一次被執行並結束時,其程式正文部分的一個副本仍被儲存在交換區。這使得下次執行該程式時能較快的將其裝入記憶體區。     如今的系統擴充套件了粘住位的使用範圍,Single UNIX Specification允許針對目錄設定粘住位。如果對一個目錄設定了粘住位,怎只有對該目錄具有寫許可權的使用者在滿足下列條件之一的情況下,才能刪除該目錄下的檔案:
 * 擁有此檔案 * 擁有此目錄  *是超級使用者 目錄/tmp和/var/spool/uucppublic是設定粘住位的典型候選者——任何使用者都可在這兩個目錄中建立檔案。任一使用者(使用者、組和其他)對這兩個目錄的許可權通常都是讀、寫和執行。但是使用者不應能刪除或更名屬於其他人的檔案,為此在這兩個目錄的檔案模式中都設定了粘住位。
十一、chown、fchown和lchown函式

下面幾個chown函式可用於更改問價 的使用者ID和組ID

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int filedes, uid_t owner, gid_t group);
int lchown(const char *pathname , uid_t owner, gid_t group);
三個函式的返回值:若成功則返回0,若出錯則返回-1
除了所引用的檔案時符號連結以外,這三個函式的操作相似。在符號連結的情況下,lchown更改符號連結本身的所有者,而不是符號連結所指向的檔案。     如若兩個引數owner過group中的任意一個是-1,則對應的ID不變。 若_POSIX_CHOWN_RESTRICTED對指定的檔案起作用,則 (1)只有超級使用者程序能更改該檔案的使用者ID。 (2)若滿足下列條件,一個非超級使用者程序就可以更改該檔案的組ID:         (a)程序擁有此檔案(其有效使用者ID等於該檔案的使用者ID)。         (b)引數owner等於-1或檔案的使用者ID,並且引數group等於程序的有效組ID或程序的附屬組ID之一。     這意味著,當_POSIX_CHOWN_RESTRICTED其作用時,不能更改其他使用者檔案的使用者ID。你可以更改你所擁有的檔案的組ID,但只能改到你所屬的組。     如果這些函式由非超級使用者程序呼叫,則在成功返回時,該檔案的設定使用者ID位和設定組ID位都會被清除。 十二、檔案長度 stat結構成員st_size表示以位元組為單位的檔案長度,此欄位只對普通檔案、目錄檔案和符號連結有意義。 對於普通檔案,其檔案長度可以是0,在讀這種檔案時,將得到檔案結束(end-of-file)指示。 對於目錄,檔案長度通常是一個數(例如16或者512)的倍數。 對於符號連結,檔案長度是檔名中實際位元組數。例如,
其中,檔案長度7就是路徑名usr/lib的長度。 如今,大多數UNIX系統提供欄位st_blksize和st_blocks。其中,第一個是對檔案I/O較為合適的塊長度,第二個時所分配的實際512位元組塊數量。     檔案中的空洞

空洞是由所設定的偏移量超過檔案尾端,並寫了某些資料後造成的。對於沒有寫過的位元組位置,read函式讀到的位元組是0.

如果使用實用程式(例如cat(1))複製這種檔案,那麼所有這些空洞都會被填滿,其中所有實際資料位元組皆填寫為0.

十三、檔案截短

有時我們需要在檔案尾端截去一些資料為了縮短檔案。將一個檔案清空為0是一個特例,在開啟檔案時使用O_TRUNC標誌可以做到這一點。

#include <unistd.h>

int truncate(const char* pathname, off_t length);

int ftruncate(int filedes, off_t length);

返回值:若成功返回0,失敗則返回-1
這兩個函式將把現有的檔案長度截短為length位元組。如果該檔案以前的長度大於length,則超過length以外的資料將不能再訪問。如果以前的長度短於length,則其效果與系統有關。若UNIX系統實現擴充套件了該檔案,則在以前的檔案尾端和新的檔案尾端之間的資料將讀作0(也就是可能在檔案中建立了一個空洞)。


十四、檔案系統

目前,正在使用的UNIX檔案系統有多種實現。例如,Solaris支援多種不同型別的磁碟檔案系統:傳統的基於BSD的UNIX檔案系統(UFS,UNIX file system),讀、寫DOS格式化軟盤的檔案系統(稱為PCFS),以及讀CD的檔案系統(HSFS)。

我們可以把一個磁碟分成一個或多個分割槽。每個分割槽可以包含一個檔案系統。(如下圖:


 i節點是固定長度的記錄項,它包含有關檔案的大部分資訊。 如果更仔細的觀察一個柱面組的i節點和資料塊部分,則可以看到 如下圖所示的情況:
如上圖,注意下面個點: *在圖中有兩個目錄項指向同一個i節點。每個i節點中都有一個連結計數,其值是指向該i節點的目錄項數。只有當連結計數減少至0時,才可刪除該檔案(也就是可以釋放該檔案佔用的資料塊)。這就是為什麼“刪除對一個檔案的連結”操作並不總是意味著“釋放該檔案佔用的磁碟塊”的原因。在stat結構中,連結計數包含在st_nlink成員中,其基本系統資料型別是nlink_t。這種連結型別稱為硬連結,LINK_MAX指定了一個檔案連線數的最大值。 *另外一種連結型別稱為符號連結。對於這種連結,該檔案的實際內容(在資料塊中)包含了該符號連結所指向的檔案的名字。在下例子中:
該目錄項中的檔名是3字元的字串lib,而在該檔案中包含了7個數據位元組usr/lib。gaii節點中的檔案型別是S_IFLNK,於是系統知道這是一個符號連結。 *i節點包含了大多數與檔案有關的資訊:檔案型別、檔案訪問許可權位、檔案長度和指向該檔案所佔用的資料塊的指標等。stat結構的大多數資訊都是取自i節點。只有兩項資料存放在目錄項中:檔名和i節點編號。i節點編號的資料型別是ino_t。 *每個檔案系統各自對他們的i節點進行編號,因此目錄項中的i節點編號數指向同一檔案系統中的相應i節點,不能使一個目錄項指向另一個檔案系統的i節點。 *當在不更換檔案系統情況下為一個檔案更名時,該檔案的實際內容並未移動,只需構造一個指向現有i節點的心目錄項,並解除與舊目錄項的連結。這就是mv(1)命令的通常操作方式。 十五、link、unlink、remove和rename函式 任何一個檔案可以有多個目錄項指向其i節點,建立一個現有檔案的連結的方法是使用link函式。
#include<unistd.h>
int link(const char *existingpath, const char *newpath);
返回值:若成功返回0,若出錯返回-1
此函式建立一個新目錄項newpath,它引用現有的檔案existingpath。若果newpath已經存在,則返回出錯。 建立新目錄項以及增加連結計數應當是個原子操作,大多數實現要求這兩個路徑名在同一個檔案系統中。如果實現支援建立執行一個目錄的硬連結,那麼也是僅限於超級使用者才可以這麼做。 為了刪除一個現有的目錄項,可以呼叫unlink函式。
#include <unistd.h>
int unlink(const char *pathname);
返回值:若成功返回0,若出錯返回-1
此函式刪除目錄項,並將由pathname所引用檔案的連結計數減1。如果還有指向該檔案的其他連結,則仍然可以通過其他連結訪問該檔案的資料。如果出錯,怎不對該檔案做任何更改。 為了解除對檔案的連結,必須對包含該目錄項目錄具有寫和執行許可權。如果對該目錄設定了粘著位,則對該目錄必須具有寫許可權,並且具備下面三個條件之一: 1.擁有該檔案 2.擁有該目錄 3.具有超級使用者特權 只有連結計數達到0時,該檔案的內容才可以被刪除。只要有程序打開了該檔案,其內容也不能刪除。關閉一個檔案時,核心首先檢查開啟該檔案的程序數。如果該數達到0,然後核心檢查其連線數,如果這個數也是0,那麼就刪除該檔案的內容。 下例為程式開啟一個檔案,然後接出對他的鎖定。
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
        if (open("tmpfile",O_RDWR) < 0)
        {
                printf("open error\n");
        }
        if (unlink("tmpfile") < 0)
        {
                printf("unlink error\n");
        }
        sleep(15);
        printf("done\n");
        return 0;
}
執行該程式,其結果是
unlink的這種性質經常被程式用來確保及時是在該程式奔潰時,他所建立的臨時檔案也不會遺留下來。程序用open或create建立一個檔案,然後立即呼叫unlink。因為該檔案仍舊是開啟的,所以不會將其內容刪除。只有當程序關閉該檔案或終止時(在這種情況下,核心會關閉該程序開啟的全部檔案),該檔案的內容才會被刪除。