Perl IO:文件鎖

文件鎖

當多個進程或多個程序都想要修同一個文件的時候,若是不加控制,多進程或多程序將可能致使文件更新的丟失。node

例如進程1和進程2都要寫入數據到a.txt中,進程1獲取到了文件句柄,進程2也獲取到了文件句柄,而後進程1寫入一段數據,進程2寫入一段數據,進程1關閉文件句柄,會將數據flush到文件中,進程2也關閉文件句柄,也將flush到文件中,因而進程1的數據被進程2保存的數據覆蓋了。多線程

因此,多進程修改同一文件的時候,須要協調每一個進程:less

  • 保證文件在同一時間只能被一個進程修改,只有進程1修改完成以後,進程2才能得到修改權
  • 進程1得到了修改權,就不容許進程2去讀取這個文件的數據,由於進程2可能讀取出來的數據是進程1修改前的過時數據

這種協調方式能夠經過文件鎖來實現。文件鎖分兩種,獨佔鎖(寫鎖)和共享鎖(讀鎖)。當進程想要修改文件的時候,申請獨佔鎖(寫鎖),當進程想要讀取文件數據的時候,申請共享鎖(讀鎖)。操作系統

獨佔鎖和獨佔鎖、獨佔鎖和共享鎖都是互斥的。只要進程1持有了獨佔鎖,進程2想要申請獨佔鎖或共享鎖都將失敗(阻塞),也就保證了這一時刻只有進程1能修改文件,只有當進程1釋放了獨佔鎖,進程2才能繼續申請到獨佔鎖或共享鎖。可是共享鎖和共享鎖是能夠共存的,這表明的是兩個進程都只是要去讀取數據,並不互相沖突。線程

獨佔鎖       共享鎖
獨佔鎖     ×           ×
共享鎖     ×           √

文件鎖:flock和lockf

Linux上的文件鎖類型主要有兩種:flock和lockf。後者是fcntl系統調用的一個封裝。它們之間有些區別:code

  • flock來自BSD,而fcntl或lockf來自POSIX,因此lockf或fcntl實現的鎖也稱爲POSIX鎖
  • flock只能對整個文件加鎖,而fcntl或lockf能夠對文件中的部分加鎖,即粒度更細的記錄鎖
  • flock的鎖是勸告鎖,lockf或fcntl能夠實現強制鎖。所謂勸告鎖,是指只有多進程雙方都遵紀守法地使用flock鎖纔有意義,某進程使用flock,但另外一進程不使用flock,則flock鎖對另外一進程徹底無限制
  • flock鎖是附加在(關聯在)文件描述符上的(見下文更深刻的描述),而lockf是關聯在文件實體上的。本文後面將詳細分析flock鎖在文件描述符上的現象

Perl中主要使用flock來實現文件鎖,也是本文的主要內容。blog

Perl的flock

flock FILEHANDLE, flags;

flock兩個參數,第一個是文件句柄,第二個是鎖標誌。進程

鎖標誌有4種,有數值格式的一、二、八、4,在導入Fcntl模塊的:flock後,也支持字符格式的LOCK_SHLOCK_EXLOCK_UNLOCK_NBip

字符格式      數值格式      意義
-----------------------------------
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是在vnode/generic-inode上的,它比fd底層的多(fd->fd table->open file table->vnode/g-inode),只不過對於perl的fork而言,由於不會複製文件描述符,使得將flock認爲附在文件描述符上也沒什麼問題,只有open操做纔會在vnode上檢測flock的互斥性,換句話說,在perl中,只有屢次open才須要考慮flock的互斥性)
  • flock是進程級別的,不適用於在多線程中使用它來鎖互斥
  • 因此fork後的父子進程在共享文件描述符的同時也會共享flock鎖
  • flock $fh, LOCK_UN會直接釋放文件描述符上的鎖
  • 當文件描述符被關閉時,文件描述符上的鎖也會自動釋放。因此使用close()去釋放鎖的時候,必需要保證全部文件句柄都被關閉才能關閉文件描述符從而釋放鎖
  • flock(包括加鎖和解鎖)或close()都會自動flush IO Buffer,保證多進程間獲取鎖時數據同步
  • 只要持有了某個文件描述符上的鎖,在這把鎖釋放以前,本身能夠隨意更換鎖的類型,例如屢次flock從EX鎖變成SH鎖

(圖注:fd是用戶空間的內容,圖中放在內核層是爲了歸納與之關聯的內核層的幾個結構:fd對應內核層的這幾個結構)

下面是正式介紹和解釋。

在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)
相關文章
相關標籤/搜索