文件描述符
對於內核而言,所有打開的文件都通過文件描述符引用。文件描述符是一個非負整數。當打開一個現有文件或創建一個新文件時,內核向進程返回一個文件描述符。當讀或寫一個文件時,使用open或creat返回的文件描述符標識該文件,將其作為參數傳遞給read或write。
按照慣例,UNIX系統shell使用文件描述符0與進程的標准輸入相關聯,文件描述符1與標准輸出相關聯,文件描述符2與標准出錯相關聯。這是各種shell以及很多應用程序使用的慣例,而與UNIX內核無關,如果不遵守這種慣例,那么很多UNIX系統應用程序就不能正常工作。
在依從POSIX的應用程序中,幻數0、1、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。