Linux系統內容下關於多進程並發寫同一個檔案的討論__Linux

來源:互聯網
上載者:User

討論關於並發環境下,多個進程對同一檔案寫入的問題,我們會涉及到檔案分享權限設定的知識。在開始之前,我們先討論一些有關檔案分享權限設定的知識。

1. 檔案分享權限設定

  Unix系統支援在不同進程間共用開啟的檔案。為此,我們先介紹一下核心用於所有I/O的資料結構。注意,下面的說明是概念性的,與特定的實現可能匹配,也可能不匹配。

  核心使用三種資料結構表示開啟的檔案,它們之間的關係決定了在檔案分享權限設定方面一個進程對另一個進程可能產生的影響。

  (1) 每個進程在進程表中都有一個記錄項,記錄項中包含有一張開啟檔案描述符表,可將其視為一個向量,每個描述符佔用一項。與每個檔案描述符相關聯的是:

      (a) 檔案描述符標識(close_on_exec)。

     (b)指向一個檔案表項的指標。

  (2)核心為所有的開啟檔案維持一張檔案表。每個檔案表項包含:

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

      (b)當前檔案位移量。

      (c)指向該檔案v節點的指標。

   (3)每個開啟檔案(或裝置)都有一個v節點(v-node)結構。v節點包含了檔案類型和對此檔案進行各種操作的函數的指標。對於大多數檔案,v節點還包含了該檔案的i節點(i-node,索引節點)。這些資訊是在開啟檔案時從磁碟上讀入記憶體的,所以所有關於檔案的資訊都是快速可供使用的。例如,i節點包含了檔案的所有者,檔案長度,檔案所在的裝置,指向檔案實際資料區塊在磁碟上所在位置的指標等等。

    注意:Linux沒有使用v節點,而是使用了通用i節點結構。雖然兩種實現有所不同,但在概念上,v節點與i節點是一樣的。兩者都指向檔案系統特有的i節點結構。

    我們忽略了默寫實現細節,但這並不影響我們的討論。例如,開啟檔案描述符表可存放在使用者控制項,而非進程表中。這些表也可以用於多種方式的實現,不必一定是數組;例如,可將它們實現為結構的串連表。這些細節並不影響我們在檔案分享權限設定方面的討論。

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

                                        圖1 開啟檔案的核心資料結構

注意:建立v節點結構的目的是對在一個電腦系統上的多檔案系統類型提供支援。這一工作是有Peter Weihberger(貝爾實驗室)和Bill Joy(Sun公司)分別獨立完成的。Sun稱此種檔案系統為虛擬檔案系統(Virtual File System),稱與檔案系統類型無關的i節點部分為v節點[Kleiman 1986].當哥哥製造商的實現增加了對Sun的網路檔案系統(NFS)的支援時,它們都廣泛採用了v節點結構。在BSD系統中首先提供v節點的是4.3BSD Reno版本,其中加入了NFS。 在SVR4中,v節點代換了SVR3中與檔案系統類型無關的i節點結構。Solaris是從SVR4發展而來的,他也是用了v節點。 Linux沒有將相關的資料結構分為i節點和v節點,而是採用了一個獨立於檔案系統的i節點和一個依賴於檔案系統的i節點。

     如果兩個獨立進程各自開啟了同一個檔案,則有圖2中所示的安排。我們假設第一個進程在檔案描述符3上開啟該檔案,而另一個進程則在檔案描述4上開啟該檔案。開啟該檔案的每一個進程都得到一個檔案表項,但對一個給定的檔案只有一個v節點表項。每個進程都有自己的檔案表項的一個理由是:這種安排使每一個進程都有它自己的對該檔案的當前位移量。

                                  圖2 兩個獨立進程各自開啟同一個檔案 

    給出了這種資料結構後,現在對前面所描述的操作做進一步說明。    在完成每個write後,在檔案表項中的當前檔案位移量即增加所寫的位元組數。如果當前檔案位移量超過了當前檔案長度,則在i節點表項中的當前檔案長度被設定為當前檔案的位移量。 如果用O_APPEND標誌開啟了一個檔案,則相應標誌也被設定到檔案表項的檔案狀態標誌中。每次對這種具有添寫標誌的檔案執行寫操作時,在檔案表項中的當前檔案位移量首先被設定為i節點表項中的檔案長度。這就使得每次寫的資料都添加到檔案的當前尾端處。 若一個檔案用lseek定位到檔案當前的尾端,則檔案表項中的當前檔案位移量被設定為i節點表項中的當前檔案長度。注意,這與用O_APPEND標誌開啟檔案是不同的。 sleek函數只修改檔案表項中的當前檔案位移量,沒有進行任何檔案I/O操作。

    可能有多個檔案描述符指向同一個檔案表項。在下一小節中討論dup函數時,我們將能看見這一點。函數調用fork後產生的父子進程中,它們共用相同的i或v節點和同一個檔案表項。(通過在現代Linux系統中進行測試得到的,測試程式和結構見下文(更正:測試結果分析有誤。已更正。))。

    注意,檔案描述符標誌和檔案狀態標誌在範圍方面的區別,前者只用於一個進程的一個描述符,而後者適用於指向該給定檔案表項的任何進程中的所有描述符。上面所述的一切對多個進程讀同一個檔案都能正確工作。每個進程都有它自己的檔案表項,其中也有它自己的當前檔案位移量。但是,當多個進程寫同一個檔案時,則可能產生預期不到得結果。為了說明如何避免這種情況,我們需要理解原子操作的概念。

2. 原子操作

2.1 添寫至一個檔案

    考慮一個進程,它要將資料添加到一個檔案尾端。早期的UNIX系統版本並不支援open的O_APPEND選項,所以程式被編寫成下列形式: if (lseek(fd, 0L, 2) < 0) /* position to EOF */
    err_sys("lseek error");
if (write(fd, buf, 100) != 100) /* and write */
    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就是這種擴充。 #include <unistd.h>

ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
                                        傳回值:讀到的位元組數,若已到檔案結尾則返回0,若出現錯誤返回-1
ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);
                                        傳回值:若成功則返回已寫的位元組數,若出錯則返回-1

    調用pread相當於順序調用lseek和read,但是pread又與這種順序調用有下列重要區別: 調用pread時,無法中斷其定位和讀操作。 不更新檔案指標。

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

2.3 建立一個檔案

    在對open函數的O_CREAT和O_EXCL選項進行說明時,我們已經見到另一個有關原子操作的例子。當同時指定這兩個選項,而該檔案又已經存在時,open將失敗。我們曾提及檢查該檔案是否存在以及建立該檔案這兩個操作是作為一個原子操作執行的。如果沒有這樣一個原子操作,那麼可能會編寫下面的程式段: if ((fd = open(pathname, O_WRONLY)) < 0) {
    if (errno == ENOENT) {
         if ((fd = creat(pathname, mode)) < 0)
              err_sys("create error");
    } else {
         err_sys("open error");
    }
}

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

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

3. dup和dup2函數

     下面兩個函數都可用來複製一個現存的檔案描述符: #include <unistd.h>

int dup(int filedes);
int dup2(int filedes, int filedes2);
                     兩函數的傳回值:若成功則返回新的檔案描述符,若出錯則返回-1

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

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

                                   圖3 執行dup之後的核心資料結構

    在圖3中,我們假定進程執行了: newfd = dup(1);

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

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

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

等效於 dup2(filedes, F_DUPFD, 0);

而調用 dup2(filedes, filedes2);

等效於 close(filedes2);
fcntl(filedes, F_DUPFD, filedes2);

在後一種情況下,dup2並不完全等同於close()加上fcntl.它們之間的區別是: dup2是一個原子操作,而close及fcntl則包含兩個函數調用。有可能在close和fcntl之間插入執行訊號捕獲函數,它可能修改檔案描述符。 dup2和fcntl有某些不同的errno。

4. 測試結果  

    帶有O_APPEND標誌的測試代碼和結果資料如下:  o_append_text.rar   

    沒有帶O_APPEND標誌的測試代碼和結果資料如下: test.rar   

    可以看到,o_append_text.rar裡面的測試結果資料檔案大小是test.rar裡面的2倍。分析其原因,是因為函數調用fork時,在父子進程中沒有為他們採取必要的同步措施,因此在寫檔案時有競爭發生,導致結果混亂。另外,檔案大小的不同是因為,檔案表項中的檔案位移量的實值型別並不是一個易失形變數類型,從而導致在寫檔案時讀取的位移值變數的值不是最新的值,從而導致檔案大小會不同的結果。

    另外,從結果資料中(可能資料沒有充分表現出如下所說的情況,但是你可以通過調整測試程式裡的參數,並多運行幾次測試程式就可以得到如下所述的結果)可以得出:當以O_APPEND標誌開啟檔案時,write將執行原子操作,read亦然。而沒有使用O_APPEND標誌開啟檔案時,父子進程的資料輸出將出現亂序的情況。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.