在多個進程同時操作同一份文件的過程中,很容易導致文件中的數據混亂,需要鎖操作來保證數據的完整性,這里介紹的針對文件的鎖,稱之為“文件鎖”-flock。
flock,建議性鎖,不具備強制性。一個進程使用flock將文件鎖住,另一個進程可以直接操作正在被鎖的文件,修改文件中的數據,原因在於flock只是用於檢測文件是否被加鎖,針對文件已經被加鎖,另一個進程寫入數據的情況,內核不會阻止這個進程的寫入操作,也就是建議性鎖的內核處理策略。
flock主要三種操作類型:
LOCK_SH,共享鎖,多個進程可以使用同一把鎖,常被用作讀共享鎖;
LOCK_EX,排他鎖,同時只允許一個進程使用,常被用作寫鎖;
LOCK_UN,釋放鎖;
進程使用flock嘗試鎖文件時,如果文件已經被其他進程鎖住,進程會被阻塞直到鎖被釋放掉,或者在調用flock的時候,采用LOCK_NB參數,在嘗試鎖住該文件的時候,發現已經被其他服務鎖住,會返回錯誤,errno錯誤碼為EWOULDBLOCK。即提供兩種工作模式:阻塞與非阻塞類型。
服務會阻塞等待直到鎖被釋放:
flock(lockfd,LOCK_EX)
服務會返回錯誤發現文件已經被鎖住時:
ret = flock(lockfd,LOCK_EX|LOCK_NB)
同時ret = -1, errno = EWOULDBLOCK
flock鎖的釋放非常具有特色,即可調用LOCK_UN參數來釋放文件鎖,也可以通過關閉fd的方式來釋放文件鎖(flock的第一個參數是fd),意味着flock會隨着進程的關閉而被自動釋放掉。
flock其中的一個使用場景為:檢測進程是否已經存在;
int checkexit(char* pfile) { if (pfile == NULL) { return -1; } int lockfd = open(pfile,O_RDWR); if (lockfd == -1) { return -2; } int iret = flock(lockfd,LOCK_EX|LOCK_NB); if (iret == -1) { return -3; } return 0; }
1. 場景概述
在多線程開發中,互斥鎖可以用於對臨界資源的保護,防止數據的不一致,這是最為普遍的使用方法。那在多進程中如何處理文件之間的同步呢?我們看看下面的圖:
圖中所示的是兩個進程在無同步的情況下同時更新同一個文件的過程,其主要的操作是:
- 1. 從文件中讀取序號。
- 2. 使用這個序號完成應用程序定義的任務。
- 3. 遞增這個序號並將其寫回文件中。
從圖中可得知兩個進程讀取分別增加了所讀取到的序號,並寫回到了文件中,但是如果有相互互斥的話,最后的值應該是1002,而不是所示的1001。為了防止出現這種情況,Linux提供了flock(對整個文件加鎖)、fcntl(對整個文件區域加鎖)兩個函數來做進程間的文件同步。同時也可以使用信號量來完成所需的同步,但通常使用文件鎖會更好一些,因為內核能夠自動將鎖與文件關聯起來。
2. flock()
flock的聲明如下
1
2
3
4
|
#include <sys/file.h>
// Returns 0 on success, or -1 on error
int flock (intfd, int operation);
|
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操作),新的文件描述符會引用同一個文件鎖。
1
2
3
|
flock(fd, LOCK_EX);
new_fd = dup(fd);
flock(new_fd, LOCK_UN);
|
這段代碼先在fd上設置一個互斥鎖,然后通過fd創建一個指向相同文件的新文件描述符new_fd,最后通過new_fd來解鎖。從而我們可以得知新的文件描述符指向了同一個鎖。所以,如果通過一個特定的文件描述符獲取了一個鎖並且創建了該描述符的一個或多個副本,那么,如果不顯示的調用一個解鎖操作,只有當文件描述符副本都被關閉了之后鎖才會被釋放。
由上我們可以推出,如果使用fork()創建一個子進程,子進程會復制父進程中的所有描述符,從而使得它們也會指向同一個文件鎖。例如下面的代碼會導致一個子進程刪除一個父進程的鎖:
1
2
3
4
5
|
flock (fd, LOCK_EX);
if (0 == fork ()) {
flock (fd, LOCK_UN);
}
|
所以,有時候可以利用這些語義來將一個文件鎖從父進程傳輸到子進程:在fork()之后,父進程關閉其文件描述符,然后鎖就只在子進程的控制之下了。通過fork()創建的鎖在exec()中會得以保留(除非在文件描述符上設置了close-on-exec標記並且該文件描述符是最后一個引用底層的打開文件描述的描述符)。
如果程序中使用open()來獲取第二個引用同一個文件的描述符,那么,flock()會將其視為不同的文件描述符。如下代碼會在第二個flock()上阻塞。
1
2
3
4
5
|
fd1 = open ("test.txt", O_RDWD);
fd2 = open ("test.txt", O_RDWD);
flock (fd1, LOCK_EX);
flock (fd2, LOCK_EX);
|
4. flock()的限制
flock()放置的鎖有如下限制
- 只能對整個文件進行加鎖。這種粗粒度的加鎖會限制協作進程間的並發。假如存在多個進程,其中各個進程都想同時訪問同一個文件的不同部分。
- 通過flock()只能放置勸告式鎖。
- 很多NFS實現不識別flock()放置的鎖。
注釋:在默認情況下,文件鎖是勸告式的,這表示一個進程可以簡單地忽略另一個進程在文件上放置的鎖。要使得勸告式加鎖模型能夠正常工作,所有訪問文件的進程都必須要配合,即在執行文件IO之前先放置一把鎖。