1.文件描述符
所有執行I/O操作的系統調用都以文件描述符(一個非負整數)來指代打開的文件。文件描述符用以表示所有類型的已打開文件,包括管道、FIFO、socket、終端、設備、普通文件。它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。每個進程,文件描述符都自成一套。
標準流(標準文件描述符)
3中標準的文件描述符:
當linux啟動后,會自動打開三個文件,就是標準輸入、標準輸出、標準錯誤。標準輸入流默認是鍵盤,標準輸出流默認是終端,向錯誤流寫數據,終端的默認做法是打印出錯誤內容,當然這些流可以更改的。
-
fprintf(stdout, "input someting") <=> printf("input someting")
- 向標準輸出流(終端程序)輸出一個字符串
-
fscanf(stdin, "%d",&a) <=> scanf("%d", &a)
- 向標準輸入流(鍵盤)讀入一個數據
-
fprintf(stderr, "a error occur")
- 向標準錯誤流寫入一個錯誤信息
重定向標準流
-
./demo.out 1>>a.txt
輸出流重定向- 將1代表的標準輸出流重定向(>>)到a.txt文件
-
./demo.out 1>>a.txt
等價于./demo.out >>a.txt
-
./demo.out >>a.txt
輸出流中的內容是追加的,追加到結尾 -
./demo.out >a.txt
輸出流中的內容是覆蓋的,再次寫入會覆蓋之前的內容
-
./demo.out <a.txt
輸入流重定向
2.I/O模型
I/O的4個主要系統調用:
-
fd = open(pathname,flags,mode)
打開或創建一個新文件- flags標志
- image
- image
- 位掩碼參數mode指定了新創建文件的權限,若open()并未指定O_CREAT標志,則忽略該參數
- S_IRUSER
- S_IWUSER
- 返回文件描述符值
- SUSv3規定,如果open()成功,必須保證其返回值為進程未用文件描述符中數值最小者,如果文件描述符0未使用,那么open一定會使用此文件描述符打開文件。
- 錯誤處理
- open()返回-1,錯誤號errno標識錯誤原因
- EACCES
- EISDIR
- EMFILE
- ENFILE
- ENOENT
- EROFS
- ETXTBSY
- flags標志
-
numread = read(fd,buffer,count)
讀取fd所指代的文件中之多count字節的數據,并存儲到buffer中- count參數指定最多能讀取的字節數
- buffer參數提供用來存放輸入數據的內存緩存地址
- 返回
- 遇到文件結束(EOF)則返回0
- 出錯返回 -1
- 正確返回存放讀取的字節數
numwritten = write(fd,buffer,count)
-
status = close(fd)
- 文件描述符屬于有限資源,因此文件描述符關閉失敗可能會導致一個進程將文件描述符資源消耗殆盡。
3.改變文件偏移量:lseek()
off_t lseek(int fd, off_t offset, int whence)
- offset參數指定了一個以字節為單位的數值
- whence參數則表明贏參照哪個基點來解釋offset參數,應為下列其中之一:
- SEEK_SET:文件頭部開始
- SEEK_CUR:當前文件偏移量處
- SEEK_END:文件結尾
- image
4.通用I/O模型以外的操作:ioctl()、fcntl()
ioctl()
ioctl()系統調用又為執行文件和設備操作提供了一種多用途機制。
-
int ioctl(int fd, int request,...);
- request指定了將在fd上執行的控制操作
- 第三個參數...(argp)可以是任意數據類型,根據request的參數值來確定argp所期望的類型。通常情況,argp指向整數或結構的指針
fcntl()
fcntl()系統調用對一個打開的文件描述符執行一些列控制操作
-
int fcntl(intn fd, int cmd, ...)
- cmd參數所支持的操作范圍很廣
5.原子操作和競爭條件
原子操作:將某一系統調用所要完成的各個動作作為不可中斷的操作,一次性加以執行,期間不會為其他進程或線程所中斷。所有的系統調用都是以原子操作方式執行的。
舉例:當同時制定O_EXCL與O_CREAT作為open()標志位時,如果要打開已經存在的文件,就會返回一個錯誤,這提供了一種機制,對文件是否存在的檢查和創建文件屬于同一原子操作。區別于先檢查文件再創建可能會造成其他進程在這個過程中搶占資源。
O_EXCL確保調用者就是文件的創建者。
O_APPEND標志,確保多個進程在對同一文件追加數據時不會覆蓋彼此的輸出。
6.打開文件的狀態標志
獲取訪問模式和狀態標志
fcntl()的用途之一是針對一個打開的文件,獲取或修改其訪問模式和狀態標志(這些值是通過open()調用的flag參數設置的),應將fcntl()的cmd參數設置為F_GETTFL,并且獲取的標志中總是包含O_LARGEFILE標志
flags = fcntl(fd, F_GETFL);
要判斷是否包含某一標志位,只需要將flags于其相&即可。如下可以判斷文件是否以同步方式打開:
if (flags & O_SYNC)
判定文件的訪問模式稍微復雜一點,因為O_RDONLY(0) O_WRONLY(1) O_RDWR(2)這三個常量并不與打開文件狀態標志中的單個比特位對應,需使用掩碼O_ACCMODE與flag相與
accessMode = flags & O_ACCMODE
if (accessMode == O_WRONLY) {...}
修改訪問模式和狀態標志
使用fcntl()的F_SETFL來修改,允許更改的標志有:
- O_APPEND
- O_NONBLOCK
- O_NOATIME
- A_ASYNC
- O_DIRECT
適用的場景:
- 文件不是由調用程序打開的,所以無法使用open來控制這些標志(文件是3個標準描述符,這些描述符在程序啟動之前就被打開)
- 文件描述符獲取是通過open之外的系統調用,比如pipe()、socket()
修改標志位代碼如下:
flags = fcntl(fd, F_GETFL)
flags |= O_APPEND
if(fcntl(fd, F_SETFL, flags) == -1) { errExit()}
7.文件描述符和打開文件之間的關系
文件描述符和打開的文件不一定是一一對應的關系,多個文件描述符可以指向同一打開文件。這些文件描述符可在相同或不同的進程中打開。
內核維護的3個數據結構:
- 進程級的文件描述符表
- 系統級的打開文件表
- 文件系統的i-node表
針對每個進程,內核為其維護打開的文件描述符表,每一條記錄的相關信息:
- 控制文件描述符操作的一組標志
- 對打開文件句柄的引用
內核對所有打開的文件維護一個系統級的描述表格(打開文件表),并將表中各條目稱為打開文件句柄,一個打開文件句柄存儲了與一個打開文件相關的全部信息:
- 當前文件偏移量
- 打開文件時所使用的狀態標識(flags參數)
- 文件訪問模式(只讀只寫等)
- 與信號驅動I/O相關的設置
- 對該文件i-node對象的引用
文件系統會為駐留其上的所有文件建立一個i-node表:
- 文件類型(例如,普通文件、套接字、FIFO)和訪問權限
- 一個指針,指向該文件所持有的鎖的列表
- 文件的各種屬性,包括文件大小以及與不同類型操作相關的時間戳
文件描述符、打開的文件句柄、i-node的關系:
總結以下要點:
- 不同文件描述符(1和2)可以指向同一打開文件句柄,可能是通過調用dup() dup2()或fcntl()形成的
- 不同進程文件描述符可以指向同一打開文件句柄,可能調用fork()出現
- 不同的文件句柄指向同一i-node表條目,換言之,指向同一文件,可能因為每個進程各自對同一文件發起了open調用
- 兩個不同的文件描述符,若指向同一打開文件句柄,將共享同一文件偏移量
- 文件描述符標志(close_on_exec標志)為進程和文件描述符所私有
8.復制文件描述符
int dup(int oldfd)
dup()調用復制一個打開的文件描述符oldfd,并返回一個新描述符,二者都指向同一打開文件句柄。系統會保證新描述一定是編號值最低的未用文件描述符
int dup2(int oldfd, int newfd)
dup2()系統調用會為oldfd參數所指定的文件描述符創建副本,其編號由newfd參數指定,如果newfd已經打開,那么dup2會將其先關閉
newfd = fcntl(oldfd, FU_DUPFD, startfd)
該調用為oldfd創建一個副本,且將使用大于等于startfd的最小未使用值作為描述符編號。
文件描述符的正、副本之間共享同一打開文件句柄所含的文件偏移量和狀態標志,新的文件描述符有其自己一套文件描述符標志,且其close-onexec標志(FD_CLOEXEC)總是處于關閉
int dup3(int oldfd, int newfd, int flags)
dup3與dup2相同,只是增加了一個附加參數flag
9.在文件特定偏移量出的I/O:pread()和pwrite()
ssize_t pread(int fd, void *buf, size_t count, off_t offset)
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset)
相比于read()和write(),會直接設置offset參數,是一個原子操作,且性能更好
10.分散輸入和集中輸出:readv()和writev()
readv()和writev()系統調用分別實現了分散輸入和集中輸出的功能
ssize_t readv(int fd, const struct iovec *iov, int iovcnt)
ssize_t writev(int fd, const struct iovec *iov, int iovcnt)
這些系統調用并非只對單個緩沖區進行讀寫操作,而是一次即可傳輸多個緩存區的數據。數組iov定義了一組用來傳輸數據的緩沖區。iovcnt指定了iov的成員個數,iov中的數據結構:
struct iovec {
void *iov_base;
size_t iov_len;
}
下圖展示關系:
分散輸入
從文件描述符fd所指代的文件中讀取一片連續的字節,然后將其散置于iov指定的緩沖區中,這一散置動作從iov[0]開始依次填滿每個緩沖區。是原子性操作。
集中輸出
將iov所指定的緩沖區中的數據拼接起來,然后寫入fd中。
在指定offset處分散輸入和集中輸出
preadv()、pwirtev()
11.截斷文件:truncate()和ftruncate()
truncate()和ftruncate()系統調用將文件大小設置為length指定長度
int truncate(const char *pathname, off_t length)
int ftruncate(int fd, off_t length)
若長度大于length則丟棄超出部分,若小于length,則在文件尾追加一系列字節或一個文件空洞。
12.非阻塞I/O
打開文件時指定O_NONBLOCK標志的作用:
- 若open()未能立即打開文件,則返回錯誤,而非陷入阻塞
- 調用open()成功后,后續I/O操作也是非阻塞的
由于管道、FIFO、套接字、設備都支持非阻塞模式,因無法通過open()設置標志,只能通過fcntl()的F_SETFL命令來修改。
由于內核緩沖區保證了普通文件I/O陷入阻塞,故而打開普通文件會忽略O_NONBLOCK標志
13.大文件I/O
LFS規范定義了一套擴展功能,允許在32位系統中運行的進程來操作無法以32位表示的大文件。
14./dev/fd 目錄
對于每個進程,內核都提供一個特殊的虛擬目錄/dev/fd/n,n是與進程中的打開文件描述符相對應的編號。
打開/dev/fd目錄中的一個文件等同于復制相應的文件描述符:
fd = open("/dev/fd/1", O_WRONLY)
fd = dup(1)
/dev/fd實際上是一個符號鏈接,鏈接到linux所專有的/proc/self/fd目錄
15.創建臨時文件
mkstemp()、tmpfile()