無緩衝檔案IO和目錄操作

HickeyZhang發表於2022-01-22

引言

在後臺開發中,對於檔案I/O我們通常不使用C語言封裝的fopen、fread、fwrite標準I/O,而是直接使用Linux提供的系統呼叫函式。因為這些系統呼叫沒有使用使用者緩衝區,我們直接與核心打交道,效率更高,且可以自己定製一些符合應用場景的操作。下面介紹Linux用於檔案I/O的資料結構,以及一些具體的系統呼叫函式。

檔案描述符

所有開啟的檔案都通過檔案描述符引用,檔案描述符只在當前程式有效,因為每個程式有一個PCB結構體,PCB包含一個檔案描述符表。

檔案描述符0對應標準輸入、1對應標準輸出、2對應標準錯誤,這些是在程式建立時預設繫結的檔案描述符。

以上分別對應unistd.h中的STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。

每個程式的最大檔案描述符可通過ulimit -a命令檢視,並可通過ulimit -n 數字設定。

image-20220122164042415

無緩衝檔案IO

我們這裡所說的無緩衝IO指的是無使用者緩衝區,如fopen、fwirte、fread等函式,程式會在使用者程式空間維護緩衝區,然後核心還有緩衝區,最後才是磁碟。而無緩衝IO是指只有核心緩衝區而無使用者緩衝區,並不是沒有任何緩衝區。

核心用於檔案I/O的資料結構

每個程式在程式表中都有一個記錄項,記錄項包含一張開啟檔案描述符表

在開啟檔案描述符表中,每個描述符佔用一項:

  • 檔案描述符標誌:目前只有CLOEXEC。
  • 指向檔案表項的指標

核心為所有開啟檔案維持一張檔案表,每個檔案表項包括:

  • 檔案狀態標誌:包括檔案型別和訪問許可權。
  • 當前檔案偏移量
  • 指向該檔案inode節點的指標

他們之間的關係如下:

image-20211102231247256 image-20211102231313264

不同的檔案表項可以指向相同的檔案(i節點即索引結點),這可以使不同的程式有它自己的對該檔案的偏移量和開啟訪問許可權。

不同的檔案描述符可以指向相同的檔案表項。如在fork後,父子程式的每個相同檔案描述符指向同一個檔案表項。

【PS】:注意檔案描述符標誌(file descriptor flags)和檔案狀態標誌(file status flags)的區別。

open函式

#include <fcntl.h>

int open(char* path, int flag, .../* mode_t mode */);

返回值:
    成功返回檔案描述符
    失敗返回-1

flag引數可以為:

  • 必選引數:O_RDONLY(只讀)、O_WRONLY(只寫)、O_RDWR(讀寫)、O_EXEC(只執行)、O_SEARCH(只搜尋,應用於目錄檔案)。這5個引數互斥,即只能指定一個。
  • 可選引數,按位或:O_APPEND(每次時都追加到檔案尾端)、O_CLOEXEC(將FD_CLOEXEC常量設定為檔案描述符標誌)、O_CREAT(若檔案不存在則建立它,用該引數時需要指定第三個引數即檔案的許可權位mode如0644,存在則直接開啟)、O_DIRECTORY(若path不是目錄,則返回錯誤)、O_EXCL(與O_CREAT同時指定,若檔案已經存在,則出錯。可以原子地測試和建立檔案。)、O_NONBLOCK(設定非阻塞)、O_SYNC(每次write等待物理I/O完成)、O_TRUNC(若檔案存在,且以只寫或讀寫開啟,則將其長度截斷為0)。

creat函式

#include <fcntl.h>

int creat(char* path, mode_t mode);

返回值:
    成功返回以只寫開啟的檔案描述符
    失敗則返回-1

creat函式的一個缺點是它以只寫方式開啟新建立的檔案。

close函式

#include <fcntl.h>

int close(int fd);

返回值:
    成功返回0
    失敗返回-1

當程式終止時,核心會自動關閉它開啟的所有檔案。

lseek函式

每個開啟檔案都有一個當前檔案偏移量(在系統全域性的開啟檔案表項中)。讀、寫操作都從當前偏移量開始,並使偏移量增加所讀的位元組數。按系統預設,當開啟一個檔案時,若沒有指定O_APPEND選項,該偏移量被設定為0。

#include <fcntl.h>

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

返回值:
    成功返回新的檔案偏移量
    失敗返回-1

whence可以取以下值:

  • SEEK_SET:將檔案偏移量設定為距檔案開始處offset位元組處,offset為正。
  • SEEK_CUR:將檔案偏移量設定為距現在位置offset位元組處,offset可正可負。
  • SEEK_END:將檔案偏移量設定為距檔案尾端offet位元組處,offset可正可負。

檔案偏移量可以大於檔案當前長度,對該檔案的下一次寫將加長該檔案,並在檔案中形成空洞。位於檔案中但沒有寫過的位元組都被讀為0.

read函式

#include <unistd.h>

ssize_t read(int fd, void* buf, size_t nbytes);

返回值:
    成功返回讀到的位元組數,若讀到檔案尾,返回0
    失敗返回-1

讀操作從檔案當前偏移量開始,讀成功後,偏移量增加讀到的位元組數。

write函式

#include <unistd.h>

ssize_t write(int fd, void* buf, size_t nbytes);

返回值:
    成功返回已寫的位元組數
    失敗返回-1

write返回值通常與引數nbytes的值相同,否則表示出錯。

read和write的阻塞、非阻塞

read和write對於常規檔案不會阻塞,一定會在有限時間內返回。read終端或網路裝置時可能阻塞,終端在使用者輸入換行時才會重新整理輸入緩衝區,網路裝置則不確定何時有輸入。寫網路裝置可能阻塞。可以在open檔案時指定O_NONBLOCK設定為非阻塞,則read時需要迴圈讀

dup和dup2函式

#include <unistd.h>

int dup(int fd);
int dup2(int fd, int fd2);

返回值:
    成功返回檔案描述符
    失敗返回-1

dup返回最小未用的檔案描述符,使返回的檔案描述符指向和fd相同的檔案表項。

dup2先關閉fd2,然後使fd2指向fd指向的檔案表項,並返回fd2。

新的描述符的執行時關閉標誌(close-on-exec)總是被dup函式清除

sync和fsync函式

#include <unistd.h>

void sync(void);
int fsync(int fd);

當向檔案中寫入資料時,核心先將資料複製到核心緩衝區,然後排入佇列,晚些時候再寫入磁碟,被稱為延遲寫

sync將所有修改過的塊緩衝區排入寫佇列,然後立即返回,並不等待實際寫磁碟操作結束。

fsync只對由檔案描述符fd指定的檔案起作用,並且等待寫磁碟操作結束後才返回,並不立即返回

stat函式

stat函式用於檢視檔案的屬性資訊。

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

int stat(char* file_path, struct stat* st);
int lstat(char* file_path, struct stat* st);  // 相比於stat,對於符號連結,lstat獲取的是符號連結本身的屬性資訊

引數:
    file_path表示檔案路徑,st表示返回的stat結構體,為傳出引數。
返回值:
    成功返回0,失敗返回-1。

stat定義如下:
    
struct stat {
    dev_t     st_dev;         /* ID of device containing file */
    ino_t     st_ino;         /* Inode number */
    mode_t    st_mode;        /* File type and mode */
    nlink_t   st_nlink;       /* Number of hard links */
    uid_t     st_uid;         /* User ID of owner */
    gid_t     st_gid;         /* Group ID of owner */
    dev_t     st_rdev;        /* Device ID (if special file) */
    off_t     st_size;        /* Total size, in bytes */
    blksize_t st_blksize;     /* Block size for filesystem I/O */
    blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */

    /* Since Linux 2.6, the kernel supports nanosecond
                  precision for the following timestamp fields.
                  For the details before Linux 2.6, see NOTES. */

    struct timespec st_atim;  /* Time of last access */
    struct timespec st_mtim;  /* Time of last modification */
    struct timespec st_ctim;  /* Time of last status change */

    #define st_atime st_atim.tv_sec      /* Backward compatibility */
    #define st_mtime st_mtim.tv_sec
    #define st_ctime st_ctim.tv_sec
};

獲取檔案型別

方法①:按位與。

switch(st.st_mode & S_IFMT){
    case S_IFBLK:  printf("block device\n");            break;
    case S_IFCHR:  printf("character device\n");        break;
    case S_IFDIR:  printf("directory\n");               break;
    case S_IFIFO:  printf("FIFO/pipe\n");               break;
    case S_IFLNK:  printf("symlink\n");                 break;
    case S_IFREG:  printf("regular file\n");            break;
    case S_IFSOCK: printf("socket\n");                  break;
    default:       printf("unknown?\n");                break;

}

方法②:使用巨集。

if(S_ISREG(st.st_mode)){
    ...
}else if(S_ISBLK(st.st_mode)){
    ...
}
...

獲取檔案許可權:按位與。

if(st.st_mode & S_IRUSR){ // 檔案所有者是否有讀許可權
    ...
}
if(st.st_mode & S_IWGRP){ // 檔案所有者組是否有寫許可權
    ...
}
if(st.st_mode & S_IXOTH){ // 其他使用者是否對檔案有執行許可權
    ...
}
...

fcntl函式

#include <fcntl.h>

int fcntl(int fd, int cmd, .../* int arg */);

返回值:
    成功返回值依賴於cmd
    失敗返回-1

cmd可以取以下值:

  • F_DUPFD、F_DUPFD_CLOEXEC複製描述符
  • F_GETFD、F_SETFD設定獲取檔案描述符標誌,目前只能改變O_CLOEXEC
  • F_GETFL、F_SETFL設定獲取檔案狀態標誌,只能改變部分屬性:O_APPENDO_ASYNCO_NONBLOCKO_DIRECTO_NOATIMEPS:改變檔案描述符標誌和檔案狀態標誌都需要先獲取原來的,然後在原來的基礎上按位或(設定)、按位與非(取消設定),最後設定。
  • F_GETOWN、F_SETOWN設定獲取非同步IO所有權
  • F_GETLK、F_SETLK、F_SETLKW設定獲取記錄鎖

getcwd函式

getcwd用於獲取程式的當前工作目錄。

#include <unistd.h>

char* getcwd(char* buf, size);

引數:
    buf為預分配的記憶體區,size為buf的大小。
返回值:
    程式的當前工作目錄儲存到buf中。
    失敗返回NULL。

chdir函式

chdir用於改變程式的當前工作目錄。

#include <unistd.h>

int chdir(const char* path);

引數:
    path為目標目錄,可為絕對或相對路徑。
    
返回值:
    成功返回0。
    失敗返回-1。

opendir函式

opendir用於開啟一個目錄檔案。

#include <sys/types.h>
#include <dirent.h>

DIR* opendir(const char* path);

引數:
    path為目錄路徑
返回值:
    失敗返回NULL。
    成功返回DIR結構體指標。
    

readdir函式

readdir用於讀取目錄檔案中的目錄項。

#include <dirent.h>

struct dirent* readdir(DIR* dir);

返回值:
    每次讀取DIR指向的目錄中的一個條目,若需要遍歷所有條目,需要迴圈呼叫readdir。readdir還會讀取到.和..即當前目錄和父目錄。

返回值:
    失敗返回NULL,並設定error。
    若所有條目都讀取完,則也返回NULL,但不設定error。
    
dirent定義如下:
    
struct dirent {
    ino_t          d_ino;       /* Inode number */
    off_t          d_off;       /* Not an offset; see below */
    unsigned short d_reclen;    /* Length of this record */
    unsigned char  d_type;      /* Type of file; not supported
                                              by all filesystem types */
    char           d_name[256]; /* Null-terminated filename */
};


d_ino為檔案對應的索引結點編號。

d_type取值如下,可以用來判斷檔案型別:

    DT_BLK      This is a block device.

    DT_CHR      This is a character device.

    DT_DIR      This is a directory.

    DT_FIFO     This is a named pipe (FIFO).

    DT_LNK      This is a symbolic link.

    DT_REG      This is a regular file.

    DT_SOCK     This is a UNIX domain socket.

    DT_UNKNOWN  The file type could not be determined.
        
d_name為檔名,以'\0'結尾,最大長度為256。

closedir函式

closedir用於關閉開啟的目錄檔案。

#include <sys/types.h>
#include <dirent.h>

int closedir(DIR* dir);

引數:
    dir為目錄結構體
返回值:
    失敗返回-1。

參考資料

  • 《APUE》
  • 《Linux核心設計與實現》

相關文章