1. 程式人生 > 實用技巧 >[APUE] 檔案 I/O

[APUE] 檔案 I/O

檔案操作相關 API:open, read, write, lseek, close.

多程序共享檔案的相關 API:dup, dup2, fcntl, sync, fsync, ioctl.

檔案操作 API

open and openat

函式原型:

#include <fcntl.h>
int open(const char *path, int oflag, ... /* mode_t mode */ );
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */ );
// Both return: file descriptor if OK, −1 on error

oflag 是下列選項的組合(通過或運算 | ):

  • 必選且只能選擇一個:O_RDONLY, O_WRONLY, O_RDWR
  • 可選項
    • O_APPEND: 寫檔案時追加到尾端。
    • O_CLOEXEC
    • O_CREAT: 檔案不存在時建立;若使用該選項,需要 mode 引數,指定檔案到訪問許可權。
    • O_DIRECTORY: 如果 path 不是目錄,出錯。
    • O_EXCL: 如果同時指定了 O_CREAT,而檔案已存在,則出錯。
    • O_NOCITY
    • O_NOFOLLOW: 如果 path 是一個符號連結,則出錯。
    • O_NOBLOCK:如果 path 是一個 FIFO、一個塊特殊檔案或一個字元特殊檔案,則為本次開啟操作和後續的I/O操作設定非阻塞模式 (Nonblocking Mode) .
    • O_SYNC: 使每次 write 操作等待物理 I/O 完成,包括由該 write 操作引起的檔案屬性的更新所需要的 I/O 。
    • O_DSYNC
    • O_RSYNC
    • O_TRUNC:如果檔案存在,且開啟模式可寫,那麼長度截斷為 0 。
    • O_TTYINIT

mode 引數可設定檔案的許可權,取值及其含義如下圖所示:

openatfd 引數可以傳入一個目錄的 fd:

int dir = open(".", O_RDONLY | O_DIRECTORY);
int fd = openat(dir, "test.c", O_RDONLY);

creat

函式原型:

int creat(const char *path, mode_t mode);
// Returns: file descriptor opened for write-only if OK, −1 on error

creat 函式相當於:open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);

mode 表示檔案的訪問許可權,將在後續章節解析。

close

函式原型:

int close(int fd);
// Returns: 0 if OK, −1 on error

關閉檔案,釋放該程序加在該檔案上的所有記錄鎖。

程序結束時,核心會自動關閉所有它開啟的檔案,所以 close 有時候可有可無。

lseek

off_t lseek(int fd, off_t offset, int whence);
// Returns: new file offset if OK, −1 on error

對於 offset 引數的解析,取決於 whence 的值:

  • SEEK_SET:該檔案的偏移量設定為距檔案開始處的 offset 個位元組
  • SEEK_CUR:該檔案的偏移量設定為當前位置加上 offset 的值,這時候 offset 可為負數。
  • SEEK_END:該檔案的偏移量設定為檔案長度加上 offset 的值,這時候 offset 可為負數。

如果 lseek 成功執行,返回新的檔案偏移量,否則返回 -1 。如果 fd 指向的是一個 FIFO、管道或者 socket,lseek 返回 -1,並把 errno 設定為 ESPIPE (Illegal Seek) . lseek 不引起任何 IO 操作,僅僅把當前偏移量記錄在核心當中,用於下一次的讀寫操作。

例子1

int main()
{
    if (lseek(STDIN_FILENO, 0, SEEK_CUR) != -1) puts("can seek");
    else puts("can not seek");
}

執行結果:

$ ./a.out < /etc/passwd
can seek
$ cat /etc/passwd | ./a.out 
can not seek

< 符號的作用是重定向輸入。

一般情況下,當前偏移量應當為非負數,但某些裝置(Linux中一切皆檔案)允許它為負數。此外,偏移量可以大於檔案長度,這種情況下,對檔案的下一次寫操作將「加長」檔案,在檔案中形成一個「空洞」(位元組均值為 0 ),空洞不一定會佔據磁碟空間,具體取決於檔案系統的實現。

例子2:空洞檔案

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
int main()
{
    int fd = -1;
    if ((fd = creat("file.hole", FILE_MODE)) == -1) err_sys("creat error");
    if (write(fd, buf1, 10) != 10)                  err_sys("write error");
    if (lseek(fd, 16384, SEEK_SET) == -1)           err_sys("lseek error");
    if (write(fd, buf2, 10) != 10)                  err_sys("write2 error");
    // now offset is at 16394
}

執行結果:

$ ll file.hole 
-rw-r--r-- 1 sinkinben sinkinben 16394 1月  20 15:20 file.hole
$ od -c file.hole 
0000000   a   b   c   d   e   f   g   h   i   j  \0  \0  \0  \0  \0  \0
0000020  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0040000   A   B   C   D   E   F   G   H   I   J
0040012

od -c 以八進位制輸出檔案內容,hex(16394) = 0x400a .

建立一個同樣長度但沒有空洞的檔案 file.nohole

$ ls -sl file.*
 8 -rw-r--r-- 1 sinkinben sinkinben 16394 1月  20 15:31 file.hole
20 -rw-r--r-- 1 sinkinben sinkinben 16394 1月  20 15:31 file.nohole

可以看出,file.nohole 佔據了 20 個磁碟塊。

read

函式原型:

ssize_t read(int fd, void *buf, size_t nbytes);
// Returns: number of bytes read, 0 if end of file, −1 on error

可能存在返回值(實際讀到的位元組數)小於要求讀取的位元組數 nbytes 的情況:

  • 讀取普通檔案:當前 offset 離檔案末端只有 30 位元組,而要求讀取 100 位元組。
  • 讀取終端裝置:通常一次最多讀取一行。
  • 從網路 socket 中讀取:網路的緩衝機制可能造成上述情況。
  • 從管道或者 FIFO 讀取:與讀取普通檔案類似,剩下的位元組數不足。
  • 當某一訊號造成讀取中斷。

read 操作一般都會採用預讀機制 (Read Ahead) 提高效能,預讀的資料放入到 Cache 當中,那麼下一次讀取就不用讀取磁碟。

write

函式原型:

ssize_t write(int fd, const void *buf, size_t nbytes);
// Returns: number of bytes written if OK, −1 on error

與 read 操作類似。返回值通常與 nbytes 相等,否則表示出錯。出錯的原因可能為磁碟已滿,或者超過一個程序的檔案長度限制。

檔案共享

在 Unix 系統中,核心為每個程序都建立了一個**檔案描述符表 (即下圖的 Process Table Entry, 名字是我自己翻譯的) **,程序開啟某個檔案都過程如下圖所示。

程序的每一個 fd 都有對應的檔案指標 (File Pointer) 指向某一個檔案表項 (File Table Entry) ,該表項包括當前開啟檔案的狀態資訊和一個 v-node 指標。其中 v-node 包含了檔案的型別和操作該檔案的函式指標等資訊,還包括一個指向檔案 inode 的指標。

如下圖所示,如果 2 個程序同時打開了同一個檔案,那麼這 2 個 File Table Entry 的 v-node 指標將會指向同一個 v-node 。由圖中的過程可以看出,不同程序開啟同一檔案,每個程序對檔案的偏移量是獨立的,檔案的狀態資訊 (File Status Flags) 也是獨立的。

基於這個過程,可以對上述對一些 IO 操作的特徵進行解析:

  • 完成一次 write 操作後,File Table Entry 中的 offset 將會增加寫入的位元組數。如果當前的 offset 超過了 i-node 中的檔案大小 (current file size) ,那麼就將 current file size 設定為當前的 offset 。
  • 使用 O_APPEND 開啟一個檔案,File Table Entry 中的 file status flags 會記錄這個 O_APPEND 。每次 write 操作執行時,首先會把 current file offset 設定為 i-node 中的 current file size。
  • 若使用 lseek 定位到檔案末端,則會把 offset 設定為 file size 。
  • lseek 只修改 File Table Entry 中的 offset,不進行任何 IO 操作。

如果程序進行了 fork 操作,那麼 Process Table Entry 中的檔案描述符表也會被子程序拷貝,所以也有可能有多個 File Pointer 指向同一個 File Table Entry 。類似,dup 操作也會使得同一程序中的 2 個不同的 fd 指向同一個 File Table Entry。

原子操作

在多程序場景下,需要對同一個日誌檔案進行寫操作,那麼就有可能會出現程序 A 的內容被程序 B 的內容覆蓋的情況(因為檔案偏移量是獨立的)。

因此,寫操作需要實現為一個原子操作(要麼全做,要麼全不做),才能滿足上述場景的要求。

pread and pwrite

函式原型:

ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
// Returns: number of bytes read, 0 if end of file, −1 on error
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset); 
// Returns: number of bytes written if OK, −1 on error

作用:從離檔案開始處的 offset 位置開始,讀取 nbytes 個位元組。

pread 的行為相當於呼叫 lseek 後再次呼叫 read ,但 pread 是一個原子操作,這意味著:

  • 呼叫 pread 過程中,無法中斷其定位 lseek 和 read 操作。
  • 不更新當前的檔案偏移量。

pwrite 與之類似。

dup and dup2

int dup(int fd);
int dup2(int fd, int fd2);
// Both return: new file descriptor if OK, −1 on error

作用:把 fd 複製為一個新的描述符。如果傳入的 fd 無效,那麼返回 -1 。

dup 返回的總是可用對檔案描述符中的最小值(也就是從 3 開始)。

對於 dup2fd2 引數,用於指定新描述符的值,如果 fd2 已經開啟,會先關閉它:

  • fd1 == fd2 : 返回 fd2,且不關閉它。
  • 如果 fd1 無效,那麼返回 -1.
  • 如果 fd1 有效,那麼把 fd1 複製為 fd2 ,返回 fd2

如下圖所示,經過 dup 操作後,會有多個檔案指標指向同一個 File Table Entry。

sync, fsync and fdatasync

現在的計算機通常都會有 Cache,為了提高 IO 效能,除了在 read 一小節提到的預讀機制外,還有延遲寫機制 (Delayed Write) 。當我們向檔案寫資料時,首先會拷貝到高速緩衝區當中,後面再把高速緩衝區中的資料寫到磁碟上(通過排隊 FIFO 的順序)。

在某些場景下,我們需要緩衝區的資料和磁碟的資料保持一致。因此需要 sync, fsync, fdatasync 這三個函式。

函式原型:

int fsync(int fd); 
int fdatasync(int fd);
// Returns: 0 if OK, −1 on error
void sync(void);

sync 的作用:

  • The sync function simply queues all the modified block buffers for writing and returns; it does not wait for the disk writes to take place. (不等待磁碟操作完成)

  • sync is normally called periodically (usually every 30 seconds) from a system daemon, often called update. The command sync also calls the sync function.(sync 通常由系統的一個守護程序 update 來週期性呼叫,命令 sync 也會呼叫這個函式。)

fsync 只對 fd 這一個檔案實現同步操作,並且等待磁碟 IO 的完成才返回。

fdatasyncfsync 類似,但它只更新檔案的資料,而 fsync 還會更新檔案的屬性(包括許可權資訊等)。

fcntl and ioctl

函式原型:

int fcntl(int fd, int cmd, ... /* int arg */ );
// Returns: depends on cmd if OK (see following), −1 on error
int ioctl(int fd, int request, ...);
// Returns: −1 on error, something else if OK

fcntl 可以改變檔案 fd 的屬性資訊。ioctl 一般用於外部裝置(比如實現驅動程式) 的 IO 操作。

總結

APUE 看得好無聊,看著看著就想睡覺。