文件描述符
對於內核而言,全部打開的文件都經過文件描述符引用。文件描述符是一個非負整數。當打開一個現有文件或建立一個新文件時,內核向進程返回一個文件描述符。當讀或寫一個文件時,使用open或creat返回的文件描述符標識該文件,將其做爲參數傳遞給read或write。
按照慣例,UNIX系統shell使用文件描述符0與進程的標準輸入相關聯,文件描述符1與標準輸出相關聯,文件描述符2與標準出錯相關聯。這是各類shell以及不少應用程序使用的慣例,而與UNIX內核無關,若是不遵照這種慣例,那麼不少UNIX系統應用程序就不能正常工做。
在依從POSIX的應用程序中,幻數0、一、2應當替換成符號常量STDIN_FILENO,
STDOUT_FILENO,
STDERR_FILENO。
這些常量都定義在頭文件<unistd.h>中。
文件描述符的變化範圍0~OPEN_MAX。早期的UNIX系統實現採用的上限值是19(容許每一個進程最多打開20個文件),但如今不少系統則將其增至63個。
OPEN函數
調用open函數能夠打開或建立一個文件。
#include<fcntl.h>
int open(const char *
pathname, int
oflag,...)
咱們將第三個參數寫爲...,ISO C用這種方法代表餘下參數的數量及其類型根據具體的調用會有所不一樣。對於open函數而言,僅當建立新文件時才使用第三個參數。在函數原型中將此參數放置在註釋中。
pathname是要打開或建立文件的名字。
oflag參數可用來講明此函數的多個選項。用下列一個或多個常量進行「或」運算構成oflag參數:
O_RDONLY //只讀打開
O_WRONLY //只寫打開
O_RDWR //讀、寫打開
在這三個常量中必須指定一個且只能指定一個。下列常數則是可選擇的。
O_APPEND //每次寫時都追加到文件的尾端
O_CREAT //若此文件不存在,則建立它。使用時,須要第三個參數
O_EXCL //若是同時指定了O_CREAT,而文件已經存在,則會出錯。用此能夠測試一個文件是否存在,若是不存在,則建立此文件,這使測試和建立者二者成爲一個原子操做。
O_TRUNC //若是此文件存在,並且爲只寫或讀寫成功打開,則將其長度截短爲0。
O_NOCTTY //若是pathname指的終端設備,則不將該設備分配做爲此進程的控制終端。
O_NONBLOCK //若是pathname指的是一個FIFO、一個塊特殊文件或一個字符特殊文件,則此選項爲文件的本次操做和後續的I/O操做設置非阻塞模式。
O_DSYNC //使每次write等待物理I/O操做完成,可是若是寫操做並不影響讀取剛寫入的數據。則不等待文件屬性被更新。
O_RSYNC //使每個以文件描述符做爲參數的read操做等待,直到任何對文件同一部分進行的未決寫操做都完成。
O_SYNC //使每次write都等到物理I/O操做完成,包括由write操做引發的文件屬性更新所需的I/O。
由open返回的文件描述符必定是最小的未用描述符數值。這一點被某些應用程序在 標準輸入,標準輸出,標準出錯輸出上打開新的文件。例如,一個應用程序能夠先關閉標準輸出(一般是文件描述符1),而後打開另外一個文件,這行打開操做前就能瞭解到該文件必定會在文件描述符1上打開。
文件名和路徑名截短
若是NAME_MAX是14,而咱們卻試圖在當前目錄中建立一個其文件名包含15個字符的新文件,此時會發生什麼哪?按照傳統,早期的系統V版本容許這種使用方法,但老是將文件名截短爲14個字符,並且不給出任何信息,而BSD類的系統則返回出錯狀態,並將errno設置爲ENAMETOOLONG。無聲無息地截短文件名會引發問題,並且他不只僅影響到建立新文件。若是NAME_MAX是14,而且存在一個其文件名剛好就是14個字符的文件,那麼以pathname做爲其參數的任意函數都沒法肯定該文件的原始名是什麼? 其緣由是這些函數沒法判斷該文件名是否被截短過。
在POSIX.1種,常量_POSX_NO_TRUNC決定了是要截短過茶國內的文件名或路徑名,仍是返回一個出錯。
creat函數
也可調用creat函數建立一個新文件
#include<fcntl.h>
int creat (const char *
pathname, mode_t
mode)
此函數至關於
open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode)
create的不足之處是它以只寫方式打開所建立的文件。在提供open的新版本以前若是要建立一個臨時文件,並要寫該文件,而後又讀該文件,則必須先調用creat、close、open。如今則可用下列方式調用open
open(pathname, O_RDWR|O_CREAT|O_TRUNC, mode)
close函數
可調用close函數關閉一個打開的文件:
#include<unistd.h>
int close(int
filedes)
關閉一個文件時還會釋放該進程加在該文件上的全部記錄鎖
當一個進程終止時,內核自動關閉它全部打開的文件。不少程序都利用了這一功能而不顯示地用close關閉打開文件。
lseek函數
每一個打開的文件都有一個與其相關聯的「當前文件偏移量」。它一般是一個非負數,用以度量從文件開始處計算的字節數。一般,讀、寫操做都從當前文件偏移量處開始,並使偏移量增長所讀寫的字節數。按系統的默認的狀況,當打開一個文件時,除非指定O_APPEND選項,不然該偏移量被設置爲0。
能夠調用lseek顯示地位一個打開的文件設置其偏移量。
#include<unistd.h>
off_t lseek(int
filedes, off_t
offset, int
whence);
對參數offset的解釋與參數whence的值有關。
若whence是SEEK_SET,則將該文件的偏移量設置爲距文件開始處的offset個字節。
若whence是SEEK_CUR,則該文件的偏移量設置爲其當前值加offset,offset可爲正或負。
若whence是SEEK_END,則將該文件的偏移量設置爲文件長度加offset,offset可爲正或負。
若lseek成功執行,則返回新的文件偏移量,爲此能夠用下列方式肯定打開文件的當前偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
這種方法也可用來肯定所涉及的文件是否能夠設置偏移量。若是文件描述符引用的是一個管道、FIFO或網絡套接字,則lseek返回-1,並將errno設置爲ESPIPE。
實例:程序用於測試可否對其標準輸入設置偏移量。
#include"apue.h"
int main()
{
if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
printf("cannot seek/n");
else
printf("seek OK/n");
exit(0);
}
一般,文件的偏移量應當是一個非負整數,可是,某些設備也可能容許負的偏移量。但對於普通文件,則其偏移量必須是非負值。由於偏移量多是負值,因此在比較lseek的返回值時應當謹慎,不要測試它是否小於0,而要測試它是否等於-1。
lseek僅將當前的文件偏移量記錄在內核中,它並不引發任何I/O操做。而後,該偏移量用於下一個讀或寫操做。
文件偏移量能夠大於文件的當前長度,在這種狀況下,對該文件的下一次寫將加長該文件,並在文件中構成一個空洞,這一點是容許的。位於文件中但沒有寫過的字節都被讀爲0。
文件中的空洞並不要求在磁盤上佔用儲存區。具體處理方式與文件系統的實現有關,當定位到超出文件尾端以後寫時,對於新寫的數據須要分配磁盤塊,可是對於原文件尾端和新開始寫位置之間的部分則不須要分配磁盤塊。
實例 建立一個具備空洞的文件
#include"apue.h"
#include"fcnl.h"
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
int main()
{
int fd;
if ((fd = creat("file.hole", FILE_MODE)) < 0)
err_sys("creat error");
if (write (fd, buf1, 10) != 10)
err_sys("buf1 write error");
if (lseek(fd,16384, SEEK_SET) == -1)
err_sys("lseek error");
if (write(fd, buf2, 10) != 10)
err_sys("buf2 write error");
exit(0);
}
read函數
調用read函數從打開文件中讀數據。
#include<unistd.h>
ssize_t read(int
filedes, void
*buf, size_t
nbytes);
有多種狀況可以使實際讀到的字節數少於要求讀的字節數:
讀普通文件時,在讀到要求字節數以前已經到達了文件尾端。例如,若在到達文件尾端以前還有30個字節,而要求讀100個字節,則read返回30,下一次再調用read時,它將回0。
當從終端設備讀時,一般一次最多讀一行
當從網絡讀時,網絡中的緩衝機構可能形成返回值小於所要求讀的字節數。
當從管道或FIFO讀時,如若管道包含的字節少於所需的數量,那麼read將只返回實際可用的字節數。
當從某些面向記錄的設備(例如磁盤)讀時,一次最多返回一個記錄。
當某一信號草成終端,而已經讀了部分數據量時。讀操做從文件的當前偏移量出開始,在成功返回以前,該偏移量將增長實際獨到的字節數
write函數
調用write函數向打開的文件寫數據。
#include"unistd.h"
ssize_t write(int
filedes, const void
*buf, size_t
nbytes)
對於普通文件,寫操做從文件的當前偏移量處開始。若是在打開該文件時,指定了O_APPEND選項,則在每次寫操做以前,將文件偏移量設置在文件的當前結尾處。在一次成功寫以後,該文件偏移量增長實際寫的字節數。
I/O的效率
程序清單3-3中的程序使用read和write函數負值一個文件。關於該程序應注意下列各點:
它從標準輸入讀,寫至標準輸出,這就假定在執行本程序以前,這些標準輸入、輸入已由shell安排好。確實,全部經常使用的UNIX系統shell都提供一種方法,他在標準輸入上打開文件用於讀,在標準輸出上建立一個文件。這使得程序沒必要自行打開輸入和輸出文件。
不少應用程序假定標準輸入是文件描述符0,標準輸出是文件描述符1。本實例中則使用STDIN_FILENO和STDOUT_FILENO.
爲考慮進程終止時,UNIX系統內核會關閉該進程的全部打開文件描述符,因此此實例並不會關閉輸入和輸出的文件。
對UNIX系統內核而言,文本文件和二進制代碼文件並沒有區別,因此本實例對這兩種文件都能工做
實例 將標準輸入複製到標準輸出
#include"apue.h"
#define BUFFSIZE 4096
int main()
{
int n;
char buf[BUFFSIZE];
while ((n = read(STDIN_FILENO, buf ,BUFFSIZE)) >0)
if ( write(STDOUT_FILENO,buf, n) !=n)
err_sys("write error");
if (n <0)
err_sys("read error");
exit(0);
}
咱們沒有回答的一個問題是如何選取BUFFSIZE值。在回答此問題以前,讓咱們先用各類不一樣的BUFFSIZE值來運行此程序。
用程序讀文件,其標準輸出被從新定向到/dev/null上。此測試所用的文件時Linux ext2文件系統,其塊長爲4096字節。系統CPU時間的最小值出如今BUFFSIZE爲4096處,繼續增長緩衝區幾乎沒有影響。
大多數文件系統爲改善其性能都採用某種預讀技術,當檢測到正進行順序讀取時,系統就試圖讀入鼻應用程序所要求的更多數據,並假想應用程序很快就會讀這些數據。當BUFFSIZE爲128KB後,預讀中止了,這對讀操做的性能產生了影響。
文件共享
UNIX系統支持在不一樣進程間共享打開的文件。在介紹dup以前,先要說明這種共享。爲此先介紹內核用於全部I/O的數據結構。
內核使用三種數據結構表示打開的文件,他們之間的關係決定了在文件共享方面一個進程對另外一個進程可能產生的影響。
(1)每一個進程在進程表中都有一個記錄項,記錄項中包含有一張打開文件描述符表,可將其視爲一個矢量,每一個描述符佔用一項。與每一個文件描述符向關聯的是:
文件描述符標誌。
指向一個文件表的指針。
(2)內核爲全部打開文件維持一張文件表。每一個文件表項包含:
文件狀態標誌(讀、寫、添寫、同步和非阻塞等)。
當前文件偏移量。
指向該文件v節點表項的指針。
(3)每一個打開文件(或設備)都有一個v節點結構。v節點包含了文件類型和對此文件進行各類操做的函數的指針。對於大多數文件,v節點還包含了該文件的i節點。這些信息是在打開文件時從磁盤上讀入內存的,因此全部關於文件的信息都是快速可提供使用的。例如,i節點包含了 文件的全部者 、文件長度、文件所在的設備、指向文件實際數據塊在磁盤上所在位置的指針等等。
咱們忽略了某些實現細節,但這並不影響咱們的討論。例如,打開文件描述符表可存放在用戶空間。而非進程表中。這些表也能夠用多種方式實現,沒必要必定是數組;例如,可將他們實現爲結構的鏈表。
圖3-1顯示了一個進程的三張表之間的關係。該進程有兩個不一樣的打開文件:一個文件打開爲標準輸入,另外一個打開爲標準輸出。從UNIX系統的早期版本以來,這三張表之間的基本關係一直保持至今。這種安排對於在不一樣進程之間共享文件的方式很是重要。
若是兩個獨立進程各自打開了同一個文件,則有圖3-2所示的安排。咱們假定第一個進程在文件描述符3上打開該文件,而另外一個進程則在文件描述符4上打開該文件。打開該文件的每一個進程都獲得一格文件表項,但對一個給定的文件只有一個v節點表項。每一個進程都有本身的文件表項的一個理由是:這種安排是每一個進程都有它本身的對該文件的當前偏移量。
給出了這些結構後,如今對前面所述的操做做進一個說明。
在完成每一個write後,在文件表項中的當前文件偏移量即增長所寫的字節數。若是這使當前文件偏移量超過了當前文件長度,則在i節點表項中的當前文件長度被設置爲當前文件偏移量。
若是用O_APPEND標誌打開了一個文件。則相應標誌也被設置到文件表項的文件狀態標誌中。每次對這種具備添寫標誌的文件執行寫操做時,在文件表項中的當前文件偏移量首先被設置爲i節點表項中的文件長度。這就使得每次寫的數據添加到文件的當前端處。
若一個文件用lseek定位到衛檢當前的尾端,則文件表項中的當前文件偏移量被設置爲i節點表項中的當前文件長度。
lseek函數只修改文件表項中的當前文件偏移量,沒有進行任何i/O操做。
可能有多個文件描述符項指向同一個文件表項。 //後續介紹
注意,文件描述符標誌和文件狀態標誌在做用域方面的區別,前者只用於一個進程的一個描述符,然後者則適用於指向該給定文件表項的任何進程中的全部描述符。 //後續介紹
本節上面所述的一切對於多個進程讀同一個文件都能正常工做。每一個進程都有它本身的文件表項,其中也有本身的當前文件偏移量,。可是,當多個進程寫同一個文件時,則可能產生預期不到的效果。爲了說明如何避免這種狀況,須要理解原子操做的概念。
原子操做
添寫至一個文件
考慮一個進程,他要將數據添加到一個文件尾端。早期的UNIX系統版本並不支持open的O_APPEND選項,因此程序被編寫爲下列形式:
if(lseek(fd,0L,2) < 0)
err_sys(lseek error);
if(write(fd , buf, 100) !=100)
err_sys("write error");
對單個進程而言,這段程序能正常工做,但如有多個進程同時使用這種方法將數據添加到用一個文件,則會擅勝問題。(例如,若此程序由多個進程同時執行,各自將消息添加到一個日誌文件中,就會產生這種狀況)。
假定有兩個獨立的進程A和B都對同一個文件進行添加操做。每一個進程都己打開了該文件,但未使用O_APPEND標誌。此時,各數據結構之間的關係如圖3-2所示。每一個進程都有它本身的文件表項,可是共享一個v節點項。假定進程A調用了lseek,他將進程A的該文件當前偏移量設置爲1500字節。而後內核切換進程是進程B運行。進程B執行lseek也將其對該文件的當前便宜設置爲1500字節。而後B調用write,他將B的該文件當前文件的增至1600。由於該文件的長度已經增長了,因此內核對v節點中的當前文件長度更新爲1600。而後內核又進行進程切換使進程A恢復運行。當A調用write時,就從其當前文件偏移量(1500)處將數據寫到文件中去。這樣也就替換了進程B剛寫到該文件中的數據。
問題出在邏輯操做「定位到文件尾端處,而後寫」上,他使用了兩個分開的函數調用。解決問題的方法是使這2兩個操做對於其餘進程而言成爲一個原子操做。任何一個須要多個函數調用的操做都不多是原子操做,由於在兩個函數調用之間,內核有可能會臨時掛起該進程。
UNIX系統提供了一種方法使這種操做成爲原子操做,該方法是在打開文件時設置O_APPEND標誌。正如前一節中所述,這就使內核每次對這種文件進行寫以前,都將進程的當前偏移量設置到該文件的尾端處,因而在每次寫以前就再也不須要調用lseek。
pread和pwrite函數
#include<unistd.h>
ssize_t pread(int
filedes, void
*buf, size_t
nbytes, off_t
offset);
ssize_t pwrite(int
filedes, void
*buf, size_t
nbytes, off_t
offset);
調用pread至關於順序調用lseek和read,可是pread又與這種順序調用有下列重要區別:
調用pread時,沒法中斷其定位和讀操做。
不更新文件指針。
調用pwrite至關於順序調用lseek和write,但也與他們有相似的區別。
建立一個文件
在對open函數的O_CREAT和O_EXCL選項進行說明時,咱們己見到另外一個有關原子操做的例子。當同時指定這兩個選項,而該文件又已經存在時,open將失敗。咱們曾說起檢查該文件是否存在以及建立該文件這兩個操做時做爲一個原子操做執行的。若是沒有這樣一個原子操做,那麼可能會編寫下列程序段:
if((fd = open(pahtname, O_WRONLY)) < 0){
if(errno == ENOENT){
if((fd = creat(pathname,mode)) < 0)
err_sys(...);
}else{
err_sys(...);
}
}
若是在open和creat之間,另外一個進程建立文件,那麼就會引發問題。例如,若在這2兩個函數調用之間,另外一個進程建立了該文件,而且寫進了一些數據,而後,原先的進程執行這段程序中的creat,這時,剛由另外一個進程寫上去的數據會被擦去。如若將這二者合併在一個原子操做中,這種問題也就不會產生。
通常而言,原子操做指的是由多步組成的操做。若是該操做原子地執行,則那麼執行完全部步驟,要麼一步也不執行,不可能只執行全部步驟地一個子集。
dup和dup2函數
下面兩個函數均可用來複制一個現存的文件描述符:
#include"unistd.h"
int dup(int
filedes);
int dup2(int
filedes, int
filedes2)
有dup返回的新文件描述符必定是當前可用文件描述符中的最小數值。用dup2則能夠用filedes2參數指定新描述符。若是filedes2已經打開,則將其關閉。若filedes等於filedes2,則dup2返回filedes2,而不關閉它。
這些函數返回的新文件描述符與參數filedes共享同一個文件表項。圖3-3顯示了這種狀況。
newfd = dup(1);
此函數開始執行時,假定下一個可用的描述符是3。由於兩個描述符指向同以文件表項,因此他們共享同一文件狀態以及同以當前便宜量。
每一個文件描述符都有它本身的一套文件描述符標誌。正如咱們將在下一節中說明的那樣,新描述符的執行時關閉標誌老是有dup函數清除。
複製一個描述符的另外一種方法是使用fcntl函數實際上調用
dup(filedes);
等效於
fcntl(filedes, F_DUPFD, 0);
而調用
dup2(filedes,filedes2);
等效於
close(filedes2);
fcntl(filedes,F_DUPFD,filedes2);
後一種狀況下,dup2並不徹底等同於close加上fcntl。
sync、fsync和fdatasync函數
傳統的UNIX實如今內核中沒有緩衝區高速緩存或葉面高速緩存,大多數磁盤I/O都經過緩衝進行。當將數據寫入文件時,內核一般先將該數據複製到其中一個緩衝區中,若是該緩衝區還沒有寫滿,則並不將其排入輸出隊列,而是等待其寫滿或當內核須要充用該緩衝區以便存放其餘磁盤塊數據時,在將該緩衝排入輸出隊列,而後待其到達隊首時,才進行實際的I/O操做。這種輸出方式被稱爲延遲寫。
延遲寫減小了磁盤讀寫次數,可是卻下降了文件內容的更新速度,使得欲寫到文件中的數據在一段時間內並無寫道磁盤上。當系統發生故障時,這種延遲可能形成文件更新內容的丟失。爲了保證磁盤上實際文件系統與緩衝區高速緩存中內容的一致性,UNIX系統提供了sync、fsync和fdatasync函數。
#include"unistd.h"
int fsync(int
filedes);
int fdatasync(int
filedes);
void sync();
sync函數只是將全部修改過的塊緩衝區排入寫隊列,而後就返回,它並不等待實際寫磁盤操做結束。
一般稱爲update的系統受滬進程會週期性地調用sync函數。這就保證了按期沖洗內核的塊緩衝區。命令sync(1)也調用sync函數。
fsync函數只對由文件描述符filedes指定的單一文件起做用,而且等待寫磁盤操做結束,而後返回。fsync可用於數據庫這樣的應用程序,這種應用程序須要確保將修改過的塊當即寫到磁盤上。
fdatasync函數相似於fsync,但它隻影響文件的數據部分。而除數據外fsync還會同步更新文件的屬性。
fcntl函數
fcntl函數能夠改變已打開的文件的性質。
#include"fcntl.h"
int fcntl(int
filedes, int
cmd,...);
fcntl函數有5種功能:
複製一個現有的描述符(cmd = F_DUPFD)
得到/設置文件描述符標記(cmd = F_GETFD或F_SETFD)
得到/設置文件狀態標誌(cmd = F_GETFL或F_SETFL)
得到/設置異步I/O全部權(cmd = F_GETOWN或F_SETOWN)
得到/設置記錄鎖(cmd = F_GETLK F_SETLK F_SETLKW)
咱們先說明這10種cmd值中前7種咱們將涉及與進程表項中各文件描述符向關聯的文件描述符標誌,以及每一個文件表項中的文件狀態標誌
F_DUPFD:
複製文件描述符filedes。新文件描述符做爲函數值返回。它是還沒有打開的各描述符中最小值。新描述符與filedes共享同一文件表項。可是,新描述符有它本身的一套文件描述符標誌,其中FD_CLOEXEC文件描述符被清除(這表示該描述符在經過一個exec時仍保持有效)
F_GETFD:
對應於filedes的文件描述符標誌做爲函數值返回。當前只訂一樂一個文件描述符標誌FD_CLOEXEC
F_SETFD
對於filedes設置文件描述符標誌。新標誌值按第三個參數設置。
F_GETFL
對應於filedes的文件狀態標誌做爲函數值返回。在說明open函數時,已說明了文件狀態。
不幸的是,三個訪問方式標誌(O_RDONLY O_WRONLY O_RDWR)並不各佔1位。所以首先必須用屏蔽字O_ACMODE取得訪問模式位,而後將結果與這三種值中的任一種做比較。
F_SETFL
將文件狀態標誌設置爲第三個參數的值(取爲正數值)。能夠更改的幾個標誌是:O_APPEND/O_NONBLOCK/O_SYNC/O_DYSNC/O_RSYNC/O_FSYNC/O_ASYNC。
F_GETOWN
取當前接收SIGIO和SIGURG信號的進程ID或進程組ID。
F_SETOWN
設置接收SIGIO和SIGURG信號的進程ID或進程組ID。正的arg指定一個進程ID,負的arg表示等於arg絕對值的一個進程組ID。