Perl IO:文件锁
文件鎖
當多個進程或多個程序都想要修同一個文件的時候,如果不加控制,多進程或多程序將可能導致文件更新的丟失。
例如進程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/perluse 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/perluse 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)轉載于:https://www.cnblogs.com/f-ck-need-u/p/10447881.html
總結
以上是生活随笔為你收集整理的Perl IO:文件锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面试官爱问的10大经典排序算法,20+张
- 下一篇: 我是如何把一个15分钟的程序优化到了10