linux多進程下的文件共享(包括每個進程的文件表項的詳細介紹)

1. 文件共享

  (1) 每個進程在進程表中都有一個記錄項,記錄項中包含有一張打開文件描述符表,可將其視爲一個矢量,每個描述符佔用一項。與每個文件描述符相關聯的是:

      (a) 文件描述符標識(close_on_exec)。

     (b)指向一個文件表項的指針。

  (2)內核爲所有的打開文件維持一張文件表。每個文件表項包含:

      (a)文件狀態標誌(讀、寫、添加、同步和非阻塞等)。

      (b)當前文件偏移量。

      (c)指向該文件v節點的指針。

   (3)每個打開文件(或設備)都有一個v節點(v-node)結構。v節點包含了文件類型和對此文件進行各種操作的函數的指針。對於大多數文件,v節點還包含了該文件的i節點(i-node,索引節點)。這些信息是在打開文件時從磁盤上讀入內存的,所以所有關於文件的信息都是快速可供使用的。例如,i節點包含了文件的所有者,文件長度,文件所在的設備,指向文件實際數據塊在磁盤上所在位置的指針等等。

    圖1顯示了一個進程的三張表之間的關係。該進程有兩個不同的打開文件:一個文件打開爲標註輸入(文件描述符爲0),另一個打開爲標準輸出(文件描述符爲1)。從Unix系統的早期版本中[Thompson 1978]以來,這三張表之間的基本關係一直保持至今。這種安排對於在不同進程之間共享文件的方式非常重要。

                                        圖1 打開文件的內核數據結構

2. 原子操作

2.1 添寫至一個文件

    考慮一個進程,它要將數據添加到一個文件尾端。早期的UNIX系統版本並不支持open的O_APPEND選項,所以程序被編寫成下列形式:

1. if (lseek(fd, 0L, 2) < 0) /* position to EOF */

2.     err_sys("lseek error");

3. if (write(fd, buf, 100) != 100) /* and write */

4.     err_sys("write error");

    對單個進程而言,這段程序能正常工作,但若對多個進程同時使用這種方法將數據添加到同一文件,則會產生問題。(例如,若此程序由多個進程同時執行,各自將消息添加到一個日誌文件中,就會產生這種情況。)

    假定有兩個獨立的進程A和B都對同一個文件進行操作,給個進程都已打開了該文件,但未使用O_APPEND標誌。此時,各數據結構之間的關係如圖2所示。每個進程都有自己的文件表項,但是共享一個v節點表項。假定進程A調用了lseek,它將進程A的該文件當前偏移量設置爲1500字節(當前文件尾端處)。然後內核調度進程使進程B運行。進程B執行sleek,也將其對該文件的當前偏移量設置爲1500字節(當前文件尾端處)。然後B調用write函數,它將B的該文件當前文件偏移量增值1600.引文該文件的長度已經增加了,所以內核對v節點中的當前文件長度更新爲1600.然後,內核又進行進程切換使進程A恢復運行。當A調用write時,就從其當前文件偏移量(1500字節)處將數據寫到文件中去。這樣就代換了進程B剛寫到該文件中的數據。

     問題出在邏輯操作“定位到文件尾端處,然後寫”上,它使用了兩個分開的函數調用。解決問題的方法是使這兩個操作對於其他進程而言成爲一個原子操作。任何一個需要多個函數調用的操作都不可能是原子操作,因爲在兩個函數調用之間,內核有可能會臨時掛起該進程。

     UNIX系統提供了一種方法是這種操作成爲原子操作,該方法是在打開文件時設置O_APPEND標誌。正如前面所述,這就是內核每次對這種文件進行寫之前,都將進程的當前偏移量設置到文件的尾端處,於是在每次寫之前就不在需要調用sleek了。

2.2 pread和pwrite函數

     Single UNIX Specification包括了XSI擴展,該擴展允許原子性地定位搜索(seek)和執行I(/O。pread和pwrite就是這種擴展。

1. #include <unistd.h>

2. 

3. ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);

4.                                         返回值:讀到的字節數,若已到文件結尾則返回0,若出現錯誤返回-1

5. ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);

6.                                         返回值:若成功則返回已寫的字節數,若出錯則返回-1

    調用pread相當於順序調用lseek和read,但是pread又與這種順序調用有下列重要區別:

· 調用pread時,無法中斷其定位和讀操作。

· 不更新文件指針。

    調用pwrite相當於順序調用lseek和write,但也和它們有類似的區別。

2.3 創建一個文件

    在對open函數的O_CREAT和O_EXCL選項進行說明時,我們已經見到另一個有關原子操作的例子。當同時指定這兩個選項,而該文件又已經存在時,open將失敗。我們曾提及檢查該文件是否存在以及創建該文件這兩個操作是作爲一個原子操作執行的。如果沒有這樣一個原子操作,那麼可能會編寫下面的程序段:

1. if ((fd = open(pathname, O_WRONLY)) < 0) {

2.     if (errno == ENOENT) {

3.          if ((fd = creat(pathname, mode)) < 0)

4.               err_sys("create error");

5.     } else {

6.          err_sys("open error");

7.     }

8. }

     如果在open和creat之間,另一個進城創建了該文件,那麼就會引起問題。例如,若在這兩個函數調用之間。另一個進程創建了該文件,並且寫進了一些數據,然後,原先的進程執行這段程序中的creat,這時,剛由另一個進程寫上去的數據就會被擦除掉。如若將這兩者合併在一個原子操作中,這種問題就不會存在了。

     一般而言,原子操作(atomic operation)指的是由多步組成的操作,如果該操作原子地執行,則要麼執行完所有步驟,要麼一步也不執行,不可能只執行所有步驟的一個子集。

3. dup和dup2函數

     下面兩個函數都可用來複制一個現存的文件描述符:

1. #include <unistd.h>

2. 

3. int dup(int filedes);

4. int dup2(int filedes, int filedes2);

5.                      兩函數的返回值:若成功則返回新的文件描述符,若出錯則返回-1

      由dup返回的新文件符一定是當前可用文件描述符中的最小數值。用dup2則可以用filedes2參數指定新描述符的數值。如果filedes2已經打開,則先將其關閉。如若filedes等於filedes2,則dup2返回filedes2,而不關閉它。

      這些函數返回的新文件描述符與參數參數filesdes共享同一個文件表項。如圖3所示。

                                   圖3 執行dup之後的內核數據結構

    在圖3中,我們假定進程執行了:

1. newfd = dup(1);

    當此函數開始執行時,假定下一個可用的文件描述是3(這是非常有可能的,因爲0、1、2是由shell打開的)。因爲兩個描述符指向同一文件表項,所以它們共享同一個文件狀態標誌(讀、寫、添加等)以及同一文件當前偏移量。

    每個文件描述符都有它自己的一套文件描述符標誌。新描述的執行時關閉(close-on-exec)標誌總是由dup函數清除。

    複製一個描述符的另一種方法是使用fcntl函數,實際上,調用

1. dup(filedes);

等效於

1. dup2(filedes, F_DUPFD, 0);

而調用

1. dup2(filedes, filedes2);

等效於

1. close(filedes2);

2. fcntl(filedes, F_DUPFD, filedes2);

在後一種情況下,dup2並不完全等同於close()加上fcntl.它們之間的區別是:

· dup2是一個原子操作,而close及fcntl則包含兩個函數調用。有可能在close和fcntl之間插入執行信號捕獲函數,它可能修改文件描述符。

· dup2和fcntl有某些不同的errno。

 

發佈了87 篇原創文章 · 獲贊 28 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章