在該章節(jié)中討論的文件描述符的概念。其中包括:打開文件,關(guān)閉文件,從文件中讀取數(shù)據(jù)和向文件中寫數(shù)據(jù)。
- 概述
所有執(zhí)行I/O操作的系統(tǒng)調(diào)用都以文件描述符(一個非負(fù)整數(shù)(通常是比較小的整數(shù)))來指代打開的文件。文件描述符用以表示所有類型的已打開文件,包括管道(pipe)、FIFO、socket、終端、設(shè)備和普通文件。
下面介紹執(zhí)行I/O操作的4個主要系統(tǒng)調(diào)用: - open:fd=open(pathname,flags,mode)函數(shù)打開pathname所標(biāo)識的文件,并返回文件描述符,用以在后續(xù)函數(shù)調(diào)用中指代打開的文件。如果文件不存在,open()函數(shù)可以創(chuàng)建之,這取決于對位掩碼參數(shù)flags的設(shè)置。flags參數(shù)還可以指定文件的打開方式:只讀、只寫亦或讀寫方式。mode參數(shù)則指定了由open()調(diào)用創(chuàng)建文件的訪問權(quán)限,如果open()函數(shù)沒有創(chuàng)建文件,則可以忽略或省略mode參數(shù)。
- read :numread=read(fd,buffer,count)調(diào)用從fd所指代的打開文件中讀取至多count字節(jié)的數(shù)據(jù),并存儲到buffer中。read()調(diào)用的返回值為實(shí)際讀取到的字節(jié)數(shù)。如果再無字節(jié)刻度,則返回值為。
- write:numwritten=write(fd,buffer,count)調(diào)用從buffer中讀取多達(dá)count字節(jié)的數(shù)據(jù)寫入由fd所指代的已打開文件中。write()調(diào)用的返回值為實(shí)際寫入文件中的字節(jié)數(shù),且有可能小于count。
- close:status=close(fd)在所有輸入/輸出操作完成后,調(diào)用close(),釋放文件描述符fd與之相關(guān)的內(nèi)核資源。
實(shí)現(xiàn)一個簡版的cp(1)命令:
#include<sys/stat.h>
#include<fcntl.h>
#include "tlpi_hdr.h"
#include "error_functions.h"http://用來輸入錯誤信息
#ifndef BUF_SIZE
#define BUF_SIZE 1024
#endif
int main(int argc, char const *argv[])
{
int inputFd,outputFd,openFlags;//用來存儲I/O調(diào)用的返回值
mode_t filePerms; //整型,用來表示文件權(quán)限及類型
ssize_t numRead; //有符號整型,當(dāng)字節(jié)數(shù)(為負(fù)時)表示錯誤
char buf[BUF_SIZE];
//strcmp():比較兩個字符串設(shè)這兩個字符串為str1,str2,若str1==str2,則返回零
if(argc!=3||strcmp(argv[1],"--help")==0)
usageErr("%s old-file new-file\n",argv[0]);
inputFd=open(argv[1],O_RDONLY);
if(inputFd==-1)
errExit("opening file %s",argv[1]);
openFlags=O_CREAT | O_WRONLY | O_TRUNC; //open()函數(shù)的參數(shù),后面會介紹
filePerms=S_IRUSR | S_IWUSR |S_IWGRP | S_IROTH |S_IWOTH;
//文件類型參數(shù),后面介紹
outputFd=open(argv[2],openFlags,filePerms);
if(outputFd==-1)
errExit("opening file %s",argv[2]);
while((numRead=read(inputFd,buf,BUF_SIZE))>0)
if(write(outputFd,buf,numRead)!=numRead)
fatal("couldn't write whole buffer");
if(numRead==-1)
errExit("read");
if(close(inputFd)==-1)
errExit("close input");
if(close(outputFd)==-1)
errExit("close output");
exit(EXIT_SUCCESS);
return 0;
}
- 通用I/O
Unix I/O模型的顯著特點(diǎn)之一就是輸入/輸出的通用性概念。
就是說open、read、write、close可以對所有類型的文件執(zhí)行I/O操作,包括終端設(shè)備。所以如果只使用了這些系統(tǒng)調(diào)用編寫的程序,可以對系統(tǒng)內(nèi)所有類型的文件使用。
實(shí)現(xiàn)通用I/O的前提就是確保每一個文件系統(tǒng)和設(shè)備驅(qū)動程序都實(shí)現(xiàn)了相同的I/O系統(tǒng)調(diào)用集。因?yàn)閘inux下文件系統(tǒng)和設(shè)備所特有的操作細(xì)節(jié)已經(jīng)放在內(nèi)核中處理,所以在編程時通??梢院雎栽O(shè)備轉(zhuǎn)悠的因素。而如果應(yīng)用程序需要訪問文件系統(tǒng)和設(shè)備的專有功能時,可以選擇ioctl()系統(tǒng)調(diào)用來處理,該系統(tǒng)調(diào)用為I/O模型之外的專有特性提供了訪問接口。 - 打開一個文件:Open()
open()調(diào)用既可以打開一個已經(jīng)存在的文件,也能創(chuàng)建并打開一個新文件。
#include<sys/stat.h>
#include<fcntl.h>
int open(const char* pathname,int flags,.../*mode_t mode*/);
//返回:打開成功的話返回文件描述符,打開失敗的話返回-1
要打開的文件由參數(shù)pathname來標(biāo)識,如果pathname是一個符號鏈接,那么該調(diào)用會對其進(jìn)行解引用。如果調(diào)用成功,open()返回文件描述符,用于在后續(xù)函數(shù)調(diào)用中指代該文件,如果發(fā)生錯誤,則返回-1,并將errno置為相應(yīng)的錯誤標(biāo)志。參數(shù)flags為位掩碼,用來指定文件的訪問模式。 當(dāng)調(diào)用open()創(chuàng)建新文件時,位掩碼參數(shù)mode指定了文件的訪問權(quán)限。
如果open()并未指定O_CREAT標(biāo)志,則可以省略mode參數(shù)。 O_RDONLY ---->以只讀方式打開文件 O_WRONLY ---->以只寫方式打開文件 O_RDWR ---->以讀寫方式打開文件。
open函數(shù)使用的例子:
#include<sys/stat.h>
#include<fcntl.h>
#include "tlpi_hdr.h"
#include "error_functions.h"http://用來輸入錯誤信息
int main(int argc, char const *argv[])
{
int fd;
fd=open("startup",O_RDONLY);
if(fd==-1)
errExit("open");
fd=open("myfile",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR);
if(fd==-1)
errExit("open");
fd=open("w.log",O_WRONLY|O_CREAT|O_TRUNC|O_APPEND,S_IRUSR|S_IWUSR);
if(fd==-1)
errExit("open");
return 0;
}
open()調(diào)用所返回的文件描述符數(shù)值:SUSv3規(guī)定,如果調(diào)用open()成功,必須保證其返回值為進(jìn)程為用文件描述符中數(shù)值最小者。
所以可以利用這項(xiàng)特性來以特定的文件描述符打開某一個文件。
例如:下例代碼會確保使用標(biāo)準(zhǔn)輸入(文件描述符0)打開一個文件
#include<sys/stat.h>
#include<fcntl.h>
#include "tlpi_hdr.h"
#include "error_functions.h"http://用來輸入錯誤信息
int main(int argc, char const *argv[])
{
int fd;
if(close(STDIN_FILENO)==-1)
//close()關(guān)閉STDIN_FILENO 使得系統(tǒng)內(nèi)最小文件描述符為0
errExit("close");
fd=open("startup",O_RDONLY);
if(fd==-1)
errExit("open");
printf("%d\n",fd);
return 0;
}
結(jié)果截圖:
由于文件描述符0未用,所以open()調(diào)用勢必使用此描述符打開文件。
-
open()調(diào)用中的flags參數(shù)
在上述的代碼例子中,flags參數(shù)除了使用文件訪問標(biāo)志外,還使用了其他操作標(biāo)志。如下圖所示
這里寫圖片描述
這里寫圖片描述
上述訪問標(biāo)志可以分為三組:
1、文件訪問模式標(biāo)志:先前描述的O_RDONLY、O_WRONLY 、O_RDWR標(biāo)志均在此類中,調(diào)用open()時,上述三者在flags參數(shù)中不能同時使用,只能指定其中一種。 調(diào)用fcntl()的F_GETFL操作可以檢索文件的訪問模式。
2、文件創(chuàng)建標(biāo)志:這些標(biāo)志位于圖中第二部分,其控制范圍不拘于open()調(diào)用行為的方方面面,還涉及后續(xù)I/O操作的各個選項(xiàng)。這些操作不能被檢索,也無法修改。
3、已打開文件的狀態(tài)標(biāo)志:這些標(biāo)志時圖中的剩余部分。使用fcntl()的F_GETFL和F_SETFL操作可以分別檢索和修改此類標(biāo)志。有時干脆稱之為文件狀態(tài)標(biāo)志。
flags常量的詳解:
O_APPEND : 標(biāo)志如其名,總是在文件尾部追加數(shù)據(jù)。
O_ASYNC : 當(dāng)對open()調(diào)用所返回的文件描述符可以實(shí)施I/O操作時,系統(tǒng)會產(chǎn)生一個信號通知進(jìn)程。這一個特性,也被稱之為信號驅(qū)動I/O,僅對特定類型的文件有效,諸如終端、FIFO及socket。在linux中,調(diào)用open()時指定O_ASYNC標(biāo)志沒有任何實(shí)質(zhì)效果,要啟用信號驅(qū)動I/O特性,必須調(diào)用fcntl()的F_SETFL操作來設(shè)置O_ASYNC標(biāo)志。
O_CLOEXEC : 為新(創(chuàng)建)的文件描述符啟用close-on-flag標(biāo)志(FD_CLOEXEC)。使用O_CLOEXEC標(biāo)志(打開文件),可以免去程序執(zhí)行fcntl()的F_GETFD和F_SETFD操作來設(shè)置close-on-exec標(biāo)志的額外工作。----------------------看不懂
O_CREATE : 如果文件不存在,將創(chuàng)建一個新的空文件。即使文件以只讀方式打開,此標(biāo)志依然有效。如果在open()調(diào)用中指定O_CREATE標(biāo)志,那么還要提供mode參數(shù),否則,會將新文件的權(quán)限設(shè)置為棧中的某個隨機(jī)值。
O_TRUNC : 如果文件已經(jīng)存在且為普通文件,那么將清空文件內(nèi)容,將其長度置為0。在linux下使用此標(biāo)志,無論以讀、寫方式打開文件,都可清空文件內(nèi)容(在這兩種情況下,都必須擁有對文件的讀寫權(quán)限)。
O_DIRECTORY : 如果pathname參數(shù)并非目錄,將返回錯誤(錯誤號errno為ENOTDIR)。這一標(biāo)志是專為實(shí)現(xiàn)opendir()函數(shù)而設(shè)計(jì)的擴(kuò)展標(biāo)志。為使O_DIRECTORY標(biāo)志的常量定義在<fcntl.h>中有效,必須定義_GNU_SOURCE功能測試宏。
open()函數(shù)的錯誤
若打開文件時發(fā)生錯誤,open()將返回-1,錯誤號errno標(biāo)識錯誤原因。以下是可能發(fā)生的錯誤
EACCES:文件權(quán)限不允許調(diào)用進(jìn)程以flags參數(shù)指定的方式打開文件。無法訪問文件,其可能的原因有目錄權(quán)限的限制、文件不存在并且無法創(chuàng)建該文件。
EMFILE:進(jìn)程已打開的文件描述符數(shù)量達(dá)到了進(jìn)程資源限制所設(shè)定的上限。
EISDIR:所指定的文件屬于目錄,而調(diào)用者企圖打開該文件進(jìn)行寫操作。不允許這種用法,在某些場合中,打開目錄進(jìn)行讀操作是必要的。
ENFILE :文件打開數(shù)量已經(jīng)達(dá)到系統(tǒng)允許的上限。
ENOENT:要么文件不存在且未指定O_CREATE標(biāo)志,要么指定了O_CREATE標(biāo)志,但pathname參數(shù)所指定路徑的目錄之一不存在,或者pathname參數(shù)為符號鏈接,而該鏈接指向的文件不存在(空鏈接)。
EROFS : 所指定的文件隸屬于只讀文件系統(tǒng),而調(diào)用者企圖以寫方式打開文件。
Create()系統(tǒng)調(diào)用
在早期的unix系統(tǒng)中,open()只有兩個參數(shù),不能用來創(chuàng)建新文件,而是使用create()系統(tǒng)調(diào)用來創(chuàng)建并打開一個新文件。
#include<fcntl.h>
int create(const char * pathname,mode_t mode)
create()系統(tǒng)調(diào)用根據(jù)pathname參數(shù)創(chuàng)建并打開一個文件,若文件已存在,則打開文件,并清空文件內(nèi)容,將其長度清0。create()返回一文件描述符,供后續(xù)系統(tǒng)調(diào)用使用。create()系統(tǒng)調(diào)用等同于如下open()調(diào)用:
fd=open(pathname,O_WRONLY|O_CREATE|O_TRUNC,mode); 現(xiàn)在一般都使用open()來代替create()的操作。
讀取文件內(nèi)容:read()
read()系統(tǒng)調(diào)用從文件描述符fd所指代的打開文件中讀取數(shù)據(jù)。
#include<unistd.h>
ssize_t read(int fd,void *buffer,size_t count);
return number of bytes read,0 on EOF ,or -1 on error
count參數(shù)指定最多能讀取的字節(jié)數(shù),(size_t數(shù)據(jù)類型屬于無符號整數(shù)類型)。buffer參數(shù)提供用來存放輸入數(shù)據(jù)的內(nèi)存緩沖區(qū)地址。緩沖區(qū)至少應(yīng)有count個字節(jié)。linux的系統(tǒng)調(diào)用不會分配內(nèi)存緩沖區(qū)用以返回信息給調(diào)用者。所以,必須預(yù)先分配大小合適的緩沖區(qū)并將緩沖區(qū)指針傳遞給系統(tǒng)調(diào)用。但有些庫函數(shù)卻會分配內(nèi)存緩沖區(qū)用以返回信息給調(diào)用者。
如果read()調(diào)用成功,將返回實(shí)際讀取的字節(jié)數(shù)。如果遇到文件結(jié)束(EOF)則返回0,如果出現(xiàn)錯誤則返回-1。ssize_t數(shù)據(jù)類型屬于有符號的整數(shù)類型,用來存放(讀取的)字節(jié)數(shù)或-1(表示錯誤)。如果讀普通文件時,在讀到要求字節(jié)數(shù)之前已經(jīng)達(dá)到文件結(jié)尾,解決方法如:若達(dá)到文件尾端之前有30個字節(jié),而要求讀50個字節(jié),則第一次read返回?cái)?shù)值為30,在下次調(diào)用read時返回0(表示文件讀取完畢)。
一次read()調(diào)用所讀取的字節(jié)數(shù)可以小于請求的字節(jié)數(shù)。對于普通文件而言,這可能時因?yàn)楫?dāng)前的讀取位置靠近文件尾部。
當(dāng)read()應(yīng)用于其他文件類型時,比如管道、FIFO、socket或者終端,在不同環(huán)境下也會出現(xiàn)read()調(diào)用讀取的字節(jié)小于請求字節(jié)數(shù)的情況。
#include<unistd.h>
#include "tlpi_hdr.h"
#include "error_functions.h"
#define MAX_READ 20
int main(int argc, char const *argv[])
{
/* code */
char buffer[MAX_READ+1];
ssize_t numRead=read(STDIN_FILENO,buffer,MAX_READ);
if(numRead==-1)
errExit("read");
buffer[numRead]='\0'; //必須加上這一行,才能在終端內(nèi)讀取字符。
printf("The input data was :%s\n",buffer);
return 0;
}
read()可以從文件中讀取任意序列的字節(jié),有時讀到的信息可能是文本數(shù)據(jù),但有時可能是二進(jìn)制整數(shù)或二進(jìn)制形式的C語言數(shù)據(jù)結(jié)構(gòu)。read()不能區(qū)分這些數(shù)據(jù),所以不能遵從c語言對字符串處理的約定------在字符串尾部追加標(biāo)識字符串結(jié)束的空字符。
所以如果要使用c語言和read()系統(tǒng)調(diào)用,必須在輸入緩沖區(qū)結(jié)尾處顯示追加一個表示終止的空字符。
數(shù)據(jù)寫入文件:write()
write()系統(tǒng)調(diào)用將數(shù)據(jù)寫入一個已經(jīng)打開的文件中
#include<unistd.h>
ssize_t write(int fd,void *buffer,size_t count
write()調(diào)用的參數(shù)含義與read()調(diào)用類似。buffer參數(shù)為要寫入文件中數(shù)據(jù)的內(nèi)存地址,count參數(shù)為欲從buffer寫入文件的數(shù)據(jù)字節(jié)數(shù),fd參數(shù)為一文件描述符,指代數(shù)據(jù)要寫入的文件。如果write()調(diào)用成功,將返回實(shí)際寫入文件的字節(jié)數(shù),該返回值可能小于count參數(shù)值。這被稱為“部分寫”。對磁盤文件來說,造成"部分寫"的原因可能時因?yàn)榇疟P已滿,或是因?yàn)檫M(jìn)程資源對文件大小的限制。
對磁盤文件執(zhí)行I/O操作時,wirte()調(diào)用成功并不能保存數(shù)據(jù)已經(jīng)寫入磁盤。因?yàn)闉榱藴p少磁盤活動量和加快write()系統(tǒng)調(diào)用,內(nèi)核會緩存磁盤的I/O操作。
關(guān)閉文件:close()
close()系統(tǒng)調(diào)用關(guān)閉一個打開的文件描述符,并將其釋放返回調(diào)用進(jìn)程,供進(jìn)程繼續(xù)使用。當(dāng)一進(jìn)程終止時,將其自動關(guān)閉其已打開的所有文件描述符。
#include<unistd.h>
int close(int fd);
顯式關(guān)閉不再需要的文件描述符是個良好的編程習(xí)慣,可以使代碼在后續(xù)修改時更具有可讀性,也更可靠。文件描述符屬于有限資源,因此文件描述符關(guān)閉失敗可能會導(dǎo)致一個進(jìn)程將文件描述符資源消耗殆盡。要像其他所有系統(tǒng)調(diào)用一樣,應(yīng)對close()的調(diào)用進(jìn)行錯誤檢查。
if(close(fd)==-1) errExit("close");
上面這個錯誤提示可以捕獲到的錯誤有:嘗試關(guān)閉一個未打開的文件描述符、兩次關(guān)閉同一文件描述符等。
- 改變文件偏移量:lseek()
對于每個打開的文件,系統(tǒng)內(nèi)核會記錄其文件偏移量,有時也將文件偏移量稱為讀寫偏移量或指針。文件偏移量是指執(zhí)行下一個read()或write()操作的文件起始位置,會以相對于文件頭部起始點(diǎn)的文件當(dāng)前位置來表示。文件第一個字節(jié)的偏移量為0。
第一次文件打開時,會將文件偏移量設(shè)置為指向文件開始,以后每次read()或write()調(diào)用將自動對其進(jìn)行調(diào)整,以指向已讀或已寫數(shù)據(jù)后的下一字節(jié)。因此,連續(xù)的read()和write()調(diào)用將按順序遞進(jìn),對文件進(jìn)行操作。
針對文件描述符fd參數(shù)所指代的已打開文件,lseek()系統(tǒng)調(diào)用依照offset和whence參數(shù)值調(diào)整該文件的偏移量。
#include<unistd.h>
off_t lseek(int fd,off_t offset,int whence) return new file offset if successful ,or -1 on error
offset參數(shù)指定了一個以字節(jié)為單位的數(shù)值,whence參數(shù)則表明應(yīng)參照哪個基點(diǎn)來解釋offset參數(shù)。
whence參數(shù)應(yīng)為下:
SEEK_SET : 將文件偏移量設(shè)置為從文件頭部起始點(diǎn)開始的offset個字節(jié)。----------頭部
SEEK_CUR: 相對于當(dāng)前文件偏移量,將文件偏移量調(diào)整offset個字節(jié)。 --------當(dāng)前位置
SEEK_END:將文件偏移量設(shè)置為起始于文件尾部的offset個字節(jié)。也就是說,offset參數(shù)應(yīng)該從文件最后一個字節(jié)之后的下一個字節(jié)算起。
如果whence參數(shù)值為SEEK_CUR或SEEK_END,offset參數(shù)可以為正數(shù)也可以為負(fù)數(shù);如果whence參數(shù)值為SEEK_SET,offset參數(shù)值必須為非負(fù)數(shù)。
lseek()調(diào)用成功會返回新的文件偏移量。下面的調(diào)用只是獲取文件偏移量的當(dāng)前位置,并沒有修改它。
curr=lseek(fd,0,SEEK_CUR)