Linux 程式設計中的檔案鎖之 flock

發表於2016-06-20

1. 場景概述

在多執行緒開發中,互斥鎖可以用於對臨界資源的保護,防止資料的不一致,這是最為普遍的使用方法。那在多程式中如何處理檔案之間的同步呢?我們看看下面的圖:

圖中所示的是兩個程式在無同步的情況下同時更新同一個檔案的過程,其主要的操作是:

  • 1. 從檔案中讀取序號。
  • 2. 使用這個序號完成應用程式定義的任務。
  • 3. 遞增這個序號並將其寫回檔案中。

從圖中可得知兩個程式讀取分別增加了所讀取到的序號,並寫回到了檔案中,但是如果有相互互斥的話,最後的值應該是1002,而不是所示的1001。為了防止出現這種情況,Linux提供了flock(對整個檔案加鎖)、fcntl(對整個檔案區域加鎖)兩個函式來做程式間的檔案同步。同時也可以使用訊號量來完成所需的同步,但通常使用檔案鎖會更好一些,因為核心能夠自動將鎖與檔案關聯起來。

2. flock()

flock的宣告如下

fcntl()函式提供了比該函式更為強大的功能,並且所擁有的功能也覆蓋了flock()所擁有的功能,但是在某些應用中任然使用著flock()函式,並且在繼承和鎖釋放方面的一些語義 中flock()與fcntl()還是有所不同的。

flock()系統呼叫是在整個檔案中加鎖,通過對傳入的fd所指向的檔案進行操作,然後在通過operation引數所設定的值來確定做什麼樣的操作。operation可以賦如下值:

在預設情況下,如果另一個程式已經持有了檔案上的一個不相容的鎖,那麼flock()會阻塞。如果需要防止這種情況的出現,可以在operation引數中對這些值取OR(|)。在這種情況下,如果一個程式已經持有了檔案上的一個不相容鎖,那麼flock()就會阻塞,相反,它會返回-1,並將errno設定成EWOULDBLOCK。

任意數量的程式可同時持有一個檔案上的共享鎖,但子任意時刻只能有一個程式能夠持有一個檔案上的互斥鎖,(這有點類似讀寫鎖)。下圖是程式A先設定了鎖,程式B後設定鎖的支援情況:

無論程式以什麼模式開啟了檔案(讀、寫或者讀寫),該檔案上都可以放置一把共享鎖或互斥鎖。在實際操作過程中,引數operation可以指定對應的值將共享鎖轉換成互斥鎖(反之亦然)。將一個共享鎖轉換成互斥鎖,如果另一個程式要獲取該檔案的共享鎖則會阻塞,除非operation引數指定了LOCK_NB標記,即:(LOCK_SH | LOCK_NB)。鎖的轉換過程不是一個原子操作,在轉換的過程中首先會刪除既有的鎖,然後建立新鎖。

3. 鎖繼承與釋放的語義

flock()根據呼叫時operation引數傳入LOCK_UN的值來釋放一個檔案鎖。此外,鎖會在相應的檔案描述符被關閉之後自動釋放。同時,當一個檔案描述符被複制時(dup()、dup2()、或一個fcntl() F_DUPFD操作),新的檔案描述符會引用同一個檔案鎖。

這段程式碼先在fd上設定一個互斥鎖,然後通過fd建立一個指向相同檔案的新檔案描述符new_fd,最後通過new_fd來解鎖。從而我們可以得知新的檔案描述符指向了同一個鎖。所以,如果通過一個特定的檔案描述符獲取了一個鎖並且建立了該描述符的一個或多個副本,那麼,如果不顯示的呼叫一個解鎖操作,只有當檔案描述符副本都被關閉了之後鎖才會被釋放。

由上我們可以推出,如果使用fork()建立一個子程式,子程式會複製父程式中的所有描述符,從而使得它們也會指向同一個檔案鎖。例如下面的程式碼會導致一個子程式刪除一個父程式的鎖:

所以,有時候可以利用這些語義來將一個檔案鎖從父程式傳輸到子程式:在fork()之後,父程式關閉其檔案描述符,然後鎖就只在子程式的控制之下了。通過fork()建立的鎖在exec()中會得以保留(除非在檔案描述符上設定了close-on-exec標記並且該檔案描述符是最後一個引用底層的開啟檔案描述的描述符)。

如果程式中使用open()來獲取第二個引用同一個檔案的描述符,那麼,flock()會將其視為不同的檔案描述符。如下程式碼會在第二個flock()上阻塞。

4. flock()的限制

flock()放置的鎖有如下限制

  • 只能對整個檔案進行加鎖。這種粗粒度的加鎖會限制協作程式間的併發。假如存在多個程式,其中各個程式都想同時訪問同一個檔案的不同部分。
  • 通過flock()只能放置勸告式鎖。
  • 很多NFS實現不識別flock()放置的鎖。

註釋:在預設情況下,檔案鎖是勸告式的,這表示一個程式可以簡單地忽略另一個程式在檔案上放置的鎖。要使得勸告式加鎖模型能夠正常工作,所有訪問檔案的程式都必須要配合,即在執行檔案IO之前先放置一把鎖。

相關文章