UNIX 環(huán)境高級編程|文件 I/O
GitHub: https://github.com/storagezhang
Emai: debugzhang@163.com
本文為《UNIX 環(huán)境高級編程》第 3 章學習筆記
本章說明了 UNIX 系統(tǒng)提供的基本 I/O 函數。
因為 read 和 write 都在內核執(zhí)行,所有稱這些函數為不帶緩沖的 I/O 函數。在只使用 read 和 write 的情況下,我們觀察了不同的 I/O 長度對讀文件所需時間的影響。我們也觀察了許多將已寫入的數據沖洗到磁盤上的方法,以及它們對應用程序性能的影響。
在說明多個進程對同樣文件進行追加寫操作已經多個進程創(chuàng)建同一文件時,本章介紹了原子操作,也介紹了內核用來共享打開文件信息的數據結構。
3.1 引言
不帶緩沖的 I/O
術語不帶緩沖指的是每個 read 和 write 都調用內核中的一個系統(tǒng)調用。
這些不帶緩沖的 I/O 函數不是 ISO C 的組成部分,但是,它們是 POSIX.1 和 Single UNIX Specification 的組成部分。
3.2 文件描述符
對于內核而言,所有打開的文件都通過文件描述符引用。
文件描述符是一個非負整數。
當打開一個現(xiàn)有文件或創(chuàng)建一個新文件時,內核向進程返回一個文件描述符。
當讀、寫一個文件時,使用 open 或 creat 返回的文件描述符標識該文件,將其作為參數傳送給 read 或 write。
UNIX 系統(tǒng) shell 及很多應用程序的慣例(在符合 POSIX.1 的應用程序中,符號常量在頭文件
文件描述符 0 :標準輸入
STDIN_FILENO
文件描述符 1 :標準輸出
STDOUT_FILENO
文件描述符 2 :標準錯誤
STDERR_FILENO
文件描述符的變化范圍是 0 ~ OPEN_MAX-1。
3.3 函數 open 和 openat
調用 open 或 openat 函數可以打開或創(chuàng)建一個文件。
#include
將最后一個參數寫為 …,ISO C 用這種方法表明余下的參數的數量及其類型是可變的。
對于 open 函數而言,僅當創(chuàng)建新文件時才使用最后這個參數。
在函數原型中將此參數放置在注釋中。
參數:
path 參數是要打開或創(chuàng)建文件的名字
oflag 參數可用來說明此函數的多個選項,用下列一個或多個常量進行“或”運算構成 oflag 參數
O_RDONLY:只讀打開
O_WRONLY:只寫打開
O_RDWR:讀寫打開
大多數實現(xiàn)將 O_RDONLY 定義為 0,O_WRONLY 定義為 1,O_RDWR 定義為 2。
O_EXEC:只執(zhí)行打開
O_SEARCH:只搜索打開(應用于目錄)
O_SEARCH 常量的目的在于在目錄打開時驗證它的搜索權限。對目錄的文件描述符的后續(xù)操作就不需要再次檢查對該目錄的搜索權限。
上述 5 個常量中必須且只能指定一個。
O_APPEND:每次寫時都追加到文件的尾端。
O_CLOEXEC:把 FD_CLOEXEC 常量設置為文件描述符標志。
O_CREAT:若此文件不存在則創(chuàng)建它。
使用此選項時,open 函數需同時說明第 3 個參數 mode,用 mode 指定該新文件的訪問權限位。
O_DIRECTORY:如果 path 引用的不是目錄,則出錯。
O_EXCL:如果同時指定了 O_CREAT,而文件已經存在,則出錯。
用此可以測試一個文件是否存在,如果不存在,則創(chuàng)建此文件,這使測試和創(chuàng)建兩者成為一個原子操作。
O_NOCTTY:如果 path 引用的是終端設備,則不將該設備分配作為此進程的控制終端。
O_NOFOLLOW:如果 path 引用的是一個符號鏈接,則出錯。
O_NONBLOCK:如果 path 引用的是一個 FIFO、一個塊特殊文件或一個字符特殊文件,則此選項為文件的本次打開操作和后續(xù)的 I/O 操作設置為非阻塞方式。
O_SYNC:使每次 write 等待物理 I/O 操作完成,包括由該 write 操作引起的文件屬性更新所需的 I/O。
O_TRUNC:如果此文件存在,而且為只寫或讀-寫成功打開,則將其長度截斷為 0。
O_TTY_INIT:如果打開一個還未打開的終端設備,設置非標準 termios 參數值,使其符合 Single UNIX Specification。
O_DSYNC:使每次 write 要等待物理 I/O 操作完成,但是如果該寫操作并不影響讀取剛寫入的數據,則不需等待文件屬性被更新。
O_RSYNC:使每一個以文件描述符作為參數進行的 read 操作等待,直至所有對文件同一部分掛起的寫操作都完成。
上述常量是可選的。
fd 參數把 open 和 openat 函數區(qū)分開,共有 3 種可能性:
path 參數指定的是絕對路徑名。
fd 參數被忽略,openat 函數就相當于 open 函數。
path 參數指定的是相對路徑名。
fd 參數指出了相對路徑名在文件系統(tǒng)中的開始地址,fd 參數是通過打開相對路徑名所在的目錄來獲取。
path 參數指定了相對路徑名,fd 參數具有特殊值 AT_FDCWD。
路徑名在當前工作目錄中獲取,openat 函數在操作上與 open 函數類似。
...
將最后一個參數寫為 …,ISO C 用這種方法表明余下的參數的數量及其類型是可變的。對于 open 函數而言,僅當創(chuàng)建新文件時才使用最后這個參數。在函數原型中將此參數放置在注釋中。
mode:文件訪問權限
文件訪問權限常量在
S_IRUSR:用戶讀
S_IWUSR:用戶寫
S_IXUSR:用戶執(zhí)行
S_IRGRP:組讀
S_IWGRP:組寫
S_IXGRP:組執(zhí)行
S_IROTH:其他讀
S_IWOTH:其他寫
S_IXOTH:其他執(zhí)行
返回值:
成功:返回文件描述符
由 open 和 openat 函數返回的文件描述符一定是最小的未用描述符數值。
失敗:返回 -1。
openat 函數是 POSIX.1 最新版本中新增的一類函數之一,希望解決兩個問題:
讓線程可以使用相對路徑名打開目錄中的文件,而不再只能打開當前工作目錄。
同一進程中的所有線程共享相同的當前工作目錄,因此很難讓同樣進程的多個不同線程在同一時間工作在不同的目錄中。
可以避免 time-of-check-to-time-of-use(TOCTTOU)錯誤。
TOCTTOU 錯誤的基本思想是:如果有兩個基于文件的函數調用,其中第二個調用依賴于第一個調用的結果,那么程序是脆弱的。因為兩個調用并不是原子操作,在兩個函數調用之間文件可能改變了,這樣也就造成了第一個調用的結果就不再有效,使得最終的結果是錯誤的。
文件名和路徑名截斷
在 POSIX.1 中,常量 _POSIX_NO_TRUNC 決定是要截斷過長的文件名或路徑名,還是返回一個出錯。
若 _POSIX_NO_TRUNC 有效,則在整個路徑名超過 PATH_MAX,或路徑名中的任一文件名超過 NAME_MAX 時,出錯返回,并將 errno 設置為 ENAMETOOLONG。
3.4 函數 creat
可以調用 creat 函數創(chuàng)建一個新文件。
#include
參數:
path:要創(chuàng)建文件的文件名
mode:指定該文件的訪問權限
文件訪問權限常量在
S_IRUSR:用戶讀
S_IWUSR:用戶寫
S_IXUSR:用戶執(zhí)行
S_IRGRP:組讀
S_IWGRP:組寫
S_IXGRP:組執(zhí)行
S_IROTH:其他讀
S_IWOTH:其他寫
S_IXOTH:其他執(zhí)行
返回值:
若成功,返回為只寫打開的文件描述符;
若出錯,返回 -1。
creat 的一個不足之處是它以只寫方式打開所創(chuàng)建的文件。
現(xiàn)在則可用下列方式調用 open 實現(xiàn):
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
在早期的 UNIX 系統(tǒng)版本中,open 的第二個參數只能是 0、1 或 2,無法打開一個尚未存在的文件,因此需要另一個系統(tǒng)調用 creat 以創(chuàng)建新文件。現(xiàn)在,open 函數提供了選項 O_CREAT 和 O_TRUNC,于是也就不再需要單獨的 creat 函數。
3.5 close
可以調用 close 函數關閉一個打開文件。
#include
參數:
fd:待關閉文件的文件描述符。
返回值:
若成功,返回 0;
若出錯,返回 -1。
3.6 函數 lseek
每個打開文件都有一個與其相關聯(lián)的“當前文件偏移量”,它通常是一個非負整數,用以度量從文件開始處計算的字節(jié)數。通常,讀、寫操作都從當前文件偏移量處開始,并使偏移量增加所讀寫的字節(jié)數。按系統(tǒng)默認的情況,當打開一個文件時,除非指定 O_APPEND 選項,否則該偏移量被設置為 0。
可以調用 lseek 顯示地為一個打開文件設置偏移量:
#include
參數:
fd:文件的文件描述符
where:
絕對偏移量(SEEK_SET,0)
相對于當前位置的偏移量(SEEK_CUR,1)
相對文件尾端的偏移量(SEEK_END,2)
對參數 offset 的解釋與參數 where 的值有關:
若 whence 是 SEEK_SET,則將該文件的偏移量設置為距文件開始處 offset 個字節(jié)。
若 whence 是 SEEK_CUR,則將該文件的偏移量設置為其當前值加 offset,offset 可為正或負。
若 whence 是 SEEK_END,則將該文件的偏移量設置為文件長度加 offset,offset 可為正或負。
返回值:
若成功,返回新的文件偏移量;
若出錯,返回 -1。
可用下列方式確定打開文件的當前偏移量:
off_t currpos; currpos = lseek(fd, 0, SEEK_CUR);
這種方法也可用來確定所涉及的文件是否可以設置偏移量。如果文件描述符指向的是一個管道、FIFO 或網絡套接字,則 lseek 返回 -1,并將 errno 設置為 ESPIPE。
通常,文件的當前偏移量應當是一個非負整數,但是,某些設備也可能運行負的偏移量。所有在比較 lseek 的返回值時應當謹慎,不要測試它是否小于 0,而要測試它是否等于 -1。
在 Inter x86 處理器上運行的 FreeBSD 的設備 /dev/kmem 支持負的偏移量。
lseek 僅將當前的文件偏移量記錄在內核中,它并不引起任何 I/O 操作。然后,該偏移量用于下一個讀或寫操作。
文件偏移量可以大于文件的當前長度,在這種情況下,對該文件的下一次寫將加長該文件,并在文件中構成一個空洞,位于文件中但沒有寫過的字節(jié)都被讀為 0。
文件中的空洞并不要求在磁盤上占用存儲區(qū)。具體處理方式與文件系統(tǒng)的實現(xiàn)有關,當定位到超出文件尾端之后寫時,對于新寫的數據需要分配磁盤塊,但是對于原文件尾端和新開始寫位置之間的部分則不需要分配磁盤塊。
注意:
盡管可以實現(xiàn) 64 位文件偏移量,但是能否創(chuàng)建一個大于 2GB(
2
3
1
?
1
2^31-1
231?1 字節(jié))的文件則依賴于底層文件系統(tǒng)的類型。
3.7 函數 read
調用 read 函數從打開文件中讀數據。
#include
參數:
fd:打開的文件的文件描述符
buf:存放讀取內容的緩沖區(qū)的地址(手動分配)
nbytes:期望讀取的字節(jié)數
返回值:
若成功,返回讀到的字節(jié)數;若已到文件尾,返回 0;
若出錯,返回 -1。
有多種情況可使實際讀到的字節(jié)數少于要求讀的字節(jié)數:
讀普通文件時,在讀到要求字節(jié)數之前已到達了文件尾端。
當從終端設備讀時,通常一次最多讀一行。
當從網絡讀時,網絡中的緩沖機制可能造成返回值小于所要求讀的字節(jié)數。
當從管道或 FIFO 讀時,如若管道包含的字節(jié)少于所需的數量,那么 read 將只返回實際可用的字節(jié)數。
當從某些面向記錄的設備(如磁帶)讀時,一次最多返回一個記錄。
當一信號造成中斷,而已經讀了部分數據量時。
讀操作從文件的當前偏移量處開始,在成功返回之前,該偏移量將增加實際讀到的字節(jié)數。
3.8 函數 write
調用 write 函數向打開文件寫數據:
#include
參數:
fd:打開的文件的文件描述符
buf:存放待寫的數據內容的緩沖區(qū)的地址(手動分配)
nbytes:期望寫入的字節(jié)數
返回值:
若成功,返回已寫的字節(jié)數;
若出錯,返回 -1;
通常與參數 nbytes 的值相同,否則表示出錯。
出錯的一個常見原因是磁盤已寫滿,或者超過了一個給定進程的文件長度限制。
寫操作從文件的當前偏移量處開始,在成功返回之前,該偏移量將增加實際寫的字節(jié)數。
3.9 I/O 的效率
大多數文件系統(tǒng)為改善性能都采用某種預讀技術。當檢測到正進行順序讀取時,系統(tǒng)就試圖讀入比應用所要求的更多數據,并假想應用很快就會讀這些數據。
3.10 文件共享
UNIX 系統(tǒng)支持在不同的進程間共享打開文件。
內核使用 3 種數據結構表示打開文件,它們之間的關系決定了在文件共享方面一個進程對另一個進程可能產生的影響:
每個進程在進程表中都有一個記錄項,記錄項包含一張打開文件描述符表,可將其視為一個矢量,每個描述符占用一項。與每個文件描述符相關聯(lián)的是:
文件描述符標志(close_on_exec)
指向一個文件表項的指針
內核為所有打開文件維持一張文件表。每個文件表項包含:
文件狀態(tài)標志(讀、寫、添寫、同步和非阻塞等)
當前文件偏移量
指向該文件 v 節(jié)點表項的指針
每個打開文件(或設備)都有一個 v 節(jié)點(v-node)結構。v 節(jié)點包含了文件類型和對此文件進行各種操作函數的指針。對于大多數文件,v 節(jié)點還包含了該文件的 i 節(jié)點(i-node,索引節(jié)點)。這些信息是在打開文件時從磁盤上讀入內存的,所以,文件的所有相關信息都是隨時可用的。
例如,i 節(jié)點包含了文件的所有者、文件長度、指向文件實際數據塊在磁盤上所在位置的指針等。
Linux 沒有使用 v 節(jié)點,而是使用了通用 i 節(jié)點結構。雖然兩種實現(xiàn)有所不同,但在概念上,v 節(jié)點與 i 節(jié)點是一樣的。兩者都指向文件系統(tǒng)特有的 i 節(jié)點結構。
創(chuàng)建 v 節(jié)點結構的目的是對在一個計算機系統(tǒng)上的多文件系統(tǒng)類型提供支持。
如果兩個獨立進程各自打開了同一文件:
打開該文件的每個進程都獲得各自的一個文件表項,但對一個給定的文件只有一個 v 節(jié)點表項。
之所以每個進程都獲得自己的文件表項,是因為這可以使每個進程都有它自己的對該文件的當前偏移量。
操作說明:
在完成每個 write 后,在文件表項中的當前文件偏移量即增加所寫入的字節(jié)數。如果這導致當前文件偏移量超出了當前文件長度,則將 i 節(jié)點表項中的當前文件長度設置為當前文件偏移量。
如果用 O_APPEND 標志打開一個文件,則相應標志也被設置到文件表項的文件狀態(tài)標志中。每次對這種具有追加寫標志的文件執(zhí)行寫操作時,文件表項中的當前文件偏移量首先會被設置為 i 節(jié)點表項中的文件長度。這就使得每次寫入的數據都追加到文件的當前尾端處。
若一個文件用 lseek 定位到文件當前的尾端,則文件表項中的當前文件偏移量被設置為 i 節(jié)點表項中的當前文件長度(這與用 O_APPEND 標志打開文件是不同的)。
lseek 函數只修改文件表項中的當前文件偏移量,不進行任何 I/O 操作。
文件描述符標志和文件狀態(tài)標志在作用范圍方面的區(qū)別:
前者只用于一個進程的一個描述符。
后者應用于指向該給定文件表項的任何進程中的所有描述符。
3.11 原子操作
Single UNIX Specification 包括了 XSI 擴展,該擴展允許原子性地定位并執(zhí)行 I/O。pread 和 pwrite 就是這種擴展。
#include
調用 pread 相當于調用 lseek 后調用 read,但是 pread 又與這種順序調用有下列重要區(qū)別:
調用 pread 時,無法中斷其定位和讀操作。
不改變當前文件偏移量。
調用 pwrite 相當于調用 lseek 后調用 write,但也與它們有類似的區(qū)別:
調用 pwrite 時,無法中斷其定位和寫操作。
不改變當前文件偏移量。
一般而言,原子操作指的是由多步組成的一個操作。如果該操作原子地執(zhí)行,則要么執(zhí)行完所有步驟,要么一步也不執(zhí)行,不可能只執(zhí)行所有步驟的一個子集。
3.12 函數 dup 和 dup2
下面兩個函數都可用來復制一個現(xiàn)有的文件描述符:
#include
參數:
fd:被復制的文件描述符(已被打開)
fd2:指定的新文件描述符的值(待生成)
如果 fd2 已被打開,則先將其關閉。
如果 fd==fd2,則 dup2 返回 fd2,而不關閉它。
否則,fd2 的 FD_CLOEXEC 文件描述符標志就被清除,這樣 fd2 在進程調用 exec 時是打開狀態(tài)。
返回值:
若成功,返回新的文件描述符。
返回的新文件描述符一定是當前可用文件描述符中的最小數值。
返回的新文件描述符與參數 fd 共享同一個文件表項。
每個文件描述符都有它自己的一套文件描述符標志,新文件描述符的執(zhí)行時關閉(close-on-exec)標志總是由 dup 函數清除。
若出錯,返回 -1。
復制一個描述符的另一種方法是使用 fcntl 函數:
dup(fd) 等效于 fcntl (fd, F_DUPFD, 0)
dup2(fd, fd2) 等效于 close(fd2) + fcntl (fd, F_DUPFD, fd2)
這種情況并不完全等同:
dup2 是一個原子操作,而 close 和 fcntl 包括兩個函數調用。
dup2 和 fcntl 有一些不同的 errno。
3.13 函數 sync、fsync 和 fdatasync
延遲寫:
傳統(tǒng)的 UNIX 系統(tǒng)實現(xiàn)在內核中設有緩沖區(qū)高速緩存或頁高速緩存,大多數磁盤 I/O 都通過緩沖區(qū)進行。當我們向文件寫入數據時,內核通常先將數據復制到緩沖區(qū)中,然后排入隊列,晚些時候再寫入磁盤。
通常,當內核需要重用緩沖區(qū)來存放其他磁盤塊數據時,它會把所有延遲寫數據塊寫入磁盤。為了保證磁盤上實際文件系統(tǒng)與緩沖區(qū)中內容的一致性,UNIX 系統(tǒng)提供了 sync、fsync 和 fdatasync 三個函數:
#include
返回值:
若成功,返回 0;
若出錯,返回 -1。
sync 只是將所有修改過的塊緩沖區(qū)排入寫隊列,然后就返回,它并不等待實際寫磁盤操作結束。
通常,稱為 update 的系統(tǒng)守護進程周期性地調用(一般每隔 30 秒)sync 函數。
這就保證了定期沖洗內核的塊緩沖區(qū)。
命令 sync(1) 也調用 sync 函數。
fsync 函數只對由文件描述符 fd 指定的一個文件起作用,并且等待寫磁盤操作結束才返回。
fsync 可用于數據庫這樣的應用程序,這種應用程序需要確保修改過的塊立即寫到磁盤上。
fdatasync 函數類似于 fsync,但它只影響文件的數據部分。
除數據外,fsync 還會同步更新文件的屬性。
3.14 函數 fcntl
fcntl 函數可以改變已經打開文件的屬性:
#include
功能:
復制一個已有的描述符
cmd = F_DUPFD 或 F_DUPFD_CLOEXEC
獲取/設置文件描述符標志
cmd = F_GETFD 或 F_SETFD
獲取/設置文件狀態(tài)標志
cmd = F_GETFL 或 F_SETFL
獲取/設置異步 I/O 所有權
cmd = F_GETOWN 或 F_SETOWN
獲取/設置記錄鎖
cmd = F_GETLK、F_SETLK 或 F_SETLKW
參數:
cmd:
F_DUPFD
復制文件描述符 fd。
新文件描述符作為函數值返回。
新描述符與 fd 共享同一文件表項,但是,新描述符有它自己的一套文件描述符標志,其 FD_CLOEXEC 文件描述符標志被清除(這表示該描述符在 exec 時仍保持有效)。
F_DUPFD_CLOEXEC
復制文件描述符,設置與新描述符關聯(lián)的 FD_CLOEXEC 文件描述符標志的值。
返回新文件描述符。
F_GETFD
對應于 fd 的文件描述符標志作為函數值返回。
當前只定義了一個文件描述符標志 FD_CLOEXEC。
F_SETFD
對于 fd 設置文件描述符標志。
新標志按第 3 個參數(取整型值)設置。
F_GETFL
對應于 fd 的文件狀態(tài)標志作為函數值返回。
首先必須用屏蔽字 O_ACCMODE 取得訪問方式位,然后將結果與 5 個訪問方式標志相比較。
在 open 函數時描述。
F_SETFL
將文件狀態(tài)標志設置為第 3 個參數的值(取整型值)。
可以更改的標志是:
O_APPEND
O_NONBLOCK
O_SYNC
O_DSYNC
O_RSYNC
O_FSYNC
O_ASYNC
F_GETOWN
獲取當前接收 SIGIO 和 SIGURG 信號的進程 ID 或進程組 ID。
正的表示進程 ID。
負的表示進程組 ID。
F_SETOWN
設置接收 SIGIO 和 SIGURG 信號的進程 ID 或進程組 ID。
正的 arg 值定一個進程 ID,負的 arg 表示等于 arg 絕對值的一個進程組 ID。
F_GETLK
F_SETLK
F_SETLKW
arg:
一個整數
指向一個結構的指針
返回值:
若成功,則依賴于 cmd。
若出錯,返回 -1。
3.15 函數 ioctl
ioctl 函數一直是 I/O 操作的雜物箱。不能用本章中其他函數表示的 I/O 操作通常都能用 ioctl 表示。終端 I/O 是使用 ioctl 最多的地方。
#include
返回值:
若成功,返回其他值。
若出錯,返回 -1。
每個設備驅動程序可以定義它自己專用的一組 ioctl 命令,系統(tǒng)則為不同種類的設備提供通用的 ioctl 命令。
3.16 /dev/fd
/dev/fd 的目錄項是名為 0、1、2 等的文件。
打開文件 /dev/fd/n 等效于復制描述符 n(假設描述符 n 是打開的)。
在下列函數調用中:
fd = open("/dev/fd/0", mode);
大多數系統(tǒng)忽略它所指定的 mode,而另外一些系統(tǒng)則要求 mode 必須是所引用的文件初始打開時所使用的打開模式的一個子集。
Linux 實現(xiàn)中的 /dev/fd 是個例外。它把文件描述符映射成指向底層物理文件的符號鏈接。
習題
當讀/寫磁盤文件時,本章中描述的函數確實是不帶緩沖機制的嗎?請說明原因。
所有磁盤 I/O 都要經過內核的塊緩存區(qū)(也稱為內核的緩沖區(qū)高速緩存)。唯一例外的是對原始磁盤設備的 I/O,但是我們不考慮這種情況。既然 read 和 write 的數據都要被內核緩沖,那么術語“不帶緩沖的 I/O”指的是在用戶的進程中對這兩個函數不會自動緩沖,每次 read 或 write 就要進行一次系統(tǒng)調用。
Linux Unix
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。