[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
引數可設定檔案的許可權,取值及其含義如下圖所示:
openat
的 fd
引數可以傳入一個目錄的 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 開始)。
對於 dup2
的 fd2
引數,用於指定新描述符的值,如果 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 calledupdate
. The commandsync
also calls the sync function.(sync
通常由系統的一個守護程序update
來週期性呼叫,命令sync
也會呼叫這個函式。)
fsync
只對 fd
這一個檔案實現同步操作,並且等待磁碟 IO 的完成才返回。
fdatasync
與 fsync
類似,但它只更新檔案的資料,而 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 看得好無聊,看著看著就想睡覺。