UNIX環境程式設計-第四章檔案和目錄

在水一方。發表於2020-10-02

第四章 檔案和目錄

4.1 函式stat、fstat、fstatat和lstat

通常函式stat、fstat、fstatat和lstat獲取檔案資訊。

函式原型:
#include <sys/stat.h>
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);
所有函式成功返回0,失敗返回-1並設定errno。

  • 函式stat傳入一個檔案地址和,傳出一個檔案屬性結構體型別。
  • 函式fstat傳入一個開啟的檔案描述符,傳出一個檔案屬性結構體型別。
  • lstat函式類似於stat,但是當命名的檔案是一個符號連結時,lstat返回該符號連結的有關資訊,而不是由該符號連結引用的檔案的資訊。
  • fstatat函式為一個相對於當前開啟目錄的路徑名返回檔案統計資訊。flag引數控制著是否跟隨一個符號連結。當AT_SYMLINK_NOFOLLOW標誌被設定時,fstatat不會跟隨符號連結,而是返回符號連結本身的資訊。否則,在預設情況下,返回的是符號連結所指向的時間檔案的資訊。如果fd引數的值是AT_FDCWD,並且pathname引數是一個相對路徑名,fstatat會被計算相對於當前目錄的pathname引數。如果pathname是一個絕對路徑,fd引數就會被忽略。這兩種情況下,根據flag的取值,fstatat的作用就跟stat或lstat一樣。

stat結構體定義如下:
在這裡插入圖片描述
timeespec截圖型別按照秒和納秒定義了時間,至少包括下面兩個欄位:
time_t tv_sec;
logn tv_nsec;

4.2 檔案型別

UNIX系統的大多數檔案時普通檔案或目錄,但是又包含了另外一些檔案型別,具體如下:

  • 普通檔案
  • 目錄檔案
  • 塊檔案
  • 字元檔案
  • 管道(FIFO)
  • 套接字(socket)
  • 符號連結

檔案類資訊包含在stat結構的st_mode成員中。下圖所示巨集確定檔案型別。這些巨集引數都是stat結構中的st_mode成員。
在這裡插入圖片描述
POSI.1允許實現將程式間通訊(IPC)物件(如訊息佇列和訊號量等)說明為檔案。下圖的巨集可用來從stat結構中確定IPC物件的型別。而這些巨集與上圖的巨集有的不同它們的引數並非是st_mode,而是指向stat結構的指標。
在這裡插入圖片描述
使用lstat獲取檔案型別:

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "argc error");
        exit(-1);
    }
    struct stat s;
    if (lstat(argv[1], &s) < 0)
    {
        perror("lstat error");
        exit(-1);
    }
    char *ptr;
    if (S_ISREG(s.st_mode))
        ptr = "regular";
    else if (S_ISDIR(s.st_mode))
        ptr = "directory";
    else if (S_ISCHR(s.st_mode))
        ptr = "character special";
    else if (S_ISBLK(s.st_mode))
        ptr = "block special";
    else if (S_ISFIFO(s.st_mode))
        ptr = "fifo";
    else if (S_ISLNK(s.st_mode))
        ptr = "symbolic link";
    else if (S_ISSOCK(s.st_mode))
        ptr = "socket";
    else
        ptr = "** unknown mode **";
    printf("%s\n", ptr);
    return 0;
}

4.3 設定使用者ID和設定組ID

一個程式相關聯的ID有6個或更多,具體如下圖所示:
在這裡插入圖片描述
當執行一個程式檔案時,程式的有效ID通常就是實際使用者ID,有效組ID通常是實際組ID。但是可以在檔案模式子(st_mode)中設定一個特殊標誌,其含義是“當執行此檔案時,將程式的有效使用者ID設定為檔案所有者的使用者ID(st_uid)”。與此類似,在檔案模式字中可以設定另一個,它可將執行檔案的程式的有效組ID設定的組所有ID(st_gid)。在檔案模式字中的這兩位被稱為設定使用者ID(set-user-ID)位和設定組ID(set-group-ID)位。

4.4 檔案訪問許可權

st_mode值包含了對檔案的訪問許可權位。
每個檔案有9個訪問許可權,可將它們分為3類,具體見下圖:
在這裡插入圖片描述

4.5 新檔案和目錄的所有權

新檔案的使用者ID設定為程式的有效使用者ID。

  1. 新檔案的組ID可以是程式的有效組ID
  2. 新檔案的組ID可以是它所在目錄的組ID

4.6 函式access和faccessat

通常兩個函式用來測試檔案的訪問許可權。

函式原型
#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
兩個函式的返回值,成功返回0,失敗返回-1並設定errno
mode引數值有以下四種:
F_OK:測試檔案是否已經存在
R_OK:測試讀許可權
W_OK:測試寫許可權
X_OK:測試執行許可權

當pathname引數為絕對路徑時faccessat和access兩個函式的作用是一樣的,當fd引數取值為AT_FDCWD而pathname引數為相對路徑時兩個函式的作用也是一樣的。
flag引數可以用於改變faccesat的行為,如果flag設定為AT_ACCESS,訪問檢查用的是呼叫程式的有效使用者ID和有效組ID,而不是實際使用者ID和實際組ID。
例項:函式access的使用方法。

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr,"usage: a.out <pathname>");
        exit(-1);
    }
    if (access(argv[1], R_OK) < 0)
    {
        fprintf(stderr, "access error for %s\n", argv[1]);
        perror(strerror(errno));
        exit(-1);
    }
    else
    {
        printf("read access OK\n");
    }
    if (open(argv[1], O_RDONLY) < 0)
    {
        fprintf(stderr, "open error for %s\n", argv[1]);
        perror(strerror(errno));
    	exit(-1);
    }
    else
    {
        printf("open for reading OK\n");
    }
    return 0;
}

4.7 函式umask

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

函式原型:
#include <sys/stat.h>
mode_t umask(mode_t cmask);
返回之前的檔案模式建立遮蔽字

引數cmask是由下圖的9個常量組成也可以通過8進位制的數字直接傳入:
在這裡插入圖片描述
通常情況下系統會有一個預設的檔案許可權掩碼。在使用open或creat函式時在某些情況下需要修改該預設的檔案許可權掩碼。
例項,使用umask設定檔案許可權掩碼:

#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main()
{
  umask(0000);
  if (creat("foo", 0666) < 0)
  {
    fprintf(stderr, "creat error for foo\n");
    perror(strerror(errno));
    exit(-1);
  }
  umask(0066);
  if (creat("bar", 0666) < 0)
  {
    fprintf(stderr, "creat error for bar\n");
    perror(strerror(errno));
    exit(-1);
  }
  return 0;
}

程式執行後使用ls -l foo bar等到如下結果:
在這裡插入圖片描述

在shell中使用umask檢視系統預設的檔案許可權掩碼:
在這裡插入圖片描述
在程式中我們使用的0666分別表示的是什麼在下圖中會有詳細說明:
在這裡插入圖片描述

4.8 函式chmod、fchmod和fchmodat

chmod、fchmod和fchmodat這三個函式主要用於更改現有檔案的訪問許可權。

函式原型:
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
三個函式成功返回0,失敗返回-1並設定errno

chmod函式時對指定檔案進行修改,fchmod時對以開啟的檔案進行操作。fchomdat和fchmod在一下兩種情況下是相同的:

  1. pathname為絕對路徑
  2. fd引數取值為AT_FDCWD而pathname引數為相對路徑。

否則fchmodat計算相當於開啟目錄(fd)的pathname。flag引數可以用於改變fchmod的行為,當設定了AT_SYMLINK_NOFOLLOW標誌時,fchmodat並不會跟隨符號連結。
引數mode如下圖所示:
在這裡插入圖片描述
例項,使用chmod修改檔案許可權:

#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main()
{
  struct stat s;
  if (stat("foo", &s) < 0)
  {
    perror("stat error for foo");
    exit(-1);
  }
  if (chmod("foo", (s.st_mode & ~S_IXGRP) | S_ISGID) < 0)
  {
    perror("chmod error for foo\n");
    exit(-1);
  }
  if (chmod("bar", 0644) < 0)
  {
    perror("chmod error for bar\n");
    exit(-1);
  }
  return 0;
}

執行前檔案的許可權:
在這裡插入圖片描述
執行後檔案許可權:
在這裡插入圖片描述
chmod函式更新的只是i節點最近一次被更改的時間。按系統預設方式,ls - l列出的是最後修改檔案內容的時間。

4.9 粘著位

S_ISVTX標誌。如果一個可執行檔案的這一位被設定了,那麼當該程式第一次被執行,在其終止時,程式正文部分的一個副本仍被儲存在交換區。(程式的正文部分是機器指令)。這使得下次執行該程式時能較快的將其載入記憶體。
如果對一個目錄設定了粘著位,只有對該目錄具有寫許可權的使用者並且滿足下列條件之一,才能刪除或重新命名該目錄下的檔案:

  1. 擁有此檔案
  2. 擁有此目錄
  3. 是超級使用者
    現在較新的UNIX系統大多數都配置了虛擬儲存系統以及快速檔案系統,所以不再需要使用這種技術。

4.10 函式chown、fchown、fchownat和lchown

這些函式可用於更改檔案的使用者ID和組ID。如果兩個引數owner或group中的任意一個是-1,則對應的ID不變。

函式原型:
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);
int lchown(const char *pathname, uid_t owner, gid_t group);
4個函式成功返回0,失敗返回-1並設定errno。

4.11 檔案長度

stat結構成員st_size表示以位元組為單位的檔案的長度。此欄位只對普通檔案,目錄檔案和符號檔案有意義。
普通檔案:檔案長度可以為0。在開始讀這種檔案時,將得到檔案結束指示。
目錄:檔案長度一百是一個數(如16或512)的整數倍。
符號連結:檔案長度是在檔名中的實際位元組數。
現今,大多數現代的UNIX系統提供欄位st_blksize和st_blocks。其中,第一個是對檔案I/O較適合的塊長度,第二個是所分配的實際位元組數。在之前的章節中我們將st_blksize用於讀操作時,讀一個檔案所需的實際量最少。為了提高效率,標誌I/O庫也試圖讀一次,寫st_blksize個位元組。

檔案中的空洞

在之前的章節中我們提到過普通檔案可以保護空洞檔案。空間檔案是由所設定的偏移量超過檔案尾端,並寫入了某些資料後造成的。

4.12 檔案截斷

有時候我們需要在檔案尾端處截去一些資料以縮短檔案。將一個檔案的長度截斷為0是一個特例,在開啟檔案時使用O_TRUNC標誌可以做到這一點。為了截斷檔案可以呼叫函式truncate和ftruncate。

#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
兩個函式,成功返回0,失敗返回-1,並設定errno。
這兩個函式將一個檔案長度截斷為length。如果該檔案以前的長度大於length,則超過length以外的資料就不會再能訪問。如果一起的長度小於length,檔案長度將增加,在以前的檔案尾端和新的檔案尾端之間的資料將讀做0(也就是也可能在檔案中建立了空洞)。

4.13 檔案系統

在這裡插入圖片描述

  • 在下圖中有兩個目錄項指向同一個i節點。每個i節點中都有一個連結計數,其值時指向該i節點的目錄項數。只有當連結計數減少至0時,才可刪除該檔案(也就是可以釋放該檔案佔用的資料塊)。這就是為什麼“解除對一個檔案的連結”操作並不總是意味著“釋放該檔案佔用的磁碟塊”的原因。這也是為什麼刪除一個目錄項的函式被稱為unlike而不是delete的原因。在stat結構中,連結計數包含在st_nlink成員中,其基本系統資料型別時nlink_t。這種連結型別稱為硬連結。其中POSIX.1常量LINK_MAX指定了一個檔案連結數的最大值。在這裡插入圖片描述
  • 另外一種連結型別被稱為符號連結(symbolic link)。符號連結檔案的實際內容(在資料塊中)包含了該符號連結所指向的檔案的名字。而該型別的i節點的檔案型別時S_IFLNK,於是系統知道這是一個符號連結。
  • i節點包含了檔案有關的所有資訊:檔案型別、檔案訪問許可權位、檔案長度和指向檔案資料塊的指標等。stat結構中的大多數資訊都取自i節點。只有兩項重要資料存放在目錄項中;檔名和i節點編號。其他的資料項(如檔名長度和目錄項長度)在APUE中並不關心。i節點編號的資料型別時ino_t。
  • 因為目錄項中的i節點編號指向同一檔案i節點,一個目錄項不能指向另一個檔案系統的i節點。這就是為什麼ls(1)命令(構造一個指向一個現有檔案的新目錄項)不能跨越檔案系統的原因。
  • 當在不更換檔案系統的情況下為一個檔案重新命名時,該檔案的實際內容並未移動,只需構造一個指向現有i節點的新目錄項。連結計數不會改變。例如為將檔案/usr/lib/foo重新命名為/usr/foo,如果目錄/usr/lib和/usr在同一個檔案系統中,則檔案foo的內容無需移動,這就是mv(1)命令的統稱操作方式。

4.14 函式link、linkat、unlike、unlinkat和remove

任何一個檔案可以有多個目錄項指向其i節點。建立一個指向現有檔案的連結的方法是使用link函式或linkat函式。

函式原型:
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const *newpath, int flag);
兩個函式的返回值:成功返回0,失敗返回-1並設定errno

這兩個函式建立一個新的目錄項newpath,它引用現有檔案existingpaht。如果newpath已經存在,則返回出錯。只建立newpath中的最後一個分量,路徑中的其它部分應當已經存在。
為了刪除一個現有的目錄項,可以呼叫unlink函式。

函式原型:
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pahtname, int flag);
兩個函式的返回值:成功返回0,失敗返回-1並設定errno

當使用這兩個函式刪除pathname時只會減少pathname所引用檔案的連結計數,只有當連結計數減為0時才會被真正刪除。
例項,開啟一個檔案然後用unlink它:

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    if (open("tempfile", O_RDWR) < 0)
    {
        perror("open error:");
        exit(-1);
    }
    if (unlink("tempfile") < 0)
    {
        perror("unlink error:");
        exit(-1);
    }
    printf("file unlinked\n");
    sleep(15);
    printf("done\n");
    return 0;
}

我們也可以使用remove函式解除對一個檔案或目錄的連結。對於檔案和unlink功能一樣,對於目錄和rmdir功能一樣。

函式原型:
#include <stdio.h>
int remove(const char *pathname);
成功返回0,失敗返回-1並設定errno。

4.15 函式rename和renameat

檔案或目錄可以使用rename或者renameat函式進行重新命名。

函式原型:
#includ <stdio.h>
int rename(const char *oldname, const char *newname);
int renameat(int fd, const char *oldname, int newfd, const char *newname);
兩個函式成功返回0,失敗返回-1並設定errno。

4.16 符號連結

符號連結時對一個檔案的間接指標,它與硬連結有所不同,硬連結直接指向檔案的i節點。引入符號連結的原因是為了避開硬連結的一些限制。

  • 硬連結通常要求連結和檔案位於同一檔案系統中。
  • 只有超級使用者才能建立指向目錄的硬連結(在底層檔案系統支援的情況下)。

當使用已符號連結引用檔案的函式時,應當考慮該函式是否出了符號連結。也就是該函式是否跟隨符號連結到達它所連結的檔案。如若該函式具有處理連結的功能,則其路徑名引數引用由符號連結指向的檔案。否則,一個路徑名引數引用連結本身,而不是由該連結指向的檔案。
下圖列出了本章中所說明的各個函式是否出了符號連結。
在這裡插入圖片描述
mkdir、mkinfo、mknod和rmdir這些函式,傳入符號連結時,它們會出錯返回。以檔案描述符作為引數的一些函式(如fstat、fchmod等)對符號連結的處理時有返回檔案描述符的函式進行的。chown是否跟隨符號連結取決於實現。在所有現代的系統中,chown函式都跟隨符號連結。

4.17 建立和讀取符號連結

可以用symlink和symlinkat函式建立一個符號連結。

函式原型:
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
兩個函式成功返回0,失敗返回-1並設定errno。
建立符號連結時並不要求actualpath已經存在。並且actualpath和sympathy並不需要位於同一檔案系統中。

因為open函式跟隨符號,所以需要有一種方法開啟該連結本身,並讀該連結中的名字,readlink和readlinkat函式提供了這種功能。

函式原型:
#include <unistd.h>
ssize_t readlink(const char *restrict pathname, char *restrict buf, size_t bufsize);
ssize_t readlinkat(int fd, const char *restrict pathname, char *restrict buf, size_t bufsize);
兩個函式成功返回讀取的位元組數,失敗返回-1並設定errno。
兩個函式組合了open、read和close的所有操作。

4.18 檔案的時間

對於每個檔案維護3個時間欄位,它們的意義如下圖。
在這裡插入圖片描述

4.19 函式futimens、utimensat和utimes

一個檔案的訪問和修改時間可以用以下幾個函式更改。futimens和utimensat函式可以指定那麼級精度的時間戳。用到的資料結構是與stat函式族相同的timespec結構。

函式原型:
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
兩個函式成功返回0,失敗返回-1並設定errno。
這兩個函式的times陣列引數的第一個元素包含訪問時間,第二個元素包含修改時間。這兩個時間值時日曆時間。這是特定時間(1970年1月1日00:00:00)以來所經過的秒數。不足秒的部分用納秒錶示。

函式utimes:

函式原型:
#include <sys/time.h>
int utimes(const char *pathname, const struct timeval times[2]);
函式成功返回0,失敗返回-1,並設定errno。
在這裡插入圖片描述

4.20 函式mkdir、mkdirat和rmdir

mkdir和mkdirat建立目錄,rmdir刪除目錄。

函式原型:
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mdirat(int fd, const char *pathane, mode_t mode);
兩個函式成功返回0,失敗返回-1並設定errno。
#include <unistd.h>
int rmdir(const char *pathname);
成功返回0,失敗返回-1並設定errno。
如果呼叫rmdir使得目錄的連結計數成為0,並且沒有其他程式開啟此目錄,則釋放由此目錄佔用的空間。如果在連結計數到達0時,有一個或多個程式開啟此目錄,則在此函式返回前刪除最後一個連結及.和…項。另外,在此目錄中不能再建立新檔案。但是在最後一個程式關閉它之前並不釋放此目錄。(即使另一些程式開啟該目錄,它們在此目錄下也不能指向其他操作。這樣處理的原因是,為了使rmdir函式成功執行,該目錄必須是空的。)

4.21 讀目錄

對於某個目錄具有訪問許可權的任一使用者都可以讀該目錄。但是,為了防止檔案系統產生混亂,只有核心才能寫目錄。一個目錄的寫許可權位和執行許可權位決定了在該目錄中能否建立檔案以及刪除目錄,它們並不表示能否寫目錄本身。

函式原型:
#inlcude <dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
兩個函式成功返回指標,出錯返回NULL;
struct dirent *readdir(DIR *dp);
成功返回指標,若在目錄尾或出錯,返回NULL;
void rewinddir(DIR *dp);
int closedir(DIR *dp);
成功返回0失敗返回-1並設定errno;
long telldir(DIR *dp);
返回值與dp關聯的目錄中的當前位置有關
void seehdir(DIR *dp, long loe);

4.22 函式chdir、fchdir和getcwd

每個程式都有一個當前工作目錄,此目錄是搜尋所有相對路徑的起點(不以斜線開始的路徑名為相對路徑)。當使用者登入到UNIX系統時,其當前工作目錄時口令檔案(/etc/passwd)中該使用者登入的第6個欄位-使用者的起始目錄,當前工作目錄時程式的一個屬性,起始目錄則是登入名的一個屬性。
程式呼叫chdir或fchdir函式可以更改當前工作目錄。

函式原型:
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
兩個函式成功返回0,失敗返回-1並設定errno。
在這兩個函式中,分別用pathname和檔案描述符來指定新的當前工作目錄。
因為當前工作目錄是程式的一個屬性,所以它隻影響呼叫chdir的程式本身,而不影響其他程式。這意味著如下程式不會產生我們可能希望得到的結果。

#include <unistd.h>
#include <sys/errno.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    if (chdir("/tmp") < 0)
    {
        perror("chdir failed: ");
        exit(-1);
    }
    printf("chdir to /tmp succeeded\n");
    return 0;
}

執行後的結果
在這裡插入圖片描述
從中可以發現執行程式後並沒有改變shell的當前工作目錄,這是shell執行程式工作方式的一個副作用。每個程式執行在獨立的程式中,shell的當前工作目錄並不會隨著程式呼叫chdir而改變。由此可見,為了改變shell程式自己的工作目錄,shell應當直接呼叫chdir函式,為此,cd命令內鍵在shell中。
使用getcwd函式可以獲取當前程式的工作目錄:

函式原型:
#include <unistd.h>
char *getcwd(char *buf, size_t size);
成功返回buf,失敗返回NULL;

例項使用getcwd獲取工作目錄:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *ptr;
    size_t size = 32;
    if (chdir("/tmp") < 0)
    {
        perror("chdir failed: ");
        exit(-1);
    }
    ptr = (char*)malloc(size);
    if (getcwd(ptr, size) == NULL)
    {
        perror("getcwd failed: ");
        exit(-1);
    }
    printf("cwd = %s\n", ptr);
    return 0;
}

4.23 裝置特殊檔案

  • 每個檔案系統所在的儲存裝置都由其主、次裝置號表示。裝置號所用的資料型別是基本系統資料型別dev_t。主裝置號標識裝置驅動程式,有時編碼為與其通訊的外設半;次裝置號標識特定的子裝置。
  • 我們通常可以使用兩個巨集:major和minor來訪問主、次裝置號,大多數實現都定義這兩個巨集。這就意味著我們無需關心這兩個數時如何存放在dev_t物件中的。
    在這裡插入圖片描述
  • 系統中與每個檔名關聯的st_dev值時檔案系統的裝置號,該檔案系統包含了這一檔名以及與其對應的i節點。
  • 只有字元特殊檔案和塊特殊檔案才有st_rdev值。此值包含實際裝置的裝置號。

4.24 檔案許可權位小結

在這裡插入圖片描述

在這裡插入圖片描述

相關文章