linux_api之文件操作


本篇索引:

1、引言

2、文件描述符

3open函數

4close函數

5read函數

6write函數

7lseek函數

8i/o效率問題

9、內核用以維護打開文件的相關數據結構

10O_APPEND標志

11dup函數(文件描述符重定位函數)

12、有關文件共享的問題

13fcntl函數

14ioctl函數

 

 

 

 

1、引言

1.1、文件io這個詞的含義

實現對文件的數據輸入(input)和輸出(output),所以簡稱為文件io

1.2、什么需要文件io

程序的目的是為了處理信息,而信息在計算機中的表現形式就是數據,所以程序就是為了處理數據並輸出數據,而所有的數據幾乎都與文件有關(linux下一切皆是文件),所以程序就必須實現對文件的讀寫。

對於有OS的計算機來說,應用程序是無法直接讀寫文件的(隔離保護作用),文件基本是靠下層的機制,必須經過OS才能訪問,所以我們就必須學習linux專門提供給應用程序,讓其實現文件io操作的系統調用函數,利用這些專門的文件io接口實現對文件的訪問。

1.3、文件io函數

常見的文件io函數有openreadwritecloselseek這五個,我們經過第二篇的學習,已經知道,相對於標准io來說文件io常被稱為不帶緩存的io。在前面我們也說過,這幾個系統調用不是ANSI C的組成部分,但是這幾個系統函數的函數原型確是ANSI C提供的。

1.4、原子操作

當多個進程共享同一資源,就比如當多個進程都想對同一文件操作,那么原子操作的概念是非常重要的。比如,A進程寫xxx文件時,如果某個條件未發生,那么B進程無論如何都不能寫該文件,直到A進程一直寫到該條件發生,才輪到B進程寫。B進程寫時也是如此,這樣就避免了AB之間互相串改對方寫入文件的數據的可能,本片會通過一個O_APPEND標志給大家引入原子操作的概念,,后續課程我們還會再次接觸到。

2、文件描述符

2.1、文件指針和文件描述符

我們學習標准io時知道,標准io實現對文件讀寫操作時,用的是文件指針FILE*fp。在第二篇中我們也說了,如果是在linux OS下,標准io向下繼續調用時,實際調用的還是文件io,而文件IO則使用文件描述符來實現對文件的操作,該文件描述符就存在了文件指針fp指向的結構體中。

2.2、什么是文件描述符

每成功打開(打開文件用文件路徑)一個文件,內核都會返回一個非負的整數,readwrite時就用此整數進行操作,該整數一般都是在調用opencreate函數時返回的,這個整數就是文件描述符。

linux下,一般來說每個進程可以使用的文件描述符都是在0~1023之間,總共1024個可用文件描述符,當然上限值是可以更改的。其中0與標准輸入、1與標准輸出、2與出錯輸出結合在了一起,因為系統啟動時按順序打開了標准輸入,輸出,出錯輸出三個文件,所以012也與這三個文件順序的結合在了一起,之后每個運行的進程都將繼承這三個文件描述符,所以之后的每個進程不必再次打開這三個文件,就可以直接使用。這就是為什么scanfprintf函數能夠直接被使用的原因了。

這三個文件描述符對應STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO這三個宏,分別定義在了<unistd.h>中,鼓勵使用宏而不是直接使用012數字,目的是為了提高程序的可辨識度和跨平台操作性。

3open函數

3.1、函數原型和所需頭文件

 #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);

3.2、函數功能

int open(const char *pathname, int flags);按照flags的要求打開已存在的文件,如果文件不存在則報錯。

int open(const char *pathname, int flags, mode_t mode);如果文件已經存在就直接按照flags的要求打開文件。如 果文件事先不存在,則按照第三個參數的權限要求創建一個新的文件,然后再按照flags要求打開文件。

3.3、參數說明

3.3.1、第一個參數:const char *pathname

文件路徑。

3.3.2、第二個參數:int flags,打開文件的方式

flags由如下宏選項中的一個或多個,通過|運算組成。

1)、O_RDONLY:只讀方式打開文件     

2)、O_WRONLY:只寫方式打開文件

3)、O_RDWR:可讀可寫方式打開文件

以上這三個只能指定其中一個,不可組合。

4)、O_APPEND:每次在文件末尾追加信息,此選項很重要,后面會詳說。

5)、O_ASYNC:異步標志,后面學習異步通知時將會用到。

6)、O_CLOEXEC:如果該進程exec新程序后,打開的文件將自動關閉,該描述符與該文件的結合無效。

在后面學習進程控制時,我們會對此進行舉例說明。

7)、O_CREAT:指定了該標志后,如果文件不存在則創建一個該名字的文件,但這需要用到第三個參數

來指定新創建文件的原始權限。

8)、O_EXCL指定這個標志時,O_CREAT必須也被指定,這兩個標志聯合使用可實文件存在則報錯 的功能。因為有時我們就是需要創建出一個不與現存任何文件同名的新文件,如果發現該文件是一 個已經存在的文件我們必須報錯,然后重新命名創建文件。使用O_EXCL就能實現這樣的功能,否

則的話,這個已經存在的文件會被直接打開並使用,這與我們的願望相違背。

9)、O_TRUNC:打開時將文件內容全部清零空

10)、O_NOCTTY:如果打開的文件是一個終端備文件的話,不要將此終端指定為控制終端,比如以后我 們涉及打開一個串口的時候,就會用到這個標志。

11)、O_NONBLOCK:這個只能用在字符類設備或網絡設備文件上,對於磁盤上的普通文件是無效的。打開字符類設備文件時,默認就是以阻塞方式打開的,但是我們可以將其改為非阻塞的。大家知道getcharscanf等函數實現鍵盤輸入時會導致阻塞,就是因為這個原因。阻塞與非阻塞有時是由文件類型決定的,因為同一個函數操作A文件時是阻塞,但是操作B文件時卻是非阻塞的。但是有時又是由函數本身的特性決定的,導致阻塞的原因要視情況而定,后面講信號時我們將詳細討論此問題。

12)、O_SYNC:同步標志,write系統調用會一直等到,直到物理設備讀寫完畢后才會返回到應用層。如果不指定的話,write只需要將內容寫到內核緩存中后就立即返回,剩下的事情就由內核定時將內核緩存中的數據分批寫到物理設備上。

3.4、返回值

函數調用成功返回進程描述符集合中(0~1023)當前最小且未用的描述符,失敗返回-1,並設置errno

3.5、簡單用例

3.5.1、打開已有文件

vitouch出一個名叫“file”的文件。

int main(void){        
int fd = -1; 
        fd = open("file", O_RDWR);
        if(fd < 0)
        {           perror("open is fail");
                exit(-1); 
          }   
        return 0;
}

3.5.2、如果文件存在則直接打開,不存在則新建一個該名字的文件,然后再打開

int main(void){
int fd = -1; 
        fd = open("file", O_RDWR|O_CREAT, 0664);//指定文件的權限
        if(fd < 0)
        {           perror("open is fail");
                exit(-1);  
}   
        return 0;
} 

3.5.2、如果存在報錯

 

int main(void)
{
int fd = -1; 
        fd = open("file", O_RDWR|O_CREATE|O_EXCL, 0664);
        if(fd < 0)
        {   
                perror("open is fail");
                exit(-1);
        }   

        return 0;
} 

我們可以在出錯處理中重新創建一個名叫“file1”的文件,如該名字的文件也已經有了,那就再換一個名字,直到找到一個不沖突的名字為止。

理解O_EXEC標志對於我們理解一些其它系統調用的類似的xxx_EXEC標志是很有幫助的,因為基本思想是一致的。

3.6、注意點

打開文件時,打開方式必須符合文件創建時文件的創建權限。換句話說,創建文件時的權限不允許寫,你卻想要以寫方式打開,這會導致函數調用失敗,這里說的很籠統,下章會對此做詳解。

4close函數

4.1、函數原型和頭文件

#include <unistd.h>

#int close(int fd);   

4.2、函數功能說明

關閉打開的文件。

4.3、函數參數

int fd:文件描述符

4.4、返回值

調用成功返回0,失敗返回-1errno被設置

4.5、測試用例:略

4.6、注意點

Close函數可不必顯示調用,因為程序正常結束時會隱式的調用該函數,記住這里說的是程序正常結束,后面講進程控制時會告訴大家什么是程序異常結束,這里只須簡單記住,exitmain函數中return都可以正常退出。

5read函數

5.1、函數原型和頭文件

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

5.2、函數功能說明

以字節單位,按塊(一塊包含很多字節)讀取文件中的數據到用戶緩存。

5.3、函數參數

5.3.1、第一個參數int fd文件描述符

5.3.2、第二個參數void *buf用戶緩存

5.3.3、第三個參數size_t count指定讀取一次的塊大小,換句話說一塊包含count字節

5.4、返回值

調用成功返回read函數實際讀取到的字節數,如果失敗,返回-1,並且errro被設置

5.5、測試用例:略

5.6、注意點

如果read調用成功,則返回實際讀取的到字節數,0=<該字節數<=count,當讀到文件末尾時,讀取到的字節數很有可能實際小於count的要求,這是很正常的,如果返回0代表已經讀取到文件末尾。

linux並不區分文本二進制和純二進制,read函數都以字節為單位讀取,至於拿到這些數據后,如何處理或解釋那就是應用程序所要做的事情了,read並不關心讀到的是文本二進制還是純二進制。

6write函數

6.1、函數原型和頭文件      

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

6.2、函數功能說明

以字節單位,按塊將用戶數據寫入文件中。

6.3、函數參數

5.3.1、第一個參數int fd文件描述符

5.3.2、第二個參數void *buf用戶緩存或直接用戶數據

5.3.3、第三個參數size_t count指定寫入一次塊的大小(以字節為單位計算)

6.4、返回值

調用成功,返回write函數實際成功寫入的字節數,如果返回0表示無數據寫入文件。如果失敗,返回-1並且errro被設置。

6.5、測試用例:略

6.6、注意點:暫無

7lseek函數

7.1、函數原型和頭文件

#include <sys/types.h>

#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

7.2、函數功能說明

定位對文件的操作位置,就像在紙上寫字時調動筆尖到某處一樣。功能同標准io中的fseek函數,可認為fseek函數就是對lseek函數做的一個封裝。其實標准io中的fseekftell函數向下調用的都是是lseek,因為這個函數兼具這兩個方面的功能,即能調動對文件的操作位置,又能返回文件的當前位移量。

7.3、函數參數

7.3.1、第一個參數:int fd

文件描述符

7.3.2、第二個參數:off_t offset

精確定位,負數代表從現在的位置向前移動offset字節,正數代表從當前位置向后移動offset字節,當對文件的操作位置定位在了文件的起始位置時,那么再向前移動的話沒有意義,函數會報錯返回。

7.3.3、第三個參數:int whence

粗定位,選項有SEEK_SET:調到文件起始位置,SEEK_CUR:調到文件當前位置,SEEK_END:調到文件末尾的位置。

7.4、返回值

成功,返回文件的當前位移量,失敗返回-1errno被設置。

7.5、測試用例: 略

7.6、注意點

此函數只能對普通文件進行操作,不能對字符設備,管道等其它文件操作,因為這些文件在磁盤上只有屬性信息,並沒有真實的數據存放,lseek定位毫無意義。

Lseek可以用來實現空洞文件,但是lseek的調動后,必須使用write函數向文件里寫點數據,該調動結果才能被記錄下來。

一般來說按照正常情況打開一個文件時,文件的“當前位移量”為0(筆尖放在了文件在開始的位置),讀寫數據時從文件的最開始處進行,但是如果我們打開指定了O_APPEND標志的話,筆尖回調到文件的末尾,當前文件位移量為文件長度,這一點我們需要注意。

7.7、函數一些特殊用法

lseek函數可以用來構建空動文件,空洞文件一種很有用的文件,這可實現多線程並發地同時向文件不同區域寫數據。先看lseek如何被用來構建空洞文件的。

int main(void)
{                    
        int fd = -1; 
        off_t f_len = -1; 
            
        fd = open("file", O_CREAT|O_RDWR, 0664) ;
        if(fd < 0)
        {   
                perror("open is fail");
                exit(-1);
        }    
                    
        /* 返回新打開文件的文件長度,其結果肯定時0 */
        f_len = lseek(fd, 0, SEEK_END);
        printf("f_len = %d\n", f_len);

        /* 文件指針向后移動十個字節 */
        f_len = lseek(fd, 10, SEEK_SET);
        printf("f_len = %d\n", f_len);  
        /* lseek只是調動文件操作位置,不會去修改文件,需要人為的調
         * 用write函數去修改下文件,否則無法固定下lseek的修改結果 */
        write(fd, "a", 1);    
            
        return 0;
}   

查看文件大小

[linux@localhost 1402]$ ls -al file 

-rw-rw-r--. 1 linux linux 11 Apr 11 15:04 file

查看文件內容

[linux@localhost 1402]$ od file -c

0000000  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0   a

空洞文件中,前面空的部分為0

8i/o效率問題

先看下面的例子。

#definen BUFSIZE m; //m是正整數

char buf(BUFSIZE);

read(fd, buf, sizeof(buf));

如果read函數讀1000000字節的文件,每次讀出的內容都會先復制到用戶緩存buf中。實際上這一過程存在着一個效率問題,那就是如果用戶緩存過小會導致讀文件的效率很低。打個比方,如果把一盤滿滿的花生米從一個地方運到另一個地方,但是我每次只允許你運一顆,估計你會覺得十分惡心,如果每次運十顆,每次運送一百顆,相對會好很多,當然一次就將整盤端過去那是最好的。

所以read讀數據就類似於將底層的花生米分批次的運送到上層去,每次運送的量由用戶緩存的大小決定的,所以用戶緩存的大小直接決定着read系統調用的次數的多少,次數越多效率越低。當然緩存也不可能無限制的大,測試表明用戶緩存開到8192時,效率基本趨於穩定,再大已經沒有任何意義,就類似於千萬富翁和億萬富翁的物質生活水平其實是一樣的,從滿足好的生活需求的角度來說,錢再多已經沒有太大意義了。

雖然理論上8192是庫緩存的最佳長度,但是我們庫緩存的默認長度一般是位5121024的長度,這個長度是內核按塊操作的塊長度。實際上1024的效率比8192的效率只差一點點而已,但是卻省了很多的空間。

其實以上就是為什么在文件io的上面再次封裝出一個標准io的原因了,因為它會幫我們選取一個最佳的緩存大小,這個緩存其實就是我們習慣上稱呼的庫緩存,並且這個緩存還是分類型(無,行,全),根據類型的不同執行不同的緩存刷新操作。

當然我們說標准io還有一個目的,就是為了實現c語言程序能夠誇系統的運行,不同OS向下調用不同的文件io

9、內核用以維護打開文件的相關數據結構

為了很好的理解這些結構,我們事先必須清楚如下概念。

9.1、進程表

內核為每個運行的進程生成了一張進程表,這張表很重要,因為其中記錄了所有與進程相關的信息,內核需要利用它來管理進程。

內核中的表要么用結構體,要么用數組,要么就是用鏈表來實現,這里的進程表就是一個名叫task_struct的結構體,它是我們linux內核中最大的結構體,大概有240多個成員項。

9.2、文件描述符表

我們可以簡單的認為,這張表直接存在了進程表中。該表記錄了該進程打開的所有文件的文件描述符信息,

文件描述表中的每一項記錄了如下信息。

9.2.1具體使用的文件描述符,我們對它已經很熟了。

9.2.2一個指向文件表的指針。

9.3、文件表

該表記錄了被打開文件的狀態信息等。

9.3.1文件狀態標志(O_RDONLY, O_RDWR, O_NONBLOCK, O_TRUNC, O_APPEND等)

9.3.2當前文件位移量(前面學過lseek函數,它可以改變這個當前位移量)

9.3.3指向v節點(也是一個結構)的指針,描述對打開文件的具體操作信息。

9.4v節點

v節點包含的具體操作文件的信息如下。

9.4.1對各類不同文件的具體操作函數的函數指針。

9.4.2包含了索引節點(i節點:記錄文件屬性)信息,這些信息是在open一個文件時直接從磁盤的i節點中讀出,然后寫入v節點中(v數據結構開在了內存中)。將磁盤上的i節點寫入內存的目的是為了實現快速使

用(因為讀磁盤的速度太慢)。

文件屬性包含,文件類型、文件大小、文件所有者、文件的創建權限等等,后面的章節會詳細講解。

9.5、進程表,文件描述符表,文件表,v節點之間的基本關系

 

9.5.1、注意點

a)前面也說過,正常情況下每個進的012三個文件描述符是系統一早就打開好了的,直接使用即可,

b)當前位移量和當前文件長度是兩個完全不同的概念,請嚴格區分。並且它們各自存放的位置是不同的,

因為這對於實現文件共享和原子操作來說是十分重要的,后面會對此進行詳細講解。

9.6、有關v節點的進一步說明

V節點實際上是一個與虛擬文件系統相關的東西,它是virtual的縮寫。虛擬文件系統是文件系統分層思想的體現,對於現代計算機而言,分層是一個十分重要的思想,這一概念會在后續的課程中反復接觸到。

虛擬文件系統就是對各種文件系統中相同的部分做一個統一的抽象,不種類同的文件系統就擁有了相同的接口,這對於不同種類的文件系統的兼容是異常重要的。

上層操作文件時,調用的都是統一的接口,如openreadwritecloselseek等等,但是真正底層在操作文件時,肯定會根據文件類型的不同,實際對文件進行操作的函數也會不同,那么相同的接口走到下面時具體的操作會發生分流,大致的圖示如下,這里以read為例進行說明。

 

 10O_APPEND標志

10.1A_PPEND標志的作用

我們前面學習open函數的時候,說過這個標志,說它的作用是追加,這個描述還不是很准確,准確說是利

用文件長度去修改文件的當前位移量,然后再對做各種操作。圖示如下:

 

此標志對於利用多個文件描述符實現對同一文件共享來說很重要,這些描述符是多次調用open函數打開同一文件取得的,而且不管這些open操作是由單進程還是多進程實現的。沒有它的話,在利用各個描述符對文件操作時會導致這些操作相互覆蓋,特別是對於寫操作來說(讀沒有太大關系),O_APPEND標志顯得尤為重要,比如下面的這個例子:

int main(void)
{
        int fd1 = -1, fd2 = -1, ret = -1; 
        fd1 = open("file", O_CREAT|O_RDWR|O_TRUNC, 0664);
        fd2 = open("file", O_CREAT|O_RDWR|O_TRUNC, 0664);
        printf("fd1 = %d, fd2 = %d\n", fd1, fd2); 
        while(1)
{   
                write(fd1, "hello\n", 6); 
                sleep(1);
                write(fd2, "world\n", 6); 
        }   
        return 0;
}

在這個代碼中,file文件被打開了兩次,所以fd1fd2都結合上了file文件。但是打開文件看到,期望的結果和實際的結果確是不一樣的:

1 hello                      1 world
2 world                        2 world
3 hello                      3 world
4 world                      4 hello                      
5 hello      
6 world
7 hello
8 world
9 hello
期望的結果                    實際的結果

導致上面結果的原因很簡單,就是world覆蓋了hello,最后一個hello沒有被覆蓋的原因是因為sleep休眠延緩了這一覆蓋的發生。想對結果進行分析,依賴於我們對於內核維護打開file的數據結構的理解。

10.1.1、同一進程內,多次open打開同一文件后的情況

上面的例子程序運行起來后就是一個進程。 這個進程里面對file文件打開了兩次,fd1fd2都指向了file文件,那么內核維護打開文件的數據結構如下:

 

看到了上面這個圖,我想很多同學已經明白導致覆蓋的原因了,其實很簡單,因為fd1fd2分別指向了兩個不同的文件表,所以都擁有自己的文件位移量。開始各自的文件位移量都為0。但是為什么擁有相同的v節點呢,畢竟它們對應的都是同一個文件file,否則就不能說fd1fd2結合上了同一個文件file

比如用fd1寫“hello\n”,它的位移量從0增到了6。但是fd2的初始文件位移量也為0,用fd2寫時,fd2的初始文件位移量也為0,也從文件的最開始位置寫“world\n”,所以”world\n”會覆蓋”hello\n”,以后的依次類推,這就是導致覆蓋的原因了。

為了解決這個問題,我們打開時加入了O_APPEND標志,修改后的open函數如下,着重看紅色部分:

fd1 = open("file", O_CREAT|O_RDWR|O_TRUNC|O_APPEND, 0664);

fd2 = open("file", O_CREAT|O_RDWR|O_TRUNC|O_APPEND, 0664);

運行結果如下:

1 hello
2 world
3 hello
4 world
5 hello
6 world
7 hello
8 world
實現了期望的結果

因為每次操作前必須先用文件長度修改當前位移量。以前面的例子進行說明,在用fd1進行向file文件寫 “hello’前,先用文件長度(開始為0)更新fd1的文件位移量(開始也為0),寫完“hello\n”后,這樣fd1的文件位移量從0變為了6,同時file的文件長度也變為了6,但是fd2的文件位移量卻任然是0,但是在指定了O_APPEND標志后,在write前會用文件長度修改fd2的文件位移量,這樣fd2的文件位移量變為了6,用fd2寫時則會從位移量6開始寫起,如此一來就不會導致相互覆蓋了。

10.1.1、多個上進程分別open同一文件后進行文件操作的情況

上面講的都是在一個進程里面的操作,多個進程分別打開同一個文件,多個進程各自對同一個文件操作也涉及到相互覆蓋的問題,其實導致的原因與上述情況的原因基本一致,但是維護打開文件的結構卻略有不同的,看如下例子,兩個程序AB,分別打開同一個文件file,各自都對file進行寫。

int main(void)
{
        int fd1 = -1, fd2 = -1, ret = -1;
        ret = fork();
        if(ret > 0) // 第一個進程
        {
                fd1 = open("file", O_CREAT|O_RDWR|O_TRUNC, 0664);
                while(1)
                {
                        write(fd1, "hello\n", 6);
                        sleep(1);
                }
        }
        else if(0 == ret)//第二個進程
        {
                fd2 = open("file", O_CREAT|O_RDWR|O_TRUNC, 0664);
                while(1)
                {
                        sleep(1);
                        write(fd2, "world\n", 6);
                }
        }
        return 0;
}

上面例子涉及fork函數的使用,實際上我們在講后面多進程時才會涉及到該函數,但是我們這里先接觸下也是很有好處的。這個例子會運行兩個進程,我們可以簡單地認為紅色部分代碼運行起來為一個進程,藍色部分代碼運行起來為一個進程。這兩個進程再各自運行的時間片到了后會相互切換(簡單認為就是A的時間片到則B運行,B的時間片到則A運行,直到兩個進程運行結束),時間片到可以簡單認為就是定時的時間到了,由於時間片非常短,從而實現了兩個進程同時向前運行的錯覺。

以上兩個進程各自都會獨立的打開file,這是兩個不同的進程,分別使用的是自己的文件描述符,然后分別向文件file里面寫東西,為了很好的演示出效果,分別休眠了一秒。但是vi file后的結果也是如下:

1 hello                    1、world
2 world                    2、world
3 hello                    3、hello
4 world
5 hello
6 world
期望的結果                  實際的結果

對於上面結果的分析,也需要借助於內核維護的被打開文件的數據結構,這個結構與之前的類似但卻有很大的區別,看下圖:

 

 

看得出來上面的和之前的結構很像,每個描述符都指向了各自獨立的文件表,但畢竟共享的是同一個文件,所以他們擁有相同的V結點。

但是不同的是,這里畢竟涉及的是兩個不同的進程,那么每個進程將會有自己的進程表,從上圖中我們其實已經很清楚覆蓋的原因了,和之前的原因基本是一致的,雖然結構上略有些區別。

解決的辦法仍然是在各自的open函數里是加入O_APPEND標志,着重看紅色部分,如下:

fd1 = open("file", O_CREAT|O_RDWR|O_TRUNC|O_APPEND, 0664);

fd2 = open("file", O_CREAT|O_RDWR|O_TRUNC|O_APPEND, 0664);

 

如此一來每個進程寫之前都會用共享的文件長度去修改自己的文件位移量,那么自然就會得到我們想要的結果,如下:

1 hello
2 world
3 hello
4 world
5 hello
6 world
實現了期望的結果

10.2O_APPEND一個很重要的特性,原子操作

我們看,不管是同一個進程還是多個進程實現多次open同一個文件,並且進行共享的操作(特別涉及寫時,讀無所謂),如果不希望出現相互覆蓋的情況的話,O_APPEND標志的指定是非常重要的,但是你本來就希望它們之間相互覆蓋的話,那么這個不說,一般這種情況很少見就是。

還是以前面的例子來說,O_APPEND作用是將文件位移量修改為文件長度,然后再實現對文件的寫操作,其實功能基本等價下面兩句話:

lseek(fd1, 0, SEEK_END);

write(fd1, "hello\n", 6);

但是利用O_APPEND標志來實現時,有一個好處,那就是將文件位移量的調動和具體操作(這里是寫)組合為一個原子操作,如果這兩種方式真的要等價的話,上面的寫法需要再做修改,如下:

加鎖;

lseek(fd1, 0, SEEK_END);

write(fd1, "hello\n", 6);

解鎖;

通過鎖機制將lseekwrite操作變成一個原子操作,所謂原子操作,就是這兩個操作捆綁在了一起,當執行了lseek操作后,就必須等到write操作完,這兩個操作之間是沒有縫隙的,所謂原子就意味着不可分割。

所以對於多進程的這種情況,雖然O_APPEND的追加特性基本能夠保證相互不覆蓋,但是如果沒有原子性的話,就不可能完全的避免相互覆蓋的情況的發生。假如O_APPEND沒有原子性的話,對於上面2個進程的例子就會出現如下情況:

1、進程1:執行lseek,將文件位移量設置為文件長度n

2、從進程1切換到進程2,進程2同樣執行lseek,因為共享的是同一個文件,所以進程lseek后的文件位 移量也為文件長度n

3、進程2n的位置寫”hello\n”,file文件長度變為n+6

4、從進程2切換回到金成1,進程1之前已經將fd1的文件位移量設置為了n,進程1就會在文件位置n處寫”world\n”,這樣一來進程1在文件n位置處寫的”world”就會覆蓋掉進程2n位置處寫的”hello”,這樣也會導致相互覆蓋,盡管出現的幾率很小,但是這也是不允許的。

對於多進程實現對文件共享時,O_APPEND標志帶有的原子性是很重要的,我們在后續的課程中還會陸續接觸到有關原子操作的概念,這里暫時先借助O_APPEND標志的原子性先給大家引入這一概念,后面再次講到原子性時,大家接受起來會容易得多。

10.3、注意點

單個進程多次打開同一文件,和多個進程各自打開同一文件並實現文件共享時,維護打開文件的數據結構基本一致,但單進程只有一個進程表,多個進程的每個進程都有自己的進程表,所以單進程里面多次open返回的文件描述符絕對不可能相同,同一進程里面的某文件描述符不可能同時指向幾個文件,但是多個進程中每個進程open返回的文件描述符可能相同,也可能不同,因為它們用的是各自進程表中0~1023的文件描述集。

從這個例子中大家很清楚的了解到,文件位移量和文件長度分開存放是很有好處的。

11dup函數(文件描述符重定位函數)

11.1、函數原型和頭文件      

#include <unistd.h>

int dup(int oldfd);

int dup2(int oldfd, int newfd);

11.2、函數功能說明

用來復制一個現存的文件描述符,其實就是讓多個文件描述符指向同一個文件(不用多次open實現),比如可以讓456這三個文件描述符指向了同一個文件file biao。

11.2.1dup:對於復制后的用到的文件描述符,選擇的是該進程中0~1023中目前最小並且未用的那個,比如最小的2這個描述符未用,2就會和oldfd一起指向同一個文件,實現文件共享

11.2.2dup2:復制后用到的文件描述符newfd由我們自己指定,如果該描述符已經被用,那么這個描述符將會先被關閉,然后再復制,並且關閉和復制是一個原子操作。

11.3、函數參數

11.3.1第一個參數int oldfd:需要被復制的文件描述符

11.3.2第二個參數int newfd:復制后的用到的文件描述符

11.4、返回值

調用成功,返回新復制的文件描述符,失敗,返回-1,並且errno設置。

11.5、測試用例:略

從前面一系列的課程的學習中,大家已經基本清楚,printf函數其實是fprintf(stdout, “”, ...);的特殊形式,stdout對應着標准輸出,是個庫函數,繼續向下調用時,庫的內部實際上調用的還是read函數,由於是標准輸出,所以read用的1這個文件描述符,所以基本上printf函數調用read(1, “”, ....);函數時,文件描述符被寫死為了1,那么printf只能打印到標准輸出上。

但是如果我希望printf()的內容不輸出到標准輸出,而是輸出的某個普通文件中,怎么辦呢,當然最好的辦法就是將庫函數中調用的read(1, “”, ....);中的1改為fd就好了,但是1基本被寫死了,那么這個願望不太好實現,其實有一種方法,那就是將1關閉,然后立即open普通文件,這樣的話1就應該指向了普通文件,也可實現我們的目的,但是我們這里不討論這種方法,而是采用剛剛學的dup函數來實現。

但是實際上這兩種方法是有很大的區別的,我們后面在文件共享的時候將為大家分析這種兩種區別。

11.5.1、用dup函數例子:

int main(void){
        int fd1 = -1, fd2 = -1, ret = -1; 

/* 由於0、1、2三個文件描述符已經被用,當前最小未用的肯定是3,所以fd1 = = 3 */
        fd1 = open("file", O_CREAT|O_RDWR, 0664);
        if(fd1 < 0){   
                perror("open file is fail");
                exit(-1);  
}   
        close(1);//關閉1
/* 當1被關閉后,當前最小且未用的文件描述符肯定是1,所以dup會將
*  fd1復制到當前最小未用的1上,這樣一來1和fd1都指向了文件file */
        ret = dup(fd1);//新復制的文件描述符會被返回
printf("fd1 = %d\n", fd1);
        printf("ret = %d\n", ret);
        printf("hello world\n");    
        return 0;         
}

 

我的目的是讓1結合上新打開的文件file,這樣的話1fd1= =3)同時指向同一個文件file,這樣的話,printf輸出的內容就到了file里面,vi  file,內容如下:

  1 fd1 = 3  

  2 ret = 1

  3 hello world

從以上答案看出,printf打印的hello world信息,已經輸入到了file文件中。但是這段代碼實際上存在一個問題,就是當涉及多線程的時候,多個線程共享本進程所有的文件描述符,close(1)后,如果還沒來得及執行dup函數時,就切換到了其它線程的話,其它線程可能就會搶先用掉1這個描述符。實際上出現這個問題的原因就是closedup不是原子操作,針對這個問題我們可以用dup2函數解決,因為它的描述符關閉和賦值是原子操作。

當然我們如果不關心復制后用的是什么數值的描述符,只是簡單復制的話,那么用dup就可以了。

11.5.1、用dup2函數例子:

int main(void)
{
        int fd1 = -1, fd2 = -1, ret = -1; 
        fd1 = open("file", O_CREAT|O_RDWR, 0664);
        if(fd1 < 0){   
                perror("open file is fail");
                exit(-1);     
}   
        Fd2 = dup2(fd1, 1);
        printf("fd1 = %d\n", fd1);
        printf("fd2 = %d\n", fd2);
        printf("hello world\n");    
        return 0;
}

dup2函數的第二個參數,用於指定新的描述符,如果這個描述符已經打開了,那就先關閉它,然后再復制,只是這個關閉和復制兩個動作是一個原子操作,所以dup2就避免了dup存在的問題。

11.5.1dup后的內核維護打開文件的數據結構

 

int main(void)
{
        int fd1 = -1, fd2 = -1, ret = -1; 
        fd1 = open("file", O_CREAT|O_RDWR, 0664);
        if(fd1 < 0){   
                perror("open file is fail");
                exit(-1);     
}   
        fd2 = dup2(fd1, 1);
while(1)
{
write(fd1, "hello\n", 6 );
write(fd2, "world\n", 6 );
sleep(1);
}

        return 0;
}

vi file后結果如下:

1 hello
2 world
3 hello
4 world
5 hello
6 world

 

看到這個結果,大家一定很奇怪,這里open時我並沒有指定所謂的O_APPEND標志啊,這是因為賦值后的內核維護未打開文件的數據結構又是不同的。

 

從上圖中,大家可以看出,dup后的多個文件描述符,直接共享同一個文件表,所以也就共享同一個文件位移量,這么一來大家對於文件位移量的修改在相互之間都是可見和共享的,這樣一來根本就不需要指定所謂的O_APPEND標志

11.6、注意點

其實我們學的 命令操作符,就涉及上面的dup系統調用,文件描述符的重定位其實就是復制。之前的例子中,文件描述符1原來是和標准輸出結合在一起,經過復制后(重定位后)就和普通文件file結合在了一起。

使用重定位函數時,也需要注意有關原子操作的問題。

12、有關文件共享的問題

12.1、各種不同的文件共享方式

實際上通過前面陸續的了解,我們發現有如下幾種文件共享上方式:

1、情況一:單個進程多次open同一文件

2、情況二:多個進程各自open同一個文件

3、情況三:通過dup函數重定位一個新的描述符,這些描述符指向同一個文件

以上三種方式都能實現文件的共享,但是它們之間又各有區別,本質的區別就在於各自的內核維護打開文件的數據結構是不同的。

12.2、各種情況所對用的內核維護打開文件的數據結構

下面都是以兩個文件描述符為例進行說明。

11.2.1第一種情況:多次open同一文件實現共享

12.2.2、第二種情況:兩個進程共享同一文件

 

12.2.3、第三種情況:dup實現共享同一文件

 

 13fcntl函數

13.1、函數原型和頭文件

#include <unistd.h>

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

13.2、函數功能

fcntl函數其實是FileControl的縮寫,利用描述符fd來改變已打開文件的性質(那些open時沒有指定,或open時沒辦法指定的性質),實現文件性質的控制。

13.3、函數參數

13.3.1、第一個參數int fd文件描述符

13.3.2、第二個參數 int cmd控制命令選項,用來控制修改什么樣的性質,對於cmd的設置選擇如下:

a)、F_DUPFD:復制現存的描述符

可用來用來模擬dupdup2,后面會有例子對此用法進行說明。

b)、F_GETFDF_SETFD:獲取或設置文件描述符狀態

這種設置只有一種情況,我們再將exec函數時將為大家舉例說明。

c)、F_GETFLF_SETFL:獲取或設置文件狀態

我們在open時如果沒有指定某些狀態(如O_NONBLOCK,O_RDWR,O_APPEND等),我們可以利用fcntl 函數補上。所以后續我們在講阻塞與非阻塞,異步通知,poll機制等時會為大家舉例說明。

d)、F_GETOWNF_SETOWN:獲取或設置異步io信號接收的所有權

我們在講異步通知或poll機制是將會為大家舉例。

e)、F_GETLKF_SETLKF_SETLKW:獲取或設置記錄所屬性用

我們將高級io的記錄鎖再為大家舉例子

13.3.3、第三個參數

到底有沒有,有又如何設置,這需要視具體情況而定,我們后面會陸續為大家舉各種例子,到時再分別做說明。

13.4、函數返回值

如果調用成功,那么具體的返回值視cmd的設置而定,具體如下:

a)、F_DUPFD:返回復制后的新的文件描述符

b)、F_GETFD:返回文件描述符狀態

c)、F_GETFL:返回文件的文件狀態

d)、F_GETOWN:返回文件描述符所有者的進程id

e)、除了F_GETKEASEF_GETSIG外,其余的設置全部返回0

如果調用失敗,不管cmd怎么設置的,一律返回-1errno被設置

13.5、測試用例

對於例子,我們這里只說明cmd設置為F_DUPFD時用來模擬dupdup2函數的用法,因為我們剛講完描述符復制的系列函數,這里再給大家做個復習,同時也初步的了解下fcntl函數如何使用。

12.5.1fcntl模擬dup函數

int main(void)
{
  int fd1 = -1, fd2 = -1;

  fd1=open("file", O_CREAT|O_RDWR, 0664);

  close(1);


  fd2 = fcntl(fd1, F_DUPFD, 0);
  printf("fd1 = %d\n", fd1);
  printf("fd2 = %d\n", fd2);
  printf("hello world\n");
  return 0;
}

fcntl函數模擬dup函數時,着重看fcntl參數是如何設置的,也即右邊代碼的紅色部分。關閉文件描述符操作和文件描述符的復制操作任然是無法做到原子操作的。

13.5.1fcntl模擬dup2函數

int main(void)
{
   int fd1 = -1, fd2 = -1, ret = -1;

  fd1 = open("file", O_CREAT|O_RDWR, 0664);

  close(1);  
  /*也可以這么寫:
  *fd2 = 1;
  *ret = fcntl(fd1, F_DUPFD, fd2);  */
  fd2 = fcntl(fd1, F_DUPFD, 1);

  printf("fd1 = %d\n", fd1);
  printf("fd2 = %d\n", fd2);
  printf("hello world\n");
  return 0;
}

dup2進行復制時,如果新的文件描述符已經被用,dup2函數會關閉它,然后再復制它,並且是原子操作,但是fcntl模擬dup2時卻必須自己手動的調用close函數進行關閉,並且關閉和復制並不是不是原子操作,利用fcntl模擬dup2時需要注意。

13.6、注意點

a)、fcntl函數是一個雜物箱,可以實現很多功能的操作

b)、模擬dup2函數時,描述符關閉和描述符復制不是原子操作

c)、不管是利用dupdup2還是fcntl復制出來的文件描述符,他么雖然共享相同內核維護打開文件的數據結構,但是它們卻擁有不同的文件描述符狀態標志(可以通過F_GETFDF_SETFD選項進行設置),原 有文件描述符的狀態標志會被dupdup2fcntl(利用cmd 設置 F_DUPFD進行設置)刷新。

對於有關XXX_CLOEXEC標志,后續課程會講到的,這里了解即可,可后面的學習大點基礎。

d)、利用fcntl函數進行文件性質修改有個好處,那就是只需要知道文件描述符,並不需要知道文件的具體路徑是什么。

14ioctl函數

這個函數也是一個雜物箱,但是一個很強大的函數,后面學習驅動時用得較多,具體的例子我們留到學習網絡編程時再講,到時我們會用ioctl函數來獲取本機的ip地址,這里暫時略去對這個函數的講解。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM