如何在Perl中独占地读取文件?

4

我有一个Perl模块,我的收集脚本库使用它。这些脚本用于扫描我的网络,在我的网络设备上执行各种任务等。

大约有15个用户,我只希望每次只有1个人运行收集脚本。如果第二个用户尝试运行脚本,则希望他们等待第一个人完成。

下面的代码仅是测试平台,以便在投入生产之前正确运行它。我有一个具有nap函数的模块。我只希望一次只有一个人打盹。

sub nap {
        my $program = shift;
        my @arr;

        #open file to check the queue
        open(IN, $path); @arr=<IN>; close IN;

        #if someone is in the queue, print it out!
        $|++;
        if (@arr > 0) { print @arr; }

        #keep checking the queue, once the queue is empty it's my turn!
        while (@arr != 0) {
                open(IN, $path); @arr=<IN>; close IN;
                sleep 1;
        }

        #it's my turn, put my name in the queue
        open(IN,">",$path);
        print IN "$ENV{USER},$program";
        close IN;

        #take a nap!
        print "\n Sleep starting \n";
        sleep 10;

        #I'm finished with my nap, clear the queue so other's can go
        open(IN,">",$path);
        close IN;
        print "\nsleep over\n";
}

我的问题是,如果只有1个用户在等待,那么它可以正常工作,但是如果有2个用户在等待,他们仍然会同时休眠(在第一个用户完成后)。
我能否锁定或阻止此文件?我已经看到了flock,但无论如何锁定它,用户仍然可以读取。
这是否是正确的解决方案?还是有更好的用于这些目的的方法?

在Unix系统中,这是有意设计的。所有锁都是建议性的。 - Sobrique
2个回答

4
你的解决方案不正确。首先,你正在使用纯文本的open,它会缓冲读写操作,这会在多个进程通过一个文件进行通信时导致复杂情况。
正如你已经怀疑并且其他人已经评论的那样,在类Unix操作系统上没有合理的方法可以强制使只有一个进程可以从文件中读取。在某种意义上正确的处理方式是使用锁文件,并且只有当前持有锁的进程才能从数据/通信文件中读取。请查看perldoc -f flock以了解详情。
不幸的是,在Unix上对文件进行锁定确实存在一些缺点。特别是如果锁文件位于网络文件系统上,则它们可能不可靠。例如,对于NFS,功能性锁取决于挂载文件系统的所有计算机都运行着锁守护程序。一种有些欺骗性但传统的解决方法是滥用mkdir的语义。如果一堆进程都试图创建具有相同名称的目录,则保证只有其中一个会成功(好吧,或者没有,但现在略过)。您可以将其用于同步进程。在进程开始执行仅应由一个进程执行的任务之前,它尝试创建一个带有预定名称的目录。如果成功,那么它可以继续。如果失败,则表示其他人已经在工作,它必须等待。当活动进程完成工作时,它会删除目录,以便另一个进程可以成功创建它。
无论如何,基本消息是你需要两个文件系统元素:一个用于进程确定哪一个进程可以工作,另一个用于实际工作。

1
关于“你使用的是纯 open,它缓冲读写”的问题,实际上不是的。open 不会读取或者写入(无论是否缓冲)。readreadlineeof 都是带缓冲的读操作,而 sysread 则没有缓冲。默认情况下,所有的写操作都是被缓冲的(除了 STDERR 的写操作)。 - ikegami
一些类Unix系统支持强制锁定(包括文件的某些区域),这源于SysV。例如,在Linux上,如果您使用-o mand挂载FS并使用chmod g+s-x更改文件权限,则可以使用fcntl(F_SETLK)锁定文件的某个区域。尽管在Linux上通常不鼓励使用该功能 - Stephane Chazelas
感谢您的解释。 - genx1mx6

1
你可以锁定文件的数据部分来锁定文件本身,这样你就可以(滥用)它来控制对该脚本的排他访问。
我将其放在一个库文件nap.pl中:
#!usr/bin/env perl
use strict;
use Fcntl qw(LOCK_EX LOCK_NB);

sub nap {
    ## make sure this script only runs one copy of itself
    until ( flock DATA, LOCK_EX | LOCK_NB) {
        print "someone else has a lock\n";
        sleep 5;
    }
}

__DATA__
This exists to allow the locking code at the beginning of the file to work.
DO NOT REMOVE THESE LINES!

然后我打开了3个终端,每个终端都运行了这个命令:

#!/usr/bin/env perl
use strict;
do 'nap.pl';

&nap;
print `ls /tmp/`;
sleep 5;

第一个终端立即打印了我的 /tmp 目录的内容。 第二个终端打印了“有其他人锁定”,然后在 5 秒后,它打印了 /tmp 的内容。 第三个终端打印了“有其他人锁定”两次,一次是立即,然后在 5 秒后再次打印 /tmp 的内容。
但要小心将其放置在库中的位置,您需要确保不锁定不相关的子程序。
我个人会将锁定代码放在每个收集脚本中,而不是放在库中。 收集脚本是您实际上尝试仅运行一个实例的脚本。 看来您的标题不准确:您不是在尝试独占地读取文件,而是在尝试独占地运行文件。

感谢您详细的回复,我有几个问题。您能解释一下这行代码吗?until ( flock DATA, LOCK_EX | LOCK_NB) 为什么要使用 | 而不是 ||?此外,我在考虑库中的锁可能是最好的选择。我有许多不同的脚本使用相同的库,收集诊断信息、收集接口状态、检查合规性等等。我仍然希望只有一个脚本在同一时间运行(以减少资源使用和 SSH 会话)。您认为我应该只在正在运行的脚本中加锁吗? - genx1mx6
1
你需要使用 LOCK_EX 来获取独占锁。LOCK_NB 会将控制权返回给你的程序,这样你就可以打印出正在发生的事情,否则它会一直等待锁。你使用 | 而不是 ||,因为 | 是一个位运算符,即 LOCK_EX=2,LOCK_NB=4,LOCK_EX | LOCK_NB=6。 - dpw
1
如果您的目标真正只是让一个收集脚本运行,则锁定库就可以了。如果您有 foobar 的收集脚本,并且希望确保只有一个 foo 的副本运行,但不应该干扰正在运行 bar 的人,则应在 foobar 中放置锁定代码。 - dpw

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