文件操作的系統調用接口:
文件是Linux系統中的重要概念。它不僅僅是對普通文件的操作接口,也是設備通信、進程間通信、網絡通信的重要編程接口。因
此文件操作的相關調用也是Linux內核提供的最重要的編程接口。
本節將重點敘述如下幾個常用的文件操作系統調用。
open:打開文件。
read:從已打開的文件中讀取數據。
write:向已打開的文件中寫入數據。
close:關閉已打開的文件。
ioctl:向文件傳遞控制信息或發出控制命令。
對文件的操作工程一般是這樣的:先打開文件,內核對打開的文件進行管理,打開成功后應用程序將獲得文件描述符;然后應用程
序使用文件描述符對文件進行讀寫操作;當全部操作完畢后,應用程序需要將文件關閉以釋放用於管理打開文件的內存。
文件描述符是一個取值從0開始的整數。內核默認一個進程同時打開的文件數有一個上限,也就是文件描述符取值的上限,一般是
1024。
每個進程在啟動后就默認有三個打開的文件描述符0,1,2,如果啟動程序時沒有進行重定向,則文件描述符0關聯到標准輸入,1關
聯到標准輸出,2關聯到標准錯誤輸出。在C庫函數中可以使用以下幾個宏來表示這幾個文件描述符:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
打開文件:
在訪問文件之前,首先應打開文件。可以使用open或creat函數來打開文件,它們的接口頭文件及函數原型如下:
#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);
int creat(const char *pathname, mode_t mode);
其各個參數及返回值的含義解釋如下.
◆ pathname:要打開的文件名稱。
◆ flags:標志位,指定打開文件的操作方式及打開時的一些行為。
◆ mode: 用於指定新文件的權限標志位。
◆ 返回值:操作成功則返回文件描述符,否則返回-1並設置標量errno的值。
flags參數有以下幾個基本的取值。
◆ O_RDONLY:以只讀方式打開文件。
◆ O_WRONLY:以只寫方式打開文件。
◆ O_RDWR:以讀寫方式打開文件。
這幾個標志位指定打開文件的操作方式,它們是互斥的,不能同時使用,但可以與下述標志用按位或的方式組合起來使用。
◆ O_CREAT:如果被打開的文件不存在,則自動創建這個文件。
◆ O_EXCL:如果O_CREAT標志已經使用,那么當由pathname參數指定的文件已經存在時open函數返回失敗。如果pathname給出的是
一個符號鏈接,無論它指向的文件是否存在,對open函數的調用都會返回失敗。
◆ O_NOCITY:如果被打開的文件是一個終端設備文件(如/dev/tty),它不會成為這個進程的控制終端。
◆ O_TRUNC:如果被打開的文件存在並且是以可寫的方式打開的,則清空文件原有內容。
◆ O_APPEND:新寫入的內容將被附加在文件原來的內容之后,即打開后文件的讀寫位置被置於文件尾。
◆ O_NONBLOCK:被打開的文件將以非阻塞的方式進行操作。
◆ O_NDELAY:同O_NONBLOCK。
◆ O_SYNC:被打開的文件將以同步I/O的方式進行操作,即任何寫操作都會先被同步到硬件設備上。同步完成后,對寫函數的調用
才返回。
◆ O_NOFOLLOW:如果pathname是一個符號鏈接,則對open函數的調用將返回失敗。
◆ O_DIRECTORY:如果pathname不是目錄,則對open函數的調用將返回失敗。
需要注意的是,open函數有兩個原型,其中一個多出了個參數mode,它用於指定創建的新文件的訪問權限。如果打開時使用了
O_CREAT標志創建新文件,則一般都要給出mode參數,它的一些常用取值如表所示,這些值可以用按位或的方式組合使用。新文件的所屬用
戶和所屬組則是創建它的進程的所屬用戶和所屬組。
權限標志定義 對應的八進制形式 含義
S_IRWXU 00700 文件所屬用戶有讀寫和執行權限
S_IRUSR(S_IREAD) 00400 文件所屬用戶有讀權限
S_IWUSR(S_IWRITE) 00200 文件所屬用戶有寫權限
S_IXUSR(S_IEXEC) 00100 文件所屬用戶有執行權限
S_IRWXG 00070 組內用戶有讀寫和執行權限
S_IRGRP 00040 組內用戶有讀權限
S_IWGRP 00020 組內用戶有寫權限
S_IXGRP 00010 組內用戶有執行權限
S_IRWXO 00007 其他用戶有讀寫和執行權限
S_IROTH 00004 其他用戶有讀權限
S_IWOTH 00002 其他用戶有寫權限
S_IXOTH 00001 其他用戶有執行權限
鑒於在調用open函數時,O_WRONLY,O_CREAT,O_TRUNC三個標志位經常組合使用,因此由一個專門的函數creat來實現。如下:
creat(pathname, mode);
實際上等價於:
open(pathname,O_WRONLY|O_CREAT|O_TRUNC,mode);
這兩個函數在打開文件成功時將返回一個文件描述符,可用於隨后的read/write或其他對文件的操作使用。兩個不同的進程打開同
一個文件是允許的,但它們得到的文件描述符一般是不同的。如果它們都對文件進行寫操作,就會出現數據不一致的情況,也就是最后寫入
的可能覆蓋先前其他進程寫入的內容,這就涉及到進程間數據共享和同步的概念了。
如果打開操作失敗,這兩個函數會返回-1,並將errno變量設置為一個合適的錯誤值。
從文件讀取數據:
文件打開后就可以進行讀寫操作了。讀操作的接口頭文件及函數原型如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
其各個參數及返回值的含義解釋如下:
◆ fd:要讀取的文件描述符。
◆ buf:指向讀取到的數據要放入的緩沖區。(buf指向的內存空間必須事先分配好)
◆ count:要讀取的字節數(緩沖區大小)。
◆ 返回值:讀取到的字節數,失敗返回-1,並設置標量errno的值。
這里的size_t型實際上就是無符號整型,而ssize_t型就是有符號整型。
這個函數將從fd代表的文件的當前讀寫位置讀取不超過count個字節到buf指向的內存中,並返回讀到的字節數。
對於普通文件來說,讀操作完成后,文件的讀寫位置會向后移動,移動的長度就是讀取的字節數,下一次讀操作將從新的讀寫位置
開始。
read函數的返回值小於指定的count是可能的,並不是錯誤。出現這種情況有各種原因,比如,文件本身可供讀取的字節數比count
小或者read系統調用被信號打斷等。read系統調用看似簡單,但實際上對於各種可能情況的處理是比較復雜的,因為I/O操作有很多異常情
況要考慮到。下面列出了read系統調用中可能遇到的情況及其處理方法。
◆ 調用返回值等於count,讀取的數據存放在buf指向的內存中,結果與預期一致。
◆ 調用返回一個大於0小於count的值,讀取的字節數存放在buf指向的內存中。出現這種情況可能是一個信號打斷了讀取過程,或
在讀取中發生了一個錯誤,或讀取的有效字節數大於0但不足count個,或在讀入count個字節前文件已經結束。如果讀取
的過程被信號打斷則可以再次進行讀取。
◆ 調用返回0,說明文件已結束,沒有可以讀入的數據。
◆ 調用阻塞,說明沒有可讀的數據,這種情況下如果以非阻塞方式操作文件,那么會立即返回錯誤。
◆ 調用返回-1,並且errno變量被設置為EINTR,表示在讀入有效字節前收到一個信號,這種情況可以重新進行讀操作。
◆ 調用返回-1,並且errno變量被設置為EAGAIN,這說明是在非阻塞方式下讀文件,並且沒有可讀的數據。
◆ 調用返回-1,並且errno變量被設置為非EINTR或EAGAIN的值,表示有其他類型的錯誤發生,必須根據具體情況進行處理。
由於有各種異常情況的存在,為了從文件中可靠的讀取指定的字節數,就必須對這些情況進行處理,必要時需重新進行讀操作。這
對於設備文件、管道或socket來說尤其有意義。例如,用下面的代碼能夠可靠的從文件中讀取指定的字節數(假設文件是以阻塞的方式進行
操作的):
ssize_t ret;
while(len != 0 && (ret = read(fd,buf,len)) != 0)
{
if(ret == -1)
{
if(errno == EINTR) continue;
perror("read");
break;
}
len -= ret;
buf += ret;
}
這里把操作放在循環中,當一次讀操作沒有得到len個字節時,將調整len和buf的值繼續進行操作。如果讀操作返回-1,說明有錯
誤發生,這時如果錯誤碼是EINTR,說明只是被信號打斷,可以繼續讀,其他情況則被認為是嚴重的錯誤,不能再繼續讀。
以上是以阻塞方式讀文件的例子,這時,如果文件是一個設備文件並且設備沒有可讀的數據,進程將進入睡眠狀態不再繼續執行,
或者說阻塞在read系統調用處,直到設備有了可讀的數據才會被喚醒繼續執行。很多時候我們需要進程能夠立刻返回以處理其他的事物,那
么就需要采用非阻塞的方式來操作文件,舉例如下:
ssize_t nr;
start:
nr = read(fd, buf, len);
if(nr == -1)
{
if(errno == EINTR) goto start;
if(errno == EAGAIN)
{
/*處理其他事物,在恰當時再調用read*/
}
else
{
/*有錯誤發生,處理錯誤*/
}
}
可以看到,在采用非阻塞的方式讀文件時,如果讀操作返回-1,我們必須檢查錯誤碼是否為EINTR或EAGAIN,如果是EINTR,可以再
次進行讀操作,而如果是EAGAIN,表示要讀取的(設備)文件現在沒有可供讀取的數據,因此進程可以繼續處理其他事物,而后在恰當的時
機再來讀取這個文件。
寫數據到文件:
向打開文件中寫入數據的接口頭文件及函數原型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
其各個參數及返回值的含義解釋如下。
◆ fd:要寫入的文件的描述符。
◆ buf:指向要寫入的數據所存放的緩沖區。
◆ count:要寫入的字節數。
◆ 返回值:實際寫入的字節數,失敗則返回-1,並設置變量errno的值。
這個函數會從fd所代表的文件當前讀寫位置開始,把buf指向的內存中最多count個字節寫入文件。寫入成功則返回寫入的字節數,
並更新文件的讀寫位置。
write系統調用返回大於0而小於count的值是合法的,並不表示有錯誤發生。
◆ 調用返回值等於count,說明數據全部寫入成功。
◆ 調用返回一個大於0小於count的值,說明部分數據沒有寫入。這可能是因為寫入過程被信號打斷,或者底層的設備暫時沒有足
夠的空間存放寫入的數據。
◆ 調用阻塞,說明暫時不能寫入數據,這種情況下如果以非阻塞方式操作文件,那么會立即返回錯誤。
◆ 調用返回-1,並且errno變量被設置為EINTR,表示在寫入一個有效字節前,收到一個信號,應用程序可以再次進行寫操作。
◆ 調用返回-1,並且errno變量被設置為EAGAIN,說明是在非阻塞方式下寫文件但文件暫時不能寫入數據。
◆ 調用返回-1,並且errno變量被設置為EBADF,表示給定的文件描述符非法,或者文件不是以寫方式打開。
◆ 調用返回-1,並且errno變量被設置為EFAULT,表示buf是無效的指針。
◆ 調用返回-1,並且errno變量被設置為EFBIG,表示寫入的數據超過了最大的文件尺寸,或者超過了允許的文件讀寫位置。
◆ 調用返回-1,並且errno變量被設置為EPIPE,說明寫入時發生了數據通道斷層的錯誤,這種情況只在文件是管道或者是socket
的情況下發生。在這種情況下,進程還將收到一個SIGPIPE信號,信號的默認處理程序是使進程退出。
◆ 調用返回-1,並且errno變量被設置為ENOSPC,說明底層設備沒有足夠的空間。
寫文件舉例如下:
ssize_t ret;
while(len != 0 && (ret = write(fd, buf, len)) != 0)
{
if(ret == -1)
{
if(errno == EINTR) continue;
perror("write");
break;
}
len -= ret;
buf += ret;
}
這里假定寫操作是以阻塞的方式進行的。如果以非阻塞方式進行寫操作,則當函數返回-1時,必須檢測errno變量的值是否為
EAGAIN,以決定能否再進行寫操作。
發送控制命令:
在Linux系統上,那些不能被抽象為讀和寫的文件操作統一由ioctl操作代表。ioctl操作用於向文件發送控制命令,這些命令不能
被視為是輸入輸出流的一部分,而只是影響文件的操作方式。對於設備文件來說,ioctl操作常用於修改設備的參數。
ioctl系統調用的接口頭文件及函數原型如下:
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...);
◆ fd:要操作的文件描述符。
◆ request:代表要進行的操作,不同的(設備)文件有不同的定義。
◆ 可變參數:取決於request參數,通常是一個指向變量或結構體的指針。
◆ 返回值:成功返回0,有效ioctl操作返回其他非負值,錯誤返回-1。
ioctl能夠進行的操作根據fd所代表的文件的具體類型而變化,非常繁多。下面舉一個例子,使用TIOCGWINSZ命令獲得終端的窗口
大小,如下:
/*文件名:console_size.c*/ /*說明:使用ioctl獲得控制台窗口的大小*/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> int main(void) { struct winsize size; /*判斷標准輸出是否為tty設備,防止輸出被重定向的情況*/ if(!isatty(STDOUT_FILENO) < 0) return -1; /*獲得窗口大小*/ if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) { perror("ioctl TIOCGWINSZ error"); return -1; } /*輸出結果*/ printf("rows is %d, columns is %d\n", size.ws_row, size.ws_col); return 0; }
關閉文件:
程序完成對文件的操作后,要使用close系統調用將文件關閉,其接口頭文件與函數原型如下:
#include <unistd.h>
int close(int fd);
fd是要關閉的文件的描述符,返回值在操作成功的情況下是0,否則是-1.
