如何在Perl中锁定文件?

29

在Perl中创建文件锁的最佳方式是什么?

是使用flock函数在文件上创建锁,还是创建一个锁文件并在锁文件上放置锁并检查锁文件上的锁更好?


1
如何编写Perl的小提示: https://dev59.com/FHVD5IYBdhLWcg3wJIEK - szabgab
不确定这里的Perl程序员是否会像C++那样做一个“FAQ”,但我认为这是一个很好的例子。 - thecoshman
14个回答

32
如果您最终使用flock,以下是一些相关代码:

如果您最终使用flock,以下是一些相关代码:

use Fcntl ':flock'; # Import LOCK_* constants

# We will use this file path in error messages and function calls.
# Don't type it out more than once in your code.  Use a variable.
my $file = '/path/to/some/file';

# Open the file for appending.  Note the file path is quoted
# in the error message.  This helps debug situations where you
# have a stray space at the start or end of the path.
open(my $fh, '>>', $file) or die "Could not open '$file' - $!";

# Get exclusive lock (will block until it does)
flock($fh, LOCK_EX) or die "Could not lock '$file' - $!";

# Do something with the file here...

# Do NOT use flock() to unlock the file if you wrote to the
# file in the "do something" section above.  This could create
# a race condition.  The close() call below will unlock the
# file for you, but only after writing any buffered data.

# In a world of buffered i/o, some or all of your data may not 
# be written until close() completes.  Always, always, ALWAYS 
# check the return value of close() if you wrote to the file!
close($fh) or die "Could not write '$file' - $!";

一些有用的链接:

针对您添加的问题,我建议在文件上放置锁定或创建一个名为“lock”的文件,每当文件被锁定时调用它,并在不再锁定时将其删除(然后确保您的程序遵守这些语义)。


你使用追加符号 >> 而不是创建/覆盖符号 >,有什么原因吗? - xagyg
注意:根据文档,“为避免协调不当的可能性,Perl现在在锁定或解锁之前刷新$fh。”这种刷新是否消除了竞争条件的可能性? - Håkon Hægland

11
其他答案已经很好地涵盖了Perl flock锁定,但在许多Unix / Linux系统上,实际上有两个独立的锁定系统:BSD flock()和基于POSIX fcntl()的锁定。
除非在构建Perl时提供特殊选项进行配置,否则其flock将使用flock()(如果可用)。 如果您只需要在应用程序内部(在单个系统上运行)进行锁定,则通常情况下这是可以接受的,并且可能是您想要的。 但是,有时您需要与使用fcntl()锁定的另一个应用程序交互(例如,在许多系统上使用Sendmail),或者您可能需要在NFS挂载的文件系统上进行文件锁定。
在这些情况下,您可能需要查看File :: FcntlLockFile :: lockf。 还可以在纯Perl中执行基于fcntl()的锁定(使用一些棘手且不可移植的pack()位)。
flock / fcntl / lockf差异的快速概述: lockf几乎总是在fcntl之上实现的,仅具有文件级别的锁定。 如果使用fcntl实现,则以下限制也适用于lockf。
fcntl提供范围级别的锁定(在文件内)和NFS上的网络锁定,但是在fork()之后,子进程不会继承锁定。 在许多系统上,您必须将文件句柄打开为只读以请求共享锁定,并且必须将其设置为读写以请求独占锁定。

flock仅支持文件级别的锁定,锁定只在单台机器内生效(您可以锁定NFS挂载的文件,但只有本地进程才能看到锁)。子进程会继承锁定状态(假设文件描述符没有关闭)。

有时候(SYSV系统),flock会使用lockf或fcntl进行模拟;在某些BSD系统中,lockf会使用flock进行模拟。一般来说,这些模拟方式的性能较差,建议尽量避免使用。


2
感谢您对底层系统调用之间某些区别的精彩解释。维基百科的Unix文件锁问题列表可能应该是必读的。 - tchrist

7
CPAN来拯救:IO::LockedFile。请参考此链接

啊啊啊!!!面向对象的 Perl 真吓人。IO::LockedFile 是使用 flock 函数实现的。 - Ryan P
CPAN模块对我来说看起来不错。即使只有几行代码,我也宁愿重用代码而不是每次都自己编写。Perl面向对象的语法相当丑陋,但它运行得还不错。 - Sam Watkins

6

Ryan P写道:

在这种情况下,当文件重新打开时,文件实际上会短暂解锁。

所以不要这样做。相反,以读/写方式打开文件:

open my $fh, '+<', 'test.dat'
    or die "Couldn’t open test.dat: $!\n";

当你准备编写计数器时,只需将指针seek移到文件的开头。注意,如果这样做,你应该在close之前进行truncate,以防止文件留下尾随垃圾,如果新内容比旧内容短。(通常,文件中的当前位置在其末尾,因此你可以直接写truncate $fh, tell $fh。)

此外,请注意我使用了三个参数的open和词法文件句柄,并检查操作的成功。 请避免全局文件句柄(全局变量不好,嗯?)和魔法两个参数的open(它曾经是 Perl 代码中许多漏洞(可利用)的源头),并始终测试你的open是否成功。


回到文件开头是否会覆盖文件内容?这是我的具体问题。我正在使用die进行测试,只是为了在示例中保持代码简单。我从未想过利用2个参数的打开函数,谢谢提醒,我会记住的。 - Ryan P
1
是的,它会覆盖文件内容。但我忘了提到,在关闭文件之前,您需要截断文件,以确保如果新内容较短,文件不会以尾随垃圾结束。 - Aristotle Pagaltzis

5
我认为最好使用词法变量作为文件句柄和错误处理来展示这个问题。此外,最好使用Fcntl模块中的常量而不是硬编码神奇数字2,因为在所有操作系统上可能不是正确的数字。
    use Fcntl ':flock'; # 导入LOCK_*常量
# 打开文件进行追加 open (my $fh, '>>', 'test.dat') or die $!;
# 尝试独占锁定文件,将等待直到获得锁定 flock($fh, LOCK_EX);
# 在此处执行一些与文件有关的操作(在我们的情况下打印)
# 实际上你不应该解锁文件 # 关闭文件将解锁它 close($fh) or warn "Could not close file $!";

查看完整的flock文档和PerlMonks上的文件锁定教程,尽管那也使用旧的文件句柄用法。

实际上,我通常跳过close()的错误处理,因为如果失败了,我也没有太多可以做的。

关于锁定什么,如果您只在一个文件中工作,请锁定该文件。 如果您需要一次锁定多个文件,则为了避免死锁,最好选择一个要锁定的文件。 无论那个文件是您真正需要锁定的几个文件之一,还是专门为锁定目的创建的单独文件都没有关系。


2
无论如何,你应该检查 close。虽然你不能做太多事情,但你至少可以告诉用户并退出,而不是默默地继续运行,好像什么都没有发生。 - Aristotle Pagaltzis
你应该更重要地检查flock是否失败。当然,大多数情况下它不会失败,但一旦失败,你就会陷入麻烦。根据Perl flock实现,请参见flock(2)lockf(3)或者fcntl(2) - Matija Nalis

4

您是否考虑过使用LockFile::Simple模块?它已经为您完成了大部分工作。

在我过去的经验中,我发现它非常易于使用且稳定可靠。


这看起来比flock结构要复杂一些。我不清楚flock的并发问题,但这特别指出了NFS可能存在的竞态条件问题,而我们将使用它。 - Ryan P

3
use strict;

use Fcntl ':flock'; # Import LOCK_* constants

# We will use this file path in error messages and function calls.
# Don't type it out more than once in your code.  Use a variable.
my $file = '/path/to/some/file';

# Open the file for appending.  Note the file path is in quoted
# in the error message.  This helps debug situations where you
# have a stray space at the start or end of the path.
open(my $fh, '>>', $file) or die "Could not open '$file' - $!";

# Get exclusive lock (will block until it does)
flock($fh, LOCK_EX);


# Do something with the file here...


# Do NOT use flock() to unlock the file if you wrote to the
# file in the "do something" section above.  This could create
# a race condition.  The close() call below will unlock it
# for you, but only after writing any buffered data.

# In a world of buffered i/o, some or all of your data will not 
# be written until close() completes.  Always, always, ALWAYS 
# check the return value on close()!
close($fh) or die "Could not write '$file' - $!";

1

我在这个问题中的目标是锁定一个被多个脚本用作数据存储的文件。最终,我使用了类似于以下代码(来自Chris)的代码:

open (FILE, '>>', test.dat') ; # open the file 
flock FILE, 2; # try to lock the file 
# do something with the file here 
close(FILE); # close the file

在他的例子中,我删除了 flock FILE, 8,因为 close(FILE) 也执行了此操作。真正的问题是当脚本启动时,它必须保持当前计数器,当它结束时,它必须更新计数器。这就是 Perl 遇到问题的地方,要读取文件,你需要:
 open (FILE, '<', test.dat');
 flock FILE, 2;

现在我想要将结果写出来,由于我想要覆盖文件,所以我需要重新打开并截断它,这将导致以下结果:

 open (FILE, '>', test.dat'); #single arrow truncates double appends
 flock FILE, 2;

在这种情况下,文件在重新打开时实际上只会短暂地解锁。这展示了外部锁定文件的情况。如果您要更改文件的上下文,请使用锁定文件。修改后的代码:
open (LOCK_FILE, '<', test.dat.lock') or die "Could not obtain lock";
flock LOCK_FILE, 2;
open (FILE, '<', test.dat') or die "Could not open file";
# read file
# ...
open (FILE, '>', test.dat') or die "Could not reopen file";
#write file
close (FILE);
close (LOCK_FILE);

另一种解决方案:从开头以读/写模式打开文件,并使用独占锁(LOCK_EX),而不是使用单独的锁文件。这将允许您在不关闭或释放锁的情况下读取并写入文件。 - DavidBooth

1

基于http://metacpan.org/pod/File::FcntlLock开发

use Fcntl qw(:DEFAULT :flock :seek :Fcompat);
use File::FcntlLock;
sub acquire_lock {
  my $fn = shift;
  my $justPrint = shift || 0;
  confess "Too many args" if defined shift;
  confess "Not enough args" if !defined $justPrint;

  my $rv = TRUE;
  my $fh;
  sysopen($fh, $fn, O_RDWR | O_CREAT) or LOGDIE "failed to open: $fn: $!";
  $fh->autoflush(1);
  ALWAYS "acquiring lock: $fn";
  my $fs = new File::FcntlLock;
  $fs->l_type( F_WRLCK );
  $fs->l_whence( SEEK_SET );
  $fs->l_start( 0 );
  $fs->lock( $fh, F_SETLKW ) or LOGDIE  "failed to get write lock: $fn:" . $fs->error;
  my $num = <$fh> || 0;
  return ($fh, $num);
}

sub release_lock {
  my $fn = shift;
  my $fh = shift;
  my $num = shift;
  my $justPrint = shift || 0;

  seek($fh, 0, SEEK_SET) or LOGDIE "seek failed: $fn: $!";
  print $fh "$num\n" or LOGDIE "write failed: $fn: $!";
  truncate($fh, tell($fh)) or LOGDIE "truncate failed: $fn: $!";
  my $fs = new File::FcntlLock;
  $fs->l_type(F_UNLCK);
  ALWAYS "releasing lock: $fn";
  $fs->lock( $fh, F_SETLK ) or LOGDIE "unlock failed: $fn: " . $fs->error;
  close($fh) or LOGDIE "close failed: $fn: $!";
}

1

除了使用锁文件的方法外,另一种替代方案是使用锁套接字。可以参考CPAN上的Lock::Socket实现。使用方法如下:

use Lock::Socket qw/lock_socket/;
my $lock = lock_socket(5197); # raises exception if lock already taken

使用套接字的几个优点:
  • 通过操作系统保证不会有两个应用程序持有相同锁:没有竞态条件。
  • 通过操作系统保证在进程退出时清理整洁,因此无需处理陈旧的锁。
  • 依赖于 Perl 运行的任何东西都支持的功能:例如在 Win32 上没有 flock(2) 支持的问题。
显然的缺点是锁名称空间是全局的。如果另一个进程决定锁定您需要的端口,则可能会发生一种拒绝服务类型的情况。
[披露:我是上述模块的作者]

使用Lock::Socket,而不是使用Lock::Simple。 - Matija Nalis
谢谢Matija Nalis!我已经修复了这个例子。 - Mark Lawrence

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接