文件锁

当多个进程或多个程序都想要修同一个文件的时候,如果不加控制,多进程或多程序将可能导致文件更新的丢失。

例如进程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_SHLOCK_EXLOCK_UNLOCK_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是在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)

Perl IO:文件锁的更多相关文章

  1. Perl IO:操作系统层次的IO

    sysopen() open()和sysopen()都打开文件句柄,open()是比较高层次的打开文件句柄,sysopen()相对要底层一点.但它们打开的文件句柄并没有区别,只不过sysopen()有 ...

  2. Perl IO:IO重定向

    文件句柄和文件描述符的关系 文件描述符是操作系统的资源,对于实体文件来说,每打开一次文件,操作系统都会为该进程分配一个文件描述符来关联(指向)这个文件,以后操作文件数据都根据这个文件描述符来操作,而不 ...

  3. Perl IO:简介和常用IO模块

    三篇Perl IO基础类文章: Perl的IO操作(1):文件句柄 Perl的IO操作(2):更多文件句柄的模式 Perl文件句柄相关的常见变量 IO对象和IO::Module家族模块 无论是哪种高级 ...

  4. Perl IO:随机读写文件

    随机读写 如果一个文件句柄是指向一个实体文件的,那么就可以对它进行随机数据的访问(包括随机读.写),随机访问表示可以读取文件中的任何一部分数据或者向文件中的任何一个位置处写入数据.实现这种随机读写的功 ...

  5. Perl IO:read()函数

    read()函数 read()函数用于从文件句柄中读取指定字节数的数据并写入到一个标量中.如果文件句柄是以Unicode方式打开的,则表示读取指定字符数而非字节数. 有两种read方式: read F ...

  6. 高级IO——文件锁

    文件锁也被称为记录所,文件锁如果深讲的话,内容不少(比如文件锁最起码分为了建议锁和强制性锁,暂时挖坑,后面填). 文件锁作用 顾名思义,就是用来保护文件数据的.当多个进程共享读写同一个文件时,为了不让 ...

  7. Perl的IO操作(2):更多文件句柄模式

    open函数除了> >> <这三种最基本的文件句柄模式,还支持更丰富的操作模式,例如管道.其实bash shell支持的重定向模式,perl都支持,即使是2>&1 ...

  8. Perl系列文章

    0.Perl书籍推荐 Perl书籍下载 密码:kkqx 下面是一些我学习Perl过程中读过完整的或部分章节的觉得好的书. 入门级别1:<Perl语言入门>即小骆驼 入门级别2:<In ...

  9. 在vi中使用perltidy格式化perl代码

    格式优美的perl代码不但让人赏心悦目,并且能够方便阅读. perltidy的是sourceforge的一个小项目,在我们写完乱七八糟的代码后,他能像变魔术一样把代码整理得漂美丽亮,快来体验一下吧!! ...

随机推荐

  1. java课程之团队开发冲刺阶段1.5

    一.总结昨天进度 1.昨天由于时间较少,没有太多的时间来进行学习Sqlite 二.遇到的困难 1.由于最终的程序需要调用本地的数据库,所以我们需要在安装程序的时候就需要直接附带安装一个本地的数据库到手 ...

  2. MySQL 中 having 和 where 的区别

    区别一: where 是数据从磁盘读入内存时候一条一条判断的 having 是将所有数据读入内存,在分组统计前,根据having的条件再将不符合条件的数据删除 区别二: having 子句可以使用字段 ...

  3. Session Cookie介绍和使用

    Cookie机制 Cookie机制 Cookie是服务器存储在本地计算机上的小块文本,并随每个请求发送到同一服务器. IETF RFC 2965 HTTP状态管理机制是一种通用的cookie规范. W ...

  4. 文件上传控件bootstrap-fileinput的使用

    1.插件下载地址:https://github.com/kartik-v/bootstrap-fileinput 2.插件的引用 需要引用jquery 需要结合bootstrap使用,即页面需要引入b ...

  5. vi 配置

    vim       ~/.vimrc source    ~/.vimrc 添加相关配置 一直生效 0  行首 $    行尾 spacebar   空格键:         合并多行 spaceba ...

  6. 用 Java 解密 C# 加密的数据(DES)(转)

    今天遇到java解密url的问题.我们的系统要获取外部传过来的URL,URL是采用 DES 算法对消息进行加密,再用 BASE64 编码.不过对方系统是用 C# 写的. 在网上搜了几篇文章终于找到一篇 ...

  7. Mesos源码分析(9): Test Framework的启动

    我们以Test Framework为例子解释Framework的启动方式. Test Framework的代码在src/examples/test_framework.cpp中的main函数 首先要指 ...

  8. Redis API的原子性分析

    在学习Redis的常用操作时,经常看到介绍说,Redis的set.get以及hset等等命令的执行都是原子性的,但是令自己百思不得其解的是,为什么这些操作是原子性的? 原子性 原子性是数据库的事务中的 ...

  9. 关于H5页面的测试总结与分析

    一.时下最流行的H5到底是什么 ?有什么优势和劣势? (1)H5 即HTML5,其实就是:移动端Web页面. (2)优势: H5可以跨平台使用,开发成本相对较低 H5可随时上线就更新版本,适合快速迭代 ...

  10. Javascript高级编程学习笔记(89)—— Canvas(6) 变换

    变换 通过上下文的变化,可以对图像进行处理后再将其绘制到画布上 当我们创建上下文时,会以默认值初始化变化矩阵,在默认的变换矩阵下所有处理都按描述直接绘制. 而当我们为上下文应用变换时,会导致使用不同的 ...