当多个进程或多个程序都想要修同一个文件的时候,若是不加控制,多进程或多程序将可能致使文件更新的丢失。node
例如进程1和进程2都要写入数据到a.txt中,进程1获取到了文件句柄,进程2也获取到了文件句柄,而后进程1写入一段数据,进程2写入一段数据,进程1关闭文件句柄,会将数据flush到文件中,进程2也关闭文件句柄,也将flush到文件中,因而进程1的数据被进程2保存的数据覆盖了。多线程
因此,多进程修改同一文件的时候,须要协调每一个进程:less
这种协调方式能够经过文件锁来实现。文件锁分两种,独占锁(写锁)和共享锁(读锁)。当进程想要修改文件的时候,申请独占锁(写锁),当进程想要读取文件数据的时候,申请共享锁(读锁)。操作系统
独占锁和独占锁、独占锁和共享锁都是互斥的。只要进程1持有了独占锁,进程2想要申请独占锁或共享锁都将失败(阻塞),也就保证了这一时刻只有进程1能修改文件,只有当进程1释放了独占锁,进程2才能继续申请到独占锁或共享锁。可是共享锁和共享锁是能够共存的,这表明的是两个进程都只是要去读取数据,并不互相冲突。线程
独占锁 共享锁 独占锁 × × 共享锁 × √
Linux上的文件锁类型主要有两种:flock和lockf。后者是fcntl系统调用的一个封装。它们之间有些区别:code
Perl中主要使用flock来实现文件锁,也是本文的主要内容。blog
flock FILEHANDLE, flags;
flock两个参数,第一个是文件句柄,第二个是锁标志。进程
锁标志有4种,有数值格式的一、二、八、4,在导入Fcntl模块的:flock
后,也支持字符格式的LOCK_SH
、LOCK_EX
、LOCK_UN
、LOCK_NB
。ip
字符格式 数值格式 意义 ----------------------------------- 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;
在开始以前,先看看在Perl中的fork、文件句柄、文件描述符、flock之间的结论。
flock $fh, LOCK_UN
会直接释放文件描述符上的锁(图注: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)