進程每打開一個文件的時候,會獲得該文件的文件描述符,而後續的讀寫操作都把文件描述符作為參數。在用戶空間或者內核空間,都是通過文件描述符來唯一地索引一個打開的文件。文件描述符使用int類型表示,文件描述符的范圍從0開始,到上限值-1,默認情況下,上限值為1024,也就是說,進程默認情況下最多可以打開1024個文件。負數是不合法的文件描述符,當函數調用出錯時,返回的文件描述符為-1。
每個進程都至少包含三個文件描述符:
遵循Linux一切皆文件的概念,文件描述符除了訪問普通文件外,幾乎能夠訪問任何能夠讀寫的東西。包括設備文件、管道、先進先出緩沖區、套接字等。
對文件進行讀寫之前,必須先打開文件。Linux提供了系統調用open()。open()有兩個函數原型:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
兩個函數均可用來打開文件,第二個函數比第一個多了參數mode,mode指定文件的權限---當創建新文件的時候才需要。如果文件打開成功,則返回文件描述符,指向pathname所指定的文件。flags參數用於指定打開的方式,它支持三種訪問模式:
flags參數還可以與下面的值進行按位或運算,修改打開文件的行為:
舉個例子,下面的句子表示:以寫的方式打開文件,如果文件不存在,則創建新的文件,並且文件的內容為空:
int fd ;
fd = open("file.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
這裡的0644指定了新創建的文件訪問權限,參數mode的取值如下:
實際上最終寫入磁盤的文件訪問權限是由mode參數和用戶的文件創建掩碼(umask)執行按位與操作得到的。舉個例子:
int main()
{
int fd;
fd = open("TEST.txt",O_WRONLY|O_CREAT|O_TRUNC,S_IRWXU|S_IRWXG|S_IRWXO );//以只讀方式打開文件
//等價於 fd = open("TEST.txt",O_WRONLY|O_CREAT|O_TRUNC,0777 );//以只讀方式打開文件
if(fd == -1)
perror("open file error!");
return 0;
}
按理來說,創建出來的文件的訪問權限應該是-rwxrwxrwx,而查看後發現其實不是:
ls -l TEST.txt
-rwxrwxr-x 1 huanzhewu huanzhewu 0 5月 7 21:29 TEST.txt 【權限為0775】
查看當前的掩碼:
$ umask
0002
可以發現 0775 = 0777 ^ (~0002) ,所以0775才是最後的文件訪問權限。umask是進程級屬性,通過調用umask()函數來修改,支持用戶修改新創建的文件和目錄的權限。
總結起來可以得到這樣一條公式:
newmode = mode ^ (~ umask)
總結一下:至此,我們了解了文件打開所提供的兩個系統調用函數open(),了解了打開文件的方式、新建文件的訪問權限設置。如果文件打開成功,那麼將返回一個文件描述符,這是一個非零整數(因為0,1,2是進行已經擁有的文件描述符),否則函數將返回-1
顧名思義,creat函數用來創建一個文件,不過我們可能產生疑問:前面的open函數使用一些選項後,不是也可以創建新文件嗎?沒錯,creat函數完全等價與下面的open語句:
int fd ;
fd = open("file.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
fd = creat("file.txt,0644"); /*兩個語句的作用完全等價*/
由於選項O_WRONLY|O_CREAT|O_TRUNC組合經常使用,因而系統調用專門使用creat函數來提供這個功能。creat函數的原型如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
其中參數的描述與open的參數一致,這裡不再贅述。
文件打開後,就能夠讀文件了。read()是最基礎、最常見的讀取文件的機制。read的函數原型為:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd 為文件描述符。每次調用read函數時,會從fd指向的文件的當前偏移(或稱文件位置)開始讀取count字節到buf所指的的內存中。隨著文件的讀取,fd的文件位置指針會向前移動。關於read的讀取,會出現很多需要思考的問題:
我們一一來看:
由於read有這麼多需要考慮的問題,如果希望每次都能讀入count個字節,下面是一段示例代碼:
//保證讀取200個字節到ptr中
ssize_t ret ;
int len = 200;
while(len!= 0 && ( ret = read(fd, ptr, len )) != 0)
{
if(ret == -1)
{
if( errno == EINTR)
continue;
perror("read");
break;
}
len -= ret ;
ptr += ret ;
}
再來看看問題1,當文件沒有數據可以讀時(一開始就沒有),read調用會被阻塞,直到文件有數據可以讀,這是一種阻塞I/O。如果文件以O_NONBLOCK模式打開,則文件為非阻塞模式,當文件沒有數據可以讀時,read系統調用返回-1,並把errno設置為EAGAIN。
ssize_t ret ;
int len = 200;
while(len!= 0 && ( ret = read(fd, ptr, len )) != 0)
{
if(ret == -1)
{
if( errno == EINTR)
{
printf("讀取被中斷\n");
continue;
}
if(errno== EAGAIN)
{
printf("文件沒有可讀\n");
//重新提交讀取
continue;
}
break;
}
len -= ret ;
ptr += ret ;
}
除了errno被設置為EINTR與EAGAIN,其他情況下都是出現嚴重的文件讀取錯誤,重新執行讀操作不會成功。
write的函數原型為:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count); 【將buf中count個字節的內容寫入fd指定的文件中】
write的返回值比較簡單:
對於普通文件,write基本能保證每次執行調用能夠寫入全部的內容。對於其他文件如socket,需要進行循環寫,保證所有的字節都寫入了文件中:
ssize_t ret ;
int len = 200;
while(len!= 0 && ( ret = write(fd, ptr, len )) != 0)
{
if(ret == -1)
{
if( errno == EINTR)
continue;
perror("write");
break;
}
len -= ret ;
ptr += ret ;
}
同樣的,當以非阻塞的模式打開文件時(O_NONBLOCK),系統調用write()會返回-1,並把errno設置為EAGAIN。
系統調用write()時,數據從用戶空間的緩沖區中拷貝到了內核空間的緩沖區,但並沒有立即把數據寫入磁盤中,這稱為延遲寫。延遲寫的問題在於,如果在數據真正寫入磁盤之前系統崩潰了,則數據可能丟失。內核設置了一個時間,在該時間內將內核空間緩沖區上的數據寫入磁盤,該時間稱為"最大存放時效"。Linux系統也支持強制文件立即寫入磁盤上,這在後面介紹。
程序完成文件的讀寫後,調用close函數關閉文件描述符與文件之間的連接,使得文件描述符可以被重用。close的函數原型為:
#incldue<unistd.h>
int close (int fd);
文件關閉成功返回0,出錯返回-1,並設置相應的errno。文件成功關閉並不以為著該文件的數據已經被寫入磁盤,同步選項在後續介紹。
總結:本文簡單介紹了文件的打開、創建、讀寫、關閉操作,介紹了一些常用的open參數選項,creat與open的等價性,循環讀、循環寫的必要性,也關注了各個系統調用的返回值含義,了解如何設置非阻塞讀寫,並簡單提到了延遲寫的問題,在後續的文件中將介紹同步I/O的內容。