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)
相关文章
相关标签/搜索