文件锁
当多个进程或多个程序都想要修同一个文件的时候, 如果不加控制, 多进程或多程序将可能导致文件更新的丢失.
例如进程 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)
来源: https://www.cnblogs.com/f-ck-need-u/p/10447881.html