Linux C 環境下不帶緩衝的I/O操作函數

摘要:本文主要介紹Linux C 環境下不帶緩衝的文件I/O操作函數——open(),creat(),read(),write(),lseek(),close()。結合實例,簡單地利用單進程和多進程對同一個文件的操作,加深對這些函數的理解。

正文:

一、Open(): 打開或創建一個文件。

1、  簡介(摘自《Unix環境高級編程》第3章 文件I/O,以下幾個函數的簡介都來源於此)

/*頭文件*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/*原型*/
int open(const char *pathname, int oflag, /*mode_t mode*/);     
          

返回:若成功爲文件描述符,若出錯爲- 1並置errno的值。

參數介紹:

Const char *pathname:文件的路徑(即文件所在目錄+文件名);

Int oflag:文件打開標誌選項,實際是一個或多個可作位或運算的宏(定義在<fcntl.h>頭文件中),下面介紹這些宏:

O_RDONLY 只讀打開。

 O_WRONLY 只寫打開。

O_RDWR 讀、寫打開。

    很多實現將 O_RDONLY定義爲0,O_WRONLY定義爲1,O_RDWR定義爲2, 以與早期的系統兼容。 在這三個常數中應當只指定一個。下列常數則是可選擇的:O_APPEND 每次寫時都加到文件的尾端。 3.11節將詳細說明此選擇項。

O_CREAT 若此文件不存在則創建它。使用此選擇項時,需同時說明第三個參數 mode,用其說明該新文件的存取許可權位。 

O_EXCL 如果同時指定了O_CREAT,而文件已經存在,則出錯。這可測試一個文件是否存在,如果不存在則創建此文件成爲一個原子操作。 3.11節將較詳細地說明原子操作。

O_TRUNC 如果此文件存在,而且爲只讀或只寫成功打開,則將其長度截短爲 0。

O_NOCTTY 如果pathname指的是終端設備,則不將此設備分配作爲此進程的控制終端。9.6節將說明控制終端。

O_NONBLOCK 如果pathname指的是一個FIFO、一個塊特殊文件或一個字符特殊文件,則此選擇項爲此文件的本次打開操作和後續的 I / O操作設置非阻塞方式。 1 2 . 2節將說明此工作方式。

O_SYNC 使每次write都等到物理I/O操作完成。3.13節將使用此選擇項。

由open返回的文件描述符一定是最小的未用描述符數字。

       mode_t mode:文件權限位,僅當打開文件不存在,創建文件時才起作用。

       參數 mode 有下列數種組合
S_ISUID 04000 文件的(set user-id on execution)位
S_ISGID 02000 文件的(set group-id on execution)位
S_ISVTX 01000 文件的sticky位
S_IRUSR(S_IREAD) 00400 文件所有者具可讀取權限
S_IWUSR(S_IWRITE)00200 文件所有者具可寫入權限
S_IXUSR(S_IEXEC) 00100 文件所有者具可執行權限
S_IRGRP 00040 用戶組具可讀取權限
S_IWGRP 00020 用戶組具可寫入權限
S_IXGRP 00010 用戶組具可執行權限
S_IROTH 00004 其他用戶具可讀取權限
S_IWOTH 00002 其他用戶具可寫入權限
S_IXOTH 00001 其他用戶具可執行權限
比如要將文件test的權限設置爲644,那麼可以採用以下幾種方法:
open("./test.txt", O_RDWR | O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
open("./test.txt", O_RDWR | O_CREAT, 0644);  //0644是八進制
open("./test.txt", O_RDWR | O_CREAT, 420);   //420是十進制

        4(二進制100)代表讀權限,2(二進制010)代表寫權限,1(二進制001)代表執行權限。

        這幾種方法,第三個參數在內存中實際存的值都是一樣的。

當要打開2G以上的大文件時,要注意幾點:(摘自:http://www.lampchina.net/article/htmls/201006/Mjg4ODkw.html)

1、 包含所有頭文件以前,先定義這些宏:

#ifndef __USE_FILE_OFFSET64
#define __USE_FILE_OFFSET64
#endif

#ifndef __USE_LARGEFILE64
#define __USE_LARGEFILE64
#endif

#ifndef _LARGEFILE64_SOURCE
#define _LARGEFILE64_SOURCE
#endif

2、 使用open打開文件的時候,加上O_LARGEFILE標誌:
int fd = open("test.dat", O_RDWR|O_APPEND|O_CREAT|O_LARGEFILE, 0666);
read(), write()等與一般的用法一致,無變化。

3、 注意lseek()函數,文件未超過2G的時候,一切工作良好;
文件超過2G後,調用返回-1,errno爲EOVERFLOW (errno=75, msg=Value too large for defined data type)
需要使用lseek64()代替lseek()。

4、注意stat()函數,傳入的文件如果在2G內,工作良好;
傳入的文件如果超過2G,返回-1, errno爲EOVERFLOW (errno=75, msg=Value too large for defined data type)
應該這樣使用:struct stat64 st; stat64("file", &st);
此外,還可以用fopen64,也要先加上上面3個宏定義。

二、creat():創建一個文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
Int creat(const char *pahtname, mode_t mode);

返回:若成功爲只寫打開的文件描述符,若出錯爲-1

此函數等效於 open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);  所以該函數現在很少使用,一般用open()代替了。

三、write():向已打開的文件寫數據

#include <unistd.h>
ssize_t write(int filedes, const void *buff, size_t nbytes);

返回:若成功爲已寫入的字節數,若失敗爲-1。ssize_t,表示一個帶符號整型。
參數說明

Int filedes: 文件描述符(可以是文件的,也可以設備的);

Const void *buff: 指向要寫入的數據地址,即寫緩存指針;

size_t mbytes: 將要寫入數據的字節數,即buff的大小,可用strlen(buff)得到。

其返回值通常與參數 nbytes的值不同,否則表示出錯。 write出錯的一個常見原因是:沒有寫權限,磁盤已寫滿,或者超過了對一個給定進程的文件長度限制 。

四、read() :從已打開文件讀數據

#include <unistd.h>
ssize_t read(int filedes, void *buff, size_t nbytes);

返回:若成功爲讀到的字節數。如已到達文件的尾端,則返回 0;若失敗爲-1。

參數說明

Int filedes: 讀取文件的文件描述符;

void *buff: 讀取數據的存放地址,即讀緩存指針;

sizex_t: 預讀取的字節數,實際返回值可能小於它;

有多種情況可使實際讀到的字節數少於要求讀字節數:

讀普通文件時,在讀到要求字節數之前已到達了文件尾端。例如,若在到達文件尾端之前還有30個字節,而要求讀 100個字節,則read返回30,下一次再調用read時,它將返回 0(文件尾端)。

 當從終端設備讀時,通常一次最多讀一行 (第11章將介紹如何改變這一點 )。

當從網絡讀時,網絡中的緩衝機構可能造成返回值小於所要求讀的字節數。

 某些面向記錄的設備,例如磁帶,一次最多返回一個記錄。

 讀操作從文件的當前位移量處開始,在成功返回之前,該位移量增加實際讀得的字節數。

注意,如果buff指向的是數組,且其內容是要作字符串處理,則在buff的最後要手動加上'\0',否則會出現亂碼的情況.。

五、lseek(): 設置文件的當前文件位移量

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int wherece);

返回:若成功爲新的文件位移,若出錯爲-1

參數說明

int filedes: 文件描述符

off_t offset:相對偏移量,可正可負

int wherece: 是預先定義好的宏,SEEK_SET, SEEK_CUR, SEEK_END

若whence是SEEK_SET,則將該文件的位移量設置爲距文件開始處 offset 個字節。

若whence是SEEK_CUR,則將該文件的位移量設置爲其當前值加offset, offset可爲正或負。

若whence是SEEK_END,則將該文件的位移量設置爲文件長度加offset, offset可爲正或負。

這種方法也可用來確定所涉及的文件是否可以設置位移量。如果文件描述符引用的是一個管道或FIFO,則lseek返回-1,並將errno設置爲EPIPE。

六、close():關閉文件。

#include <unistd.h>
int close(int filedes);

返回:若成功爲0,若出錯爲-1

參數int filedes爲文件描述符.

關閉一個文件時也釋放該進程加在該文件上的所有記錄鎖。 當一個進程終止時,它所有的打開文件都由內核自動關閉。很多程序都使用這一功能而不顯式地用close關閉打開的文件。但個人還是主張顯式地用close(),這樣可以提高程序的可讀性,也可減輕內核對進程終止後的後續處理負擔。

 

在本文件的開頭已經提到以上幾個系統函數都是非緩衝的,那麼什麼是帶緩衝的,什麼又是不帶緩衝的呢?!

“緩衝文件系統”的文件操作先將數據 送到內存中的緩衝區, ANSI C函數庫中的fopen()/fread()/fwrite()/fflush()/fclose()等函數操作,只有當緩衝區滿或是出現衝冼緩衝區標誌時纔對緩衝區的數據作作處理。像printf()是行緩衝,當出現’\n’時它才作輸出。這樣可以減少用戶空間與內核空間數據交換次數, 從而降低開銷。

非緩衝,即沒有上面所提的“緩衝區”,直接調用內核對要操作的數據進行處理。像write(),是直接將參數buff指向的數據寫入到文件,而且它的寫操作是原子性的,會將buff的數據一次性寫入到文件。這樣比帶緩衝的數據處理更及時。

理論都是浮雲,下面進入實踐階段。兩個簡單的實例:

例一: 單進程

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <error.h>

int main()
{
         int fd = 0;
         char buf[50];
         memset(buf, 0, 50);
         if ((fd = open("./hello.txt", O_CREAT | O_RDWR | O_APPEND, 0644)) == -1) {
                   perror("open hello.txt");
                   return 0;
         }
         if (3 != write(fd, "aaa", 3)) {
                   perror("write");       
         }
         if (3 != write(fd, "bbb", 3)) {
                   perror("write");       
         }

         lseek(fd, 3, SEEK_SET);
         if (3 != write(fd, "ccc", 3)) {
                   perror("write");       
         }

         lseek(fd, 0, SEEK_SET);
         if (3 != read(fd, buf, 3)) {
                   perror("read");
         }
         buf[3] = '\0';
         if ( 3 != write(fd, buf, 3)) {
                   perror("write");
         }
         close(fd);
         return 0;  
}

 結果:  aaabbbcccaaa

   結果分析:文件打開方式是O_CREAT | O_RDWR | O_APPEND,先寫入aaabbb,因爲有O_APPEND,所以lseek(fd, 3, SEEK_SET);對write(fd, "ccc", 3)不起作用,所以ccc還是從文件結束位置開始寫。但是lseek()對read()起作用,把位移量設在文件開頭處,然後讀取三個字符即aaa,再把aaa寫到文件結尾。

例二:多進程

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <error.h>
 
int main()
{
         int fd = 0;
         pid_t pid;
         char buf[50];
         
         memset(buf, 0, 50);
         pid = fork();
         if (pid < 0) {
             perror("fork failed");
             exit(1);    
         } 

         if (pid == 0) {     /*child process*/                 
             if ((fd = open("./hello.txt", O_CREAT | O_RDWR | O_APPEND, 0644)) == -1) {
                 perror("open hello.txt");
                 return 0;
             }
             write(fd, "aaa", 3);  //爲了代碼簡潔,把這裏的perror()語句去掉了。
               write(fd, "bbb", 3);
             write(fd, "ccc", 3);
             close(fd);
         }        
         else {              /*parent process*/              
             sleep(3);
             if ((fd = open("./hello.txt", O_CREAT | O_RDWR, 0644)) == -1) {
                 perror("open hello.txt");
                 return 0;
             }
             lseek(fd, 3, SEEK_SET);    
             write(fd, "ddd", 3);
             close(fd);
        }

        return 0;  
}

結果:  aaadddccc

結果分析:這個例子是兩個父子進程對同一個文件進行寫操作。先讓父進程休息3秒,以便子進程先執行完再讓父進程執行。子進程寫入aaabbbccc,子進程open()的打開模式是O_CREAT | O_RDWR | O_APPEND;然後,父進程執行,其open()的打開模式是O_CREAT | O_RDWR,先讓sleek()把文件位移量設置在距離文件開始處3個字節,再寫入ddd,從結果來看,ddd把子進程寫的bbb覆蓋掉了,所以最終結果是aaadddccc。

       多個進程對同一個文件進行操作時,每個進程都擁有自己的文件描述符、文件表,它們最終指向的都是同一個文件的V節點表(文件信息),如下圖。

 

                     圖 1   兩個進程打開同一個文件

      爲了節約系統資源,有些系統對它作了改進,採用了寫時複製(copy on write)的技術,即父子進程在打開文件時擁有同一個文件表,當某進程要改變文件表中的某一項時,才把這項複製到另一個內存空間,讓該進程獨享,例如,上面例子的當前文件位移量。

 

小結與拓展:

         本文的主要工作是對linux下不帶緩衝的文件操作函數進行了簡單的總結,第一次寫技術博客,有不足或錯誤的地方還望各位大蝦批評指正,小輩感激不盡!

         還有一些與文件操作相關的函數,這裏就沒寫了,有需要的朋友可以參考查閱:

int dup (int oldfd):用來複制參數oldfd所指的文件描述詞,並將它返回。此新的文件描述詞和參數oldfd指的是同一個文件,共享所有的鎖定、讀寫位置和各項權限或旗標。例如,當利用lseek()對某個文件描述詞作用時,另一個文件描述詞的讀寫位置也會隨着改變。不過,文件描述詞之間並不共享close-on-exec旗標。返回值  當複製成功時,則返回最小及尚未使用的文件描述詞。若有錯誤則返回-1,errno會存放錯誤代碼。

int dup2(int odlfd,int newfd):dup2()用來複制參數oldfd所指的文件描述詞,並將它拷貝至參數newfd後一塊返回。若參數newfd爲一已打開的文件描述詞,則newfd所指的文件會先被關閉。dup2()所複製的文件描述詞,與原來的文件描述詞共享各種文件狀態,詳情可參考dup()。返回值  當複製成功時,則返回最小及尚未使用的文件描述詞。若有錯誤則返回-1,

int fcntl(int fd , int cmd);

int fcntl(int fd,int cmd,long arg);

int fcntl(int fd,int cmd,struct flock * lock);

函數說明  fcntl()用來操作文件描述詞的一些特性。參數fd代表欲設置的文件描述詞,參數cmd代表欲操作的指令。

int   fileno(FILE *stream)返回stream對應的文件描述符。

FILE* fdopen(int filedes,const char* mode)從文件描述符fd 變換到文件流 FILE* ;

int rename(const char *oldpath, const char *newpath);將文件名改名;

int ftruncate(int fd, off_t length); 把描述符fd引用的文件縮短到length指定的長度。成功返回0,失敗返回-1並設置errno。

int fstat(int fd, struct stat *buf); 把描述符fd引用的文件的相關信息保存到buf指向的stat結構中。成功返回0,失敗返回-1並設置errno。

int fsync(int fd); 把在fd上執行的寫入操作同步到真正的磁盤或其它下層設備文件中。成功返回0,失敗返回-1並設置errno。

int flock(int fd,int operation); flock()會依參數operation所指定的方式對參數fd所指的文件做各種鎖定或解除鎖定的動作。此函數只能鎖定整個文件,無法鎖定文件的某一區域。

 

PS: 本文亦可在下面鏈接可見

http://dev.jizhiinfo.net/?post=27

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章