第四章 文件IO 通用 IO 模型
本章的重點是用於執行文件輸入和輸出的系統調用。我們介紹了文件描述符的概念,然後探討了構成所謂通用I/O模型的系統調用。這些系統調用用於打開和關閉文件,以及讀取和寫入數據。我們關注磁盤文件上的 I/O。然而,這裏涵蓋的大部分內容對後續章節都相關,因為相同的系統調用用於對各種類型的文件(如管道和終端)執行 I/O。
第五章擴展了本章關於文件I/O的討論,並提供了更多關於文件I/O的細節。文件I/O的另一個方面——緩衝——複雜到足以值得它自己的章節。第13章涵蓋了內核和stdio庫中的I/O緩衝。
概述
所有用於執行 I/O 的系統調用都使用文件描述符來引用已打開的文件,文件描述符是一個(通常很小)的非負整數。文件描述符用於引用所有類型的已打開文件,包括管道、FIFO、套接字、終端、設備和普通文件。每個進程都有自己的文件描述符集。
由外殼程序在程序啓動之前代表執行。或者更精確地説,程序繼承了外殼程序的文件描述符的副本,並且外殼程序通常始終打開這三個文件描述符。(在交互式外殼中,這三個文件描述符通常指向外殼正在運行的終端。)如果命令行中指定了 I/O 重定向,那麼外殼程序會確保在啓動程序之前適當地修改文件描述符。
按慣例,大多數程序預期能夠使用表4‑1中列出的三個標準文件描述符。這三個描述符在程序啓動時被打開。
Table 4-1: Standard file descriptors
|
File descriptor
|
Purpose
|
POSIX name
|
stdio stream
|
|
0
|
standard input
|
STDIN_FILENO
|
stdin
|
|
1
|
standard output
|
STDOUT_FILENO
|
stdout
|
|
2
|
standard error
|
STDERR_FILENO
|
stderr
|
當在程序中引用這些文件描述符時,我們可以使用數字(0、1 或 2),或者最好使用在<unistd.h> 中定義的 POSIX 標準名稱。
儘管變量stdin、stdout和stderr最初指向進程的標準輸入、輸出和錯誤,但可以通過使用freopen()庫函數將它們更改為指向任何文件。作為其操作的一部分,freopen()可能會更改重新打開的流的底層文件描述符。換句話説,例如在stdout上執行一次freopen()之後,就不再安全地假設底層文件描述符仍然是1。
以下是為執行文件 I/O 的四個關鍵系統調用(編程語言和軟件包通常僅通過 I/O 庫間接使用這些調用):
- fd =open(pathname, flags, mode) 打開由 pathname 指定的文件,返回一個文件描述符,用於在後續調用中引用打開的文件。如果文件不存在,open() 可能根據 flags 位掩碼參數的設置來創建它。flags 參數還指定文件是要以讀取、寫入還是兩者模式打開。mode 參數指定如果文件由此調用創建,應將其設置為的權限。如果 open() 調用不是用來創建文件,則忽略此參數並且可以省略。
- numread = read(fd, buffer, count) 從由 fd 指定的打開文件中最多讀取count 個字節,並將它們存儲在 buffer 中。read() 調用返回實際讀取的字節數。如果沒有更多字節可以讀取(即遇到文件末尾),read() 返回 0。
- numwritten = write(fd, buffer, count) 將最多 count 個字節從 buffer 寫入由 fd 指定的打開文件。write() 調用返回實際寫入的字節數,可能小於 count。
- status = close(fd) 在所有 I/O 完成後被調用,以釋放文件描述符 fd 及其關聯的內核資源。
在深入探討這些系統調用之前,我們先通過清單4‑1展示它們的使用示例。這個程序是cp(1)命令的一個簡單版本。它將第一個命令行參數指定的現有文件內容複製到第二個命令行參數指定的目標文件中。
我們可以使用列表 4‑1 中的程序,如下所示:$ ./copy oldfile newfile;
//Listing 4-1: Using I/O system calls
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– fileio/copy.c
#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
#ifndef BUF_SIZE /* Allow "cc -D" to override definition */
#define BUF_SIZE 1024
#endif
int
main(int argc, char *argv[])
{
int inputFd, outputFd, openFlags;
mode_t filePerms;
ssize_t numRead;
char buf[BUF_SIZE];
if (argc != 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s old-file new-file\n", argv[0]);
/* Open input and output files */
inputFd = open(argv[1], O_RDONLY);
if (inputFd == -1)
errExit("opening file %s", argv[1]);
openFlags = O_CREAT | O_WRONLY | O_TRUNC;
filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
S_IROTH | S_IWOTH; /* rw-rw-rw- */
outputFd = open(argv[2], openFlags, filePerms);
if (outputFd == -1)
errExit("opening file %s", argv[2]);
/* Transfer data until we encounter end of input or an error */
while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
if (write(outputFd, buf, numRead) != numRead)
fatal("couldn't write whole buffer");
if (numRead == -1)
errExit("read");
if (close(inputFd) == -1)
errExit("close input");
if (close(outputFd) == -1)
errExit("close output");
exit(EXIT_SUCCESS);
}
//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– fileio/copy.c
I/O的普遍性
UNIX I/O 模型的顯著特點之一是其 I/O 的普適性概念。這意味着相同的四個系統調用——open()、read()、write() 和 close()——被用於對各種類型的文件進行I/O 操作,包括終端等設備。因此,如果我們僅使用這些系統調用編寫程序,該程序將能在任何類型的文件上運行。例如,以下都是 Listing 4‑1 中程序的合法用法:
$ ./copy test test.old Copy a regular file
$ ./copy a.txt /dev/tty Copy a regular file to this terminal
$ ./copy /dev/tty b.txt Copy input from this terminal to a regular file
$ ./copy /dev/pts/16 /dev/tty Copy input from another terminal
I/O 的通用性是通過確保每個文件系統與設備驅動程序都實現相同的 I/O 系統調用集來實現的。由於特定於文件系統或設備的細節在內核內部處理,因此在編寫應用程序時,我們可以通常忽略特定於設備的因素。當需要訪問文件系統或設備的特定功能時,程序可以使用萬能的 ioctl() 系統調用(第 4.8 節),它為超出通用 I/O模型的功能提供了接口。
打開文件 open()
open() 系統調用要麼打開一個現有文件,要麼創建並打開一個新文件。
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, ... /* mode_t mode */);
// Returns file descriptor on success, or –1 on error
要打開的文件由路徑名參數標識。如果路徑名是一個符號鏈接,則會對其進行解析。成功時,open() 返回一個文件描述符,該描述符用於後續的系統調用中引用該文件。如果發生錯誤,open() 返回 –1,並且 errno 會相應地設置。
標誌參數是一個位掩碼,用於指定文件的訪問模式,其使用表 4‑2 中所示的常量之一。
早期的UNIX實現使用數字0、1和2,而不是表4‑2中所示的名字。大多數現代的UNIX實現定義這些常量具有這些值。因此,我們可以看到O_RDWR 不等於O_RDONLY | O_WRONLY;後者的組合是一個邏輯錯誤。
當使用 open() 創建新文件時,模式位掩碼參數指定要應用於文件的權限。(用於類型化模式的模式_t 數據類型是在 SUSv3 中指定的一種整數類型。)如果open() 調用沒有指定 O_CREAT,則可以省略模式
表4‑2:文件訪問模式
|
訪問模式
|
描述
|
|
O_RDONLY
|
以只讀方式打開文件
|
|
O_WRONLY
|
以只寫方式打開文件
|
|
O_RDWR
|
以讀寫方式打開文件
|
我們在第15.4節詳細描述了文件權限。稍後,我們會看到,實際上應用於新文件的權限不僅取決於模式參數,還取決於進程umask(第 15.4.6節)以及父目錄的(可選存在的)默認訪問控制列表(第 17.6節)。在此期間,我們只需指出,模式參數可以指定為一個數字(通常用八進制表示),或者最好通過按位或運算(|)將表 15‑4(第295頁)中列出的零個或多個位掩碼常量組合在一起。
列表 4‑2 顯示了 open() 的使用示例,其中一些示例使用了額外的標誌位,我們稍後將進行描述。
//列表 4‑2:open() 的使用示例 open()
/* Open existing file for reading */
fd = open("startup", O_RDONLY);
if (fd == -1)
errExit("open");
/* Open new or existing file for reading and writing, truncating to zero
bytes; file permissions read+write for owner, nothing for all others */
fd = open("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open");
/* Open new or existing file for writing; writes should always
append to end of file */
fd = open("w.log", O_WRONLY | O_CREAT | O_TRUNC | O_APPEND,
S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open");
文件描述符返回的數字open()
SUSv3 規定,如果 open() 成功,它將保證使用進程中最小編號的未使用文件描述符。我們可以使用此功能來確保文件使用特定的文件描述符打開。例如,以下序列確保文件使用標準輸入(文件描述符 0)打開。
if (close(STDIN_FILENO) == -1) /* Close file descriptor 0 */
errExit("close");
fd = open(pathname, O_RDONLY);
if (fd == -1)
errExit("open");
由於文件描述符 0 是未使用的,open() 保證會使用該描述符打開文件。在 5.5 節中,我們探討了 dup2() 和 fcntl() 的使用,以實現類似的結果,但可以更靈活地控制所使用的文件描述符。在該節中,我們還展示了控制文件打開的文件描述符為何有用的一個示例。
open() 標誌參數
表 4‑3 總結了可以在 flags 中進行按位 OR 運算 (|) 的所有常量集。最後一列指示了這些常量中哪些在 SUSv3 或 SUSv4 中被標準化。
|
Flag
|
目的
|
SUS?
|
|
O_RDONLY
|
僅打開用於讀取
|
v3
|
|
O_WRONLY
|
僅用於寫入打開
|
v3
|
|
O_RDWR
|
打開用於讀寫
|
v3
|
|
O_CLOEXEC
|
設置 close‑on‑exec 標誌(自 Linux 2.6.23)
|
v4
|
|
O_CREAT
|
如果文件不存在,則創建文件
|
v3
|
|
O_DIRECT
|
文件 I/O 繞過緩衝區緩存
|
—
|
|
O_DIRECTORY
|
如果路徑名不是目錄則失敗
|
v4
|
|
O_EXCL
|
使用 O_CREAT: 以獨佔方式創建文件
|
v3
|
|
O_LARGEFILE
|
用於 32 位系統打開大文件
|
—
|
|
O_NOATIME
|
不要在 read () 中更新文件最後訪問時間(自 Linux 2.6.8 起)
|
—
|
|
O_NOCTTY
|
不要讓 pathname 成為控制終端
|
v3
|
|
O_NOFOLLOW
|
不要解引用符號鏈接
|
v4
|
|
O_TRUNC
|
截斷現有文件到零長度
|
v3
|
|
O_APPEND
|
寫入操作總是追加到文件末尾
|
v3
|
|
O_ASYNC
|
當 I/O 可能時生成信號
|
—
|
|
O_DSYNC
|
提供同步 I/O 數據完整性(自 Linux 2.6.33 起)
|
v3
|
|
O_NONBLOCK
|
以非阻塞模式打開
|
v3
|
|
O_SYNC
|
使文件寫入同步
|
v3
|
表 4‑3 中的常量分為以下幾組:
- 文件訪問模式標誌:這些是前面描述的 O_RDONLY、 O_WRONLY 和 O_RDWR 標誌。它們可以使用 fcntl() F_GETFL 操作(第 5.3 節)來檢索。
- 文件創建標誌:這些是表 4‑3 第二部分中顯示的標誌。它們控制 open() 調用的行為各個方面,以及後續 I/O 操作的選項。這些標誌不能被檢索或更改。
- 打開文件狀態標誌:這些是表 4‑3 中的剩餘標誌。它們可以使用 fcntl() F_GETFL 和 F_SETFL 操作(第5.3 節)來檢索和修改。這些標誌有時也簡單地稱為文件狀態標誌。
自內核 2.6.22 起,可讀取 /proc/PID/fdinfo 目錄中的 Linux 特定文件,以獲取系統上任何進程的文件描述符信息。該目錄中有一個文件對應進程的每個已打開文件描述符,其名稱與描述符編號相匹配。該文件中的 pos 字段顯示當前文件偏移量(第 4.7 節)。flags 字段是一個八進制數,顯示文件訪問模式標誌和打開文件狀態標誌。(要解碼此數字,我們需要查看 C 庫頭文件中這些標誌的數值。)
- O_ASYNC 在由 open() 返回的文件描述符上 I/O 成為可能時生成信號。此功能,稱為信號驅動 I/O,僅適用於某些文件類型,例如終端、FIFO 和套接字。在 Linux 上,在調用 open() 時指定O_ASYNC 標誌沒有任何效果。要啓用信號驅動 I/O,我們必須改用 fcntl()F_SETFL 操作(第 5.3 節)來設置此標誌。
- O_CLOEXEC 啓用新文件描述符的close‑on‑exec標誌(FD_CLOEXEC)。使用O_CLOEXEC 標誌允許程序避免額外的fcntl() F_SETFD 和F_SETFD 操作來設置close‑on‑exec標誌。在多線程程序中,它也是必要的,以避免使用後一種技術時可能出現的競態條件。這些競態條件可能發生在以下情況:一個線程打開文件描述符,然後嘗試在另一個線程執行fork()並隨後執行任意程序的exec()的同時將其標記為close‑on‑exec。(假設第二線程在第一個線程打開文件描述符並使用fcntl()設置close‑on‑exec標誌的同一時間,成功地在fork()和exec()之間完成了操作。)這樣的競態條件可能導致打開的文件描述符被無意中傳遞給不安全的程序。(我們將在第 5.1節中更多地討論競態條件。)
- O_CREAT 如果文件尚不存在,則將其創建為一個新的空文件。此標誌即使文件僅用於讀取也有效。如果我們指定 O_CREAT,則必須在 open() 調用中提供模式參數;否則,新文件的權限將被設置為來自堆棧的某個隨機值。
- O_DIRECT 允許文件 I/O 繞過緩衝區緩存。此功能在 13.6 節中描述。必須定義 _GNU_SOURCE 功能測試宏,以便從 <fcntl.h> 提供此常量定義。
- O_DIRECTORY 返回錯誤(errno 等於 ENOTDIR),如果 pathname 不是一個目錄。這個標誌是一個擴展功能,專門為實現 opendir()(第 18.8 節)而設計。必須定義 _GNU_SOURCE 功能測試宏,才能從 <fcntl.h> 中使用這個常量定義。
- O_DSYNC (自 Linux 2.6.33 起) 根據同步 I/O 數據完整性完成的同步要求執行文件寫入。參見第 13.3 節關於內核 I/O 緩衝區的討論。
- O_EXCL 此標誌與 O_CREAT 一起使用,用於指示如果文件已存在,則不應打開;相反,open() 應失敗,並將 errno 設置為 EEXIST。換句話説,此標誌允許調用者確保它是創建文件的過程。存在檢查和文件創建是原子執行的。我們在第 5.1 節討論原子性的概念。當在 flags 中指定 O_CREAT 和 O_EXCL 時,如果路徑名是符號鏈接,open() 會失敗(錯誤碼為 EEXIST)。SUSv3 要求此行為,以便特權應用程序可以在已知位置創建文件,而不會出現符號鏈接導致文件創建在不同的位置(例如,系統目錄),這會帶來安全影響。
- O_LARGEFILE 以支持大文件的方式打開文件。此標誌用於 32 位系統以處理大文件。儘管它未在 SUSv3 中指定,但 O_LARGEFILE 標誌在其他幾個 UNIX 實現中也可用。
- O_NOATIME (自 Linux 2.6.8 起)不要在從該文件讀取時更新文件最後訪問時間(即第15.1節中描述的st_atime字段)。要使用此標誌,調用進程的有效用户ID必須與文件的所有者匹配,或者進程必須具有特權(CAP_FOWNER);否則,open()函數會以錯誤EPERM失敗。(實際上,對於非特權進程,在以 O_NOATIME 標誌打開文件時,必須匹配的是進程的文件系統用户ID,而不是其有效用户ID,如第9.5節所述。) 這個標誌是非標準的Linux擴展。為了從 <fcntl.h> 暴露其定義,我們必須定義_GNU_SOURCE 功能測試宏。O_NOATIME 標誌是為索引和備份程序使用的。它的使用可以顯著減少磁盤活動量,因為在讀取文件內容以及更新文件i節點中的最後訪問時間時(第14.4節),不需要在磁盤上反覆來回尋道。使用MS_NOATIME mount() 標誌(第14.8.1節)和 FS_NOATIME_FL 標誌(第15.5節)可以獲得類似 O_NOATIME 的功能。
- O_NOCTTY 如果正在打開的文件是終端設備,則阻止其成為控制終端。控制終端在34.4節中討論。如果正在打開的文件不是終端,則此標誌無效。
- O_NOFOLLOW 通常情況下,open() 會解析符號鏈接的路徑名。但是,如果指定了O_NOFOLLOW 標誌,那麼當路徑名是符號鏈接時,open() 會失敗(並將errno 設置為 ELOOP)。這個標誌很有用,尤其是在特權程序中,用於確保open() 不會解析符號鏈接。為了從 <fcntl.h> 暴露這個標誌的定義,我們必須定義 _GNU_SOURCE 功能測試宏。
open() 引發的錯誤
如果嘗試打開文件時發生錯誤,open() 會返回 –1,並且 errno 會標識錯誤的根本原因。以下是一些可能發生的錯誤(除了在描述 flags 參數時已經提到的那些):
- EACCES 文件權限不允許調用進程以 flags 指定的模式打開文件。或者,由於目錄權限,文件無法訪問,或者文件不存在且無法創建。
- EISDIR 指定的文件是一個目錄,調用者嘗試以寫入方式打開它。這是不允許的。(另一方面,有時以讀取方式打開目錄是有用的。我們在第18.11節中考慮一個示例。)
- EMFILE 已達到打開文件描述符數量的進程資源限制(RLIMIT_NOFILE,如第36.3節所述)。
- ENOENT 指定的文件不存在,並且O_CREAT 未指定,或 O_CREAT已指定,並且路徑名中的某個目錄不存在或是一個指向不存在的路徑名的符號鏈接(懸空鏈接)。
- EROFS指定的文件位於只讀文件系統上,調用者嘗試以寫入方式打開它。
- ETXTBSY指定的文件是一個可執行文件(程序),它目前正在執行。不允許修改(即以寫入方式打開)與正在運行的程序關聯的可執行文件。(我們必須首先終止程序才能修改可執行文件。)
當後來我們描述其他系統調用或庫函數時,通常不會以上述方式列出可能發生的錯誤範圍。(此類列表可以在每個系統調用或庫函數的相應手冊頁中找到。)我們在此這樣做有兩個原因。其中之一是 open() 是我們詳細描述的第一個系統調用,上述列表説明系統調用或庫函數可能因多種原因而失敗。其次,open() 可能失敗的具體原因本身就是一個有趣的列表,説明了在文件被訪問時起作用的多個因素和檢查。(上述列表不完整:參見 open(2) 手冊頁以獲取 open() 可能失敗的其他原因。)
create() 系統調用
在早期的UNIX實現中,open()函數只有兩個參數,不能用來創建新文件。相反,使用creat()系統調用可以創建並打開新文件。
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
// 返回文件描述符,出錯時返回 –1
creat() 返回一個文件描述符,該描述符可以在後續的系統調用中使用。調用creat() 等同於以下 open() 調用:
fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
因為 open() 的 flags 參數提供了更大的控制權來決定如何打開文件(例如,我們可以指定O_RDWR 而不是 O_WRONLY),所以 creat() 現在已經過時了,儘管它可能仍然出現在較舊的程序中。
read() 從文件讀取
系統調用 read() 從由描述符 fd 指向的打開文件中讀取數據。
#include <unistd.h>
ssize_t read(int fd, void *buffer, size_t count);
// 返回讀取的字節數,EOF時返回0,出錯時返回–1
讀取的計數參數指定了要讀取的最大字節數。(size_t數據類型是一種無符號整數類型。)緩衝區參數提供了輸入數據要放置的內存緩衝區的地址。此緩衝區必須至少有count字節長。
系統調用不為其返回信息給調用者而分配緩衝區的內存。相反,我們必須傳遞一個指向先前已分配的正確大小內存緩衝區的指針。這與一些庫函數形成對比,
這些庫函數確實會分配內存緩衝區以返回信息給調用者。
一個成功的 read() 調用會返回實際讀取的字節數,如果遇到文件末尾則返回 0。出錯時,會返回通常的 –1。ssize_t 數據類型是一種有符號整數類型,用於保存字節數或 –1 錯誤指示。
對 read() 的調用可能會讀取少於請求的字節數。對於普通文件,這可能是由於我們接近文件末尾。
當 read() 應用於其他類型的文件——例如管道、FIFO、套接字或終端——也存在多種情況,可能導致它讀取的字節數少於請求的字節數。例如,默認情況下,從終端的 read() 只讀取到下一個換行 (\n) 字符。我們將在後續章節中涵蓋其他文件類型時考慮這些情況。
使用 read() 從終端等輸入一系列字符,我們可能會期望以下代碼能正常工作:
#define MAX_READ 20
char buffer[MAX_READ];
if (read(STDIN_FILENO, buffer, MAX_READ) == -1)
errExit("read");
printf("The input data was: %s\n", buffer);
這段代碼的輸出很可能很奇怪,因為它可能會包含除了實際輸入的字符串之外的字符。這是因為 read() 不將終止空字符放在 printf() 被要求打印的字符串的末尾。稍加思考,我們就會意識到這一定是這樣的,因為 read() 可以用來從文件中讀取任意序列的字節。在某些情況下,這些輸入可能是文本,但在其他情況下,輸入可能是二進制整數或以二進制形式表示的 C 結構。read() 沒有辦法區分這些,因此它無法顧及 C 語言中空字符終止字符串的約定。如果輸入緩衝區的末尾需要終止空字符,我們必須顯式地放在那裏:
char buffer[MAX_READ + 1];
ssize_t numRead;
numRead = read(STDIN_FILENO, buffer, MAX_READ);
if (numRead == -1)
errExit("read");
buffer[numRead] = '\0';
printf("The input data was: %s\n", buffer);
因為終止空字節需要一個字節的內存,所以緩衝區的大小必須至少比我們預期的最大字符串大一個。
write() 寫入文件
write() 系統調用將數據寫入一個已打開的文件。
#include <unistd.h>
ssize_t write(int fd, void *buffer, size_t count);
//返回寫入的字節數,出錯時返回 –1
write() 的參數與 read() 類似:buffer 是要寫入數據的地址;count 是從buffer 中要寫入的字節數;fd 是一個文件描述符,指向要寫入數據的目標文件。
在成功的情況下,write() 返回實際寫入的字節數;這可能小於 count。對於磁盤文件,導致這種部分寫入的可能原因是磁盤已滿或進程資源限制(文件大小)已達到。(相關的限制是 RLIMIT_FSIZE,在 36.3 節中描述。)
當對磁盤文件執行 I/O 操作時,write() 的成功返回並不能保證數據已傳輸到磁盤,因為內核對磁盤 I/O 進行緩衝處理,以減少磁盤活動並加速 write() 調用。我們在第 13 章中討論了詳細信息。
close() 關閉文件
系統調用 close() 關閉一個已打開的文件描述符,使其可以被進程後續重用。當進程終止時,其所有已打開的文件描述符都會自動關閉。
#include <unistd.h>
int close(int fd);
// 返回成功時為 0,出錯時為 –1
通常建議顯式關閉不需要的文件描述符,因為這樣做可以使我們的代碼在面對後續修改時更加易讀和可靠。此外,文件描述符是一種消耗型資源,如果未關閉文件描述符,可能會導致進程耗盡描述符。在編寫處理多個文件的長生命程序時(例如shell 或網絡服務器),這是一個特別重要的問題。就像每一個其他系統調用一樣,對 close() 的調用應該用錯誤檢查代碼括起來,如下所示:if (close(fd) == -1) errExit("close");
這會捕獲諸如嘗試關閉未打開的文件描述符或兩次關閉同一文件描述符等錯誤,以及捕獲特定文件系統在關閉操作期間可能診斷的錯誤條件。
NFS(網絡文件系統)提供了一個特定於文件系統的錯誤示例。如果發生NFS提交失敗,即數據未到達遠程磁盤,則此錯誤會傳播到應用程序,表現為close()調用失敗。
lseek() 改變文件偏移量
對於每個打開的文件,內核記錄一個文件偏移量,有時也稱為讀寫偏移量或指針。這是下一次 read() 或 write() 將開始的文件位置。文件偏移量表示相對於文件開頭的順序字節位置。文件的第一字節位於偏移量 0。文件偏移量在文件打開時被設置為指向文件的開始,並且每次調用 read() 或write() 時會自動調整,以便指向剛剛讀取或寫入的字節之後的下一個字節。因此,連續的 read() 和 write() 調用會順序地遍歷文件。
lseek() 系統調用根據 offset 和 whence 中指定的值調整由文件描述符 fd 指向的打開文件的文件偏移量。
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//若成功則返回新的文件偏移量,否則返回–1
偏移量 offset 參數指定一個以字節為單位的值。(off_t數據類型是由SUSv3指定的有符號整數類型。)whence 參數指示從哪個基點解釋偏移量,其值如下:
- SEEK_SET 文件偏移量是從文件開頭偏移 offset 字節的位置
- SEEK_CUR 文件偏移量相對於當前文件偏移量調整 offset 字節
- SEEK_END 文件偏移量設置為文件大小加上offset
換句話説,offset 是相對於文件最後一個字節之後的下一個字節來解釋的.
如果 whence 是 SEEK_CUR 或 SEEK_END,offset 可以是負數或正數;對於 SEEK_SET,offset 必須是非負數。
lseek() 成功的返回值是新的文件偏移量。以下調用在不改變它的情況下檢索文件偏移量的當前位置:curr = lseek(fd, 0, SEEK_CUR);
某些 UNIX 實現(但不是 Linux)有非標準的 tell(fd) 函數,它和上面提到的lseek() 調用具有相同的功能。
這裏是一些 lseek() 調用的其他示例,以及指示文件偏移量移動到哪裏的註釋:
lseek(fd, 0, SEEK_SET); /* Start of file */
lseek(fd, 0, SEEK_END); /* Next byte after the end of the file */
lseek(fd, -1, SEEK_END); /* Last byte of file */
lseek(fd, -10, SEEK_CUR); /* Ten bytes prior to current location */
lseek(fd, 10000, SEEK_END); /* 10001 bytes past last byte of file */
調用 lseek() 只是調整內核中與文件描述符關聯的文件偏移量的記錄。它不會導致任何物理設備訪問。我們在第 5.4 節中描述了文件偏移量、文件描述符和打開文件之間關係的一些進一步細節。
我們不能對所有類型的文件使用 lseek()。對管道、FIFO、套接字或終端應用 lseek()是不被允許的;lseek() 會失敗,並將 errno 設置為 ESPIPE。另一方面,可以將 lseek() 應用於那些這樣做是有意義的設備。例如,可以定位磁盤或磁帶設備上的指定位置。
文件空洞
如果程序 seeks 到文件末尾之後,然後執行 I/O 會怎樣?對 read() 的調用將返回0,表示文件已結束。有點令人驚訝的是,可以在文件末尾任意位置寫入字節。
文件的前一個結束位置和新建字節之間的空間被稱為文件空洞。從編程的角度來看,空洞中的字節是存在的,從空洞中讀取會返回一個包含0(空字節)的字節緩衝區。文件空洞並不會佔用任何磁盤空間。文件系統不會為空洞分配任何磁盤塊,直到某個後續時間點數據被寫入其中。文件空洞的主要優勢在於,一個稀疏填充的文件比如果空字節實際上需要在磁盤塊中分配所要求的磁盤空間要少。核心轉儲文件(第22.1節)是包含大量空洞的文件的常見示例。
文件空洞不佔用磁盤空間的説法需要稍加限定。在大多數文件系統中,文件空間以塊為單位分配(第14.3節)。塊的大小取決於文件系統,但通常是1024、2048或4096字節等。如果空洞的邊緣落在塊內而不是塊邊界上,那麼會為塊中的其他部分數據分配整個塊,而對應空洞的部分則用空字節填充。
孔洞的存在意味着一個文件的標稱大小可能大於它實際使用的磁盤存儲空間(在某些情況下,可能大得多)。將字節寫入文件孔洞中間會減少可用磁盤空間,因為內核會分配數據塊來填充孔洞,儘管文件的大小不會改變。這種情況不常見,但仍然需要了解。
SUSv3 規定了一個函數,posix_fallocate(fd, offset, len),該函數確保為磁盤文件(由描述符 fd 指向)在磁盤上分配 offset 和 len 指定的字節範圍內的空間。這允許應用程序確保後續對文件的 write() 操作不會因為磁盤空間耗盡而失敗(這種情況可能發生在文件中的空洞被填充,或者其他應用程序消耗了磁盤空間時發生)。歷史上,glibc 對此函數的實現通過向指定範圍內的每個塊寫入一個 0 字節來實現預期效果。自版本 2.6.23 以來,Linux 提供了一個fallocate() 系統調用,它提供了一種更高效的方式來確保所需空間被分配,並且當該系統調用可用時,glibc 的 posix_fallocate() 實現會使用該系統調用。
第14.4節描述了文件中如何表示空洞,而第 15.1節描述了stat()系統調用,該調用可以告訴我們文件當前的大小,以及實際分配給文件的數據塊數量。
示例程序
通用的 I / O 模式示例程序Listing 4‑3 演示了 lseek() 與 read() 和write() 的結合使用。此程序的第一個命令行參數是要打開的文件名。其餘參數指定要對該文件執行的 I/O 操作。每個操作由一個字母后跟一個相關值組成(沒有分隔空間):
- soffset:從文件開頭跳轉到字節數偏移量處
- rlength:從當前文件偏移量開始讀取長度字節,並以文本形式顯示
- rlength:從當前文件偏移量開始,讀取長度字節,並以十六進制形式顯示
- wstr:將str中指定的字符序列寫入當前文件偏移量處
//清單 4‑3: read() 的演示, write() ,和 lseek()
//––––––––––––––––––––––––––––––––––––––––––––––––––––––––– fileio/seek_io.c
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
size_t len;
off_t offset;
int fd, ap, j;
char *buf;
ssize_t numRead, numWritten;
if (argc < 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s file {r<length>|R<length>|w<string>|s<offset>}...\n", argv[0]);
fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); /* rw-rw-rw- */
if (fd == -1)
errExit("open");
for (ap = 2; ap < argc; ap++) {
switch (argv[ap][0]) {
case 'r': /* Display bytes at current offset, as text */
case 'R': /* Display bytes at current offset, in hex */
len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
buf = malloc(len);
if (buf == NULL)
errExit("malloc");
numRead = read(fd, buf, len);
if (numRead == -1)
errExit("read");
if (numRead == 0) {
printf("%s: end-of-file\n", argv[ap]);
} else {
printf("%s: ", argv[ap]);
for (j = 0; j < numRead; j++) {
if (argv[ap][0] == 'r')
printf("%c", isprint((unsigned char) buf[j]) ?
buf[j] : '?');
else
printf("%02x ", (unsigned int) buf[j]);
}
printf("\n");
}
free(buf);
break;
case 'w': /* Write string at current offset */
numWritten = write(fd, &argv[ap][1], strlen(&argv[ap][1]));
if (numWritten == -1)
errExit("write");
printf("%s: wrote %ld bytes\n", argv[ap], (long) numWritten);
break;
case 's': /* Change file offset */
offset = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
if (lseek(fd, offset, SEEK_SET) == -1)
errExit("lseek");
printf("%s: seek succeeded\n", argv[ap]);
break;
default:
cmdLineErr("Argument must start with [rRws]: %s\n", argv[ap]);
}
}
exit(EXIT_SUCCESS);
}
//––––––––––––––––––––––––––––––––––––––––––––––––––––––––– fileio/seek_io.c
以下 shell 會話日誌演示了 Listing 4‑3 中程序的使用,展示了當我們嘗試從文件空洞中讀取字節時會發生什麼:
$ touch tfile # Create new, empty file
$ ./seek_io tfile s100000 wabc # Seek to offset 100,000, write “abc”
s100000: seek succeeded
wabc: wrote 3 bytes
$ ls -l tfile # Check size of file
-rw-r--r-- 1 mtk users 100003 Feb 10 10:35 tfile
$ ./seek_io tfile s10000 R5 # Seek to offset 10,000, read 5 bytes from hole
s10000: seek succeeded
R5: 00 00 00 00 00 # Bytes in the hole contain 0
超越通用IO模型的操作 ioctl()
ioctl() 系統調用是一個通用的機制,用於執行本章前面描述的通用 I/O 模型之外的文件和設備操作。
#include <sys/ioctl.h>
int ioctl(int fd, int request, ... /* argp */);
//返回值取決於請求,或在出錯時為 ‑1
fd 參數是用於在由 request 參數指定的控制操作上執行的設備或文件的打開文件描述符。設備特定的頭文件定義了可以傳遞在 request 參數中的常量。如標準 C 省略號(...)表示法所示,ioctl() 的第三個參數(我們標記為argp)可以是任何類型。請求參數的值使 ioctl() 能夠確定在 argp 中期望哪種類型的值。通常,argp 是指向整數或結構的指針;在某些情況下,它未被使用。我們將看到 ioctl() 在後面的章節中的多個用法(例如,參見第 15.5節)。
總結
為了對普通文件執行 I/O,我們首先必須使用 open() 獲取一個文件描述符。然後使用 read() 和 write() 執行 I/O。完成所有 I/O 後,我們應該使用 close() 釋放文件描述符及其相關資源。這些系統調用可用於對各種類型的文件執行 I/O。
所有文件類型和設備驅動程序都實現相同的 I/O 接口,這使得 I/O 具有通用性,意味着一個程序通常可以用於任何類型的文件,而無需特定於文件類型的代碼。對於每個打開的文件,內核維護一個文件偏移量,該偏移量決定了下一次讀取或寫入將發生的位置。文件偏移量會隱式地隨着讀取和寫入操作而更新。通過使用lseek(),我們可以顯式地將文件偏移量重新定位到文件內的任何位置或文件末尾之後的位置。在文件之前末尾的位置寫入數據會在文件中創建一個空洞。從文件空洞讀取將返回包含零字節的字節。
ioctl()系統調用是針對不符合標準文件I/O模型的設備和文件操作的一個總稱。
練習
tee 命令讀取其標準輸入直到文件結束,將輸入的副本寫入標準輸出和其命令行參數指定的文件名。 (我們在第 44.7 節討論 FIFO 時會展示這個命令的使用示例。)使用 I/O系統調用實現 tee。默認情況下,tee 會覆蓋具有給定名稱的任何現有文件。實現 –a 命令行選項(tee –a file),它使 tee 如果文件已存在,則將文本追加到文件的末尾。(有關用於解析命令行選項的 getopt() 函數的描述,請參閲附錄 B。)
編寫一個類似cp的程序,當用於複製包含空洞(空字節序列)的普通文件時,也會在目標文件中創建相應的空洞。