文件鎖 當多個進程或多個程式都想要修同一個文件的時候,如果不加控制,多進程或多程式將可能導致文件更新的丟失。 例如進程1和進程2都要寫入數據到a.txt中,進程1獲取到了文件句柄,進程2也獲取到了文件句柄,然後進程1寫入一段數據,進程2寫入一段數據,進程1關閉文件句柄,會將數據flush到文件中,進 ...
文件鎖
當多個進程或多個程式都想要修同一個文件的時候,如果不加控制,多進程或多程式將可能導致文件更新的丟失。
例如進程1和進程2都要寫入數據到a.txt中,進程1獲取到了文件句柄,進程2也獲取到了文件句柄,然後進程1寫入一段數據,進程2寫入一段數據,進程1關閉文件句柄,會將數據flush到文件中,進程2也關閉文件句柄,也將flush到文件中,於是進程1的數據被進程2保存的數據覆蓋了。
所以,多進程修改同一文件的時候,需要協調每個進程:
- 保證文件在同一時間只能被一個進程修改,只有進程1修改完成之後,進程2才能獲得修改權
- 進程1獲得了修改權,就不允許進程2去讀取這個文件的數據,因為進程2可能讀取出來的數據是進程1修改前的過期數據
這種協調方式可以通過文件鎖來實現。文件鎖分兩種,獨占鎖(寫鎖)和共用鎖(讀鎖)。當進程想要修改文件的時候,申請獨占鎖(寫鎖),當進程想要讀取文件數據的時候,申請共用鎖(讀鎖)。
獨占鎖和獨占鎖、獨占鎖和共用鎖都是互斥的。只要進程1持有了獨占鎖,進程2想要申請獨占鎖或共用鎖都將失敗(阻塞),也就保證了這一時刻只有進程1能修改文件,只有當進程1釋放了獨占鎖,進程2才能繼續申請到獨占鎖或共用鎖。但是共用鎖和共用鎖是可以共存的,這代表的是兩個進程都只是要去讀取數據,並不互相衝突。
獨占鎖 共用鎖
獨占鎖 × ×
共用鎖 × √
文件鎖:flock和lockf
Linux上的文件鎖類型主要有兩種:flock和lockf。後者是fcntl系統調用的一個封裝。它們之間有些區別:
- flock來自BSD,而fcntl或lockf來自POSIX,所以lockf或fcntl實現的鎖也稱為POSIX鎖
- flock只能對整個文件加鎖,而fcntl或lockf可以對文件中的部分加鎖,即粒度更細的記錄鎖
- flock的鎖是勸告鎖,lockf或fcntl可以實現強制鎖。所謂勸告鎖,是指只有多進程雙方都遵紀守法地使用flock鎖才有意義,某進程使用flock,但另一進程不使用flock,則flock鎖對另一進程完全無限制
- flock鎖是附加在(關聯在)文件描述符上的,而lockf是關聯在文件實體上的。本文後面將詳細分析flock鎖在文件描述符上的現象
Perl中主要使用flock來實現文件鎖,也是本文的主要內容。
Perl的flock
flock FILEHANDLE, flags;
flock兩個參數,第一個是文件句柄,第二個是鎖標誌。
鎖標誌有4種,有數值格式的1、2、8、4,在導入Fcntl模塊的:flock
後,也支持字元格式的LOCK_SH
、LOCK_EX
、LOCK_UN
、LOCK_NB
。
字元格式 數值格式 意義
-----------------------------------
LOCK_SH 1 申請共用鎖
LOCK_EX 2 申請獨占鎖
LOCK_UN 8 釋放鎖
LOCK_NB 4 非阻塞模式
獨占鎖和獨占鎖、獨占鎖和共用鎖是衝突的。所以,當進程1持有獨占鎖時,進程2想要申請獨占鎖或共用鎖預設將被阻塞。如果使用了非阻塞模式,那麼本該阻塞的過程將立即返回,而不是阻塞等待其它進程釋放鎖。非阻塞模式可以結合共用鎖或獨占鎖使用。所以,有下麵幾種方式:
use Fcntl qw(:flock);
flock $fh, LOCK_SH; # 申請共用鎖
flock $fh, LOCK_EX; # 申請獨占鎖
flock $fh, LOCK_UN; # 釋放鎖
flock $fh, LOCK_SH | LOCK_NB; # 以非阻塞的方式申請共用鎖
flock $fh, LOCK_EX | LOCK_NB; # 以非阻塞的方式申請獨占鎖
flock在操作成功時返回true,否則返回false。例如,在申請鎖的時候,無論是否使用了非阻塞模式,只要沒申請到鎖就返回false,否則返回true,而在釋放鎖的時候,成功釋放則返回true。
例如,兩個程式(不是單程式內的兩個進程,這種情況後面分析)同時運行,其中一個程式寫a.txt文件,另一個程式讀a.txt文件,但要保證先寫完再讀。
程式1的代碼內容:
#!/usr/bin/perl
use strict;
use warnings;
use Fcntl qw(:flock);
open my $fh, '>', "a.txt"
or die "open failed: $!";
flock $fh, LOCK_EX;
print $fh, "Hello World1\n";
print $fh, "Hello World2\n";
print $fh, "Hello World3\n";
flock $fh, LOCK_UN;
程式2的代碼內容:
#!/usr/bin/perl
use strict;
use warnings;
use Fcntl qw(:flock);
open my $fh, '<', "a.txt"
or die "open failed: $!";
# 非阻塞的方式每秒申請一次共用鎖
# 只要沒申請成功就返回false
until(flock $fh, LOCK_SH | LOCK_NB){
print "waiting for lock released\n";
sleep 1;
}
while(<$fh>){
print "readed: $_";
}
flock $fh, LOCK_UN;
fork、文件句柄、文件描述符和鎖的關係
在開始之前,先看看在Perl中的fork、文件句柄、文件描述符、flock之間的結論。
- 文件句柄是指向文件描述符的,文件描述符是指向實體文件的(假如是實體文件的描述符的話)
- fork只會複製文件句柄,不會複製文件描述符,而是通過複製的不同文件句柄指向同一個文件描述符而實現文件描述符共用
- 通過引用計數的方式來計算某個文件描述符上文件句柄的數量
- close()一次表示引用數減1,直到所有文件句柄都關閉了即引用數為0時,文件描述符才被關閉
- flock是附在文件描述符上的,不是文件句柄也不是實體文件上的
- flock是進程級別的,不適用於在多線程中使用它來鎖互斥
- 所以fork後的父子進程在共用文件描述符的同時也會共用flock鎖
flock $fh, LOCK_UN
會直接釋放文件描述符上的鎖
- 當文件描述符被關閉時,文件描述符上的鎖也會自動釋放。所以使用close()去釋放鎖的時候,必須要保證所有文件句柄都被關閉才能關閉文件描述符從而釋放鎖
- flock(包括加鎖和解鎖)或close()都會自動flush IO Buffer,保證多進程間獲取鎖時數據同步
- 只要持有了某個文件描述符上的鎖,在這把鎖釋放之前,自己可以隨意更換鎖的類型,例如多次flock從EX鎖變成SH鎖
下麵是正式介紹和解釋。
在C或操作系統上的fork會複製(dup)文件描述符,使得父子進程對同一文件使用不同文件描述符。但Perl的fork只會複製文件句柄而不會複製文件描述符,父子進程的不同文件句柄會共用同一個文件描述符,並使用引用計數的方式來統計有多少個文件句柄在使用這個文件描述符。
之所以複製文件句柄是因為文件句柄在Perl中是一種變數類型,在不同作用域內是互相獨立的。而文件描述符對Perl來說相對更底層一些,屬於操作系統的數據資源,對Perl來說是屬於可以共用的數據。
也就是說,如果只fork了一次,那麼父子進程的兩個文件句柄都共用同一個文件描述符,都指向這個文件描述符,這個文件描述符上的引用計數為2。當父進程close關閉了該文件描述符上的一個文件句柄,子進程需要也關閉一次才是真的關閉這個文件描述符。
不僅如此,由於文件描述符是共用的,導致加在文件描述符上的鎖(比如flock鎖)在父子進程上看上去也是共用的。儘管只在父子某一個進程上加一把鎖,但這兩個進程都將持有這把鎖。如果想要釋放這個文件描述符上的鎖,直接unlock(flock $fh, LOCK_UN
)或關閉文件描述符即可。
但是註意,close()關閉的只是文件描述符上的一個文件句柄引用,在文件描述符真的被關閉之前(即所有文件句柄都被關掉),鎖會一直存在於描述符上。所以,很多時候使用close去釋放時的操作(之所以使用close而非unlock類操作,是因為unlock存在race condition,多個進程可能會在釋放鎖的同時搶到那個文件的鎖),可能需要在多個進程中都執行,而使用unlock類的操作只需在父子中的任何一進程中即可釋放鎖。
例如,分析下麵的代碼中父進程三處加獨占鎖位置(1)、(2)、(3)對子進程中加共用鎖的影響。
use Fcntl qw(:flock);
open my $fh, ">", "a.log";
# (1) flock $fh, LOCK_EX;
# 這裡開始fork子進程
my $pid = fork;
# (3) flock $fh, LOCK_EX;
unless($pid){
# 子進程
# flock $fh, LOCK_SH;
}
# 父進程
# (2) flock $fh, LOCK_EX;
首先分析父進程在(3)處加鎖對子進程的影響。(3)是在fork後且進入子進程代碼段之前運行的,也就是說父子進程都執行了一次flock加獨占鎖,顯然只有一個進程能夠加鎖。但無論是誰加鎖了,這個描述符上的鎖對另一個進程都是共用的,也就是兩個進程都持有EX鎖,這似乎違背了我們對獨占鎖的獨占性常識,但並沒有,因為實際上文件描述符上只有一個鎖,只不過這個鎖被兩個進程中的文件句柄持有了。因為子進程也持有EX鎖,自己可以直接申請SH鎖實現自己的鎖切換,如果父進程這時還沒有關閉文件句柄或解鎖,它也將持有SH鎖。
再看父進程中加在(1)或(2)處的獨占鎖,他們其實是等價的,因為在有了子進程後,無論在哪裡加鎖,鎖(文件描述符)都是共用的,引用計數都會是2。這時子進程要獲取共用鎖是完全無需阻塞的,因為它自己就持有了獨占鎖。
也就是說,上面無論是在(1)、(2)還是(3)處加鎖,在子進程中都能隨意無阻塞換鎖,因為子進程在換鎖前已經持有了這個文件描述符上的鎖。
那麼上面的示例中,如何讓子進程申請互斥鎖的時候被阻塞?只需在子進程中打開這個文件的新文件句柄即可,它會創建一個新的文件描述符,在兩個文件描述符上申請鎖時會檢查鎖的互斥性。但是必須記住,要讓子進程能成功申請到互斥鎖,必須在父進程中unlock或者在父子進程中都close(),往往我們會忘記在子進程中也關閉文件句柄而導致文件描述符繼續存在,其上的鎖也繼續保留,從而導致子進程在該文件描述符上持有的鎖阻塞了自己去申請其它描述符的鎖。
例如,下麵在子進程中打開了新的$fh1
,且父子進程都使用close()來保證文件描述符的關閉、鎖的釋放。當然,也可以直接在父或子進程中使用一次flock $fh, LOCK_UN
來直接釋放鎖。
use Fcntl qw(:flock);
open my $fh, ">", "a.log";
# (1) flock $fh, LOCK_EX;
# 這裡開始fork子進程
my $pid = fork;
# (3) flock $fh, LOCK_EX;
unless($pid){
# 子進程
open $fh1, ">", "a.log";
close $fh; # close(1)
# flock $fh1, LOCK_SH;
}
# 父進程
# (2) flock $fh, LOCK_EX;
close $fh; # close(2)