该脚本如何确保自身只运行一个实例?


22

2013年8月19日,Randal L. Schwartz发布了 Shell脚本,该脚本旨在确保在Linux上“该脚本只有一个实例在运行,而没有竞争条件或无需清除锁定文件”:

#!/bin/sh
# randal_l_schwartz_001.sh
(
    if ! flock -n -x 0
    then
        echo "$$ cannot get flock"
        exit 0
    fi
    echo "$$ start"
    sleep 10 # for testing.  put the real task here
    echo "$$ end"
) < $0

它似乎按广告宣传工作:

$ ./randal_l_schwartz_001.sh & ./randal_l_schwartz_001.sh
[1] 11863
11863 start
11864 cannot get flock
$ 11863 end

[1]+  Done                    ./randal_l_schwartz_001.sh
$

这是我的理解:

  • 脚本将<自身内容的副本(即从$0)重定向()到0子shell 的STDIN(即文件描述符)。
  • 在子Shell中,脚本尝试获取flock -n -x文件描述符上的非阻塞排他锁()0
    • 如果该尝试失败,则子外壳程序退出(主脚本也将退出,因为没有其他操作可做)。
    • 如果尝试成功,则子外壳程序将运行所需的任务。

这是我的问题:

  • 为什么脚本需要将其自身内容的副本而不是其他文件的内容重定向到子shell继承的文件描述符?(我尝试从另一个文件重定向并按上述方式重新运行,并且执行顺序发生了变化:非后台任务在后台任务之前获得了锁。因此,也许使用文件本身的内容可以避免竞争;但是如何?)
  • 无论如何,为什么脚本需要将文件内容的副本重定向到子shell继承的文件描述符?
  • 为什么0在一个shell中对文件描述符进行独占锁定会阻止在不同shell中运行的同一脚本的副本获得对文件描述符的独占锁定0?不要炮弹都有自己的标准文件描述符(的单独副本012,即STDIN,STDOUT和STDERR)?

当您尝试从其他文件重定向实验时,确切的测试过程是什么?
Freiheit

1
我想您可以参考此链接。stackoverflow.com/questions/185451/...
德布Paikar

Answers:


22

为什么脚本需要将其自身内容的副本而不是其他文件的内容重定向到子shell继承的文件描述符?

您可以使用任何文件,只要脚本的所有副本都使用同一文件即可。使用$0只是绑锁脚本本身:如果您复制的脚本,然后修改它的一些其他用途,您不需要拿出了锁定文件的新名称。这很方便。

如果通过符号链接调用脚本,则锁定在实际文件上,而不是链接上。

(当然,如果某个进程运行该脚本并为其赋值作为第零参数而不是实际路径,则此方法会中断。但这很少这样做。)

(我尝试使用其他文件并如上所述重新运行,并且执行顺序已更改)

您确定那是因为使用了文件,而不仅仅是随机变化吗?与管道一样,实际上没有办法确保命令以什么顺序运行cmd1 & cmd。这主要取决于操作系统调度程序。我的系统上出现随机变化。

无论如何,为什么脚本需要将文件内容的副本重定向到子shell继承的文件描述符?

看起来是这样,shell本身持有持有锁的文件描述的副本,而不仅仅是flock持有它的实用程序。flock(2)当具有它的文件描述符关闭时,释放使用进行的锁定。

flock有两种模式,一种是基于文件名进行锁定,然后运行外部命令(在这种情况下,该命令flock包含所需的打开文件描述符),或者从外部获取文件描述符,因此,外部进程负责保持它。

请注意,文件的内容在这里无关紧要,并且没有副本。重定向到子Shell本身不会复制任何数据,它只是打开文件的句柄。

为什么在一个外壳程序中对文件描述符0持有排他锁会阻止在另一个外壳程序中运行的同一脚本的副本在文件描述符0上获得排他锁?外壳程序是否没有它们自己的标准文件描述符的单独副本(0、1和2,即STDIN,STDOUT和STDERR)?

是的,但是锁在文件上,而不是文件描述符上。一次只有一个打开的文件实例可以持有该锁。


我认为通过使用exec打开锁文件的句柄,您应该能够在没有子外壳的情况下执行相同的操作:

$ cat lock.sh
#!/bin/sh

exec 9< "$0"

if ! flock -n -x 9; then
    echo "$$/$1 cannot get flock" 
    exit 0
fi

echo "$$/$1 got the lock"
sleep 2
echo "$$/$1 exit"

$ ./lock.sh bg & ./lock.sh fg ; wait; echo
[1] 11362
11363/fg got the lock
11362/bg cannot get flock
11363/fg exit
[1]+  Done                    ./lock.sh bg

1
使用{ }代替( )也可以避免子shell。
R.,

在G +帖子的评论中,有人进一步建议使用大致相同的方法exec
David Z

@R ..,哦,可以。但是,实际脚本周围的多余花括号仍然很难看。
ilkkachu

9

文件锁通过文件描述附加文件。在较高级别上,脚本的一个实例中的操作顺序为:

  1. 打开附加了锁的文件(“锁文件”)。
  2. 对锁定文件进行锁定。
  3. 做东西。
  4. 关闭锁定文件。这将释放附加到通过打开文件创建的文件描述上的锁。

持有锁会阻止运行同一脚本的另一个副本,因为这就是锁的作用。只要文件的排他锁存在于系统中的某个位置,就无法创建同一锁的第二个实例,即使通过不同的文件描述也是如此。

打开文件将创建文件描述。这是一个内核对象,在编程接口中没有太多直接可见性。您可以通过文件描述符间接访问文件描述,但是通常您将其视为访问文件(读取或写入其内容或元数据)。锁是文件描述的属性之一,而不是文件或描述符的属性。

刚开始时,打开文件时,文件描述只有一个文件描述符,但是可以通过创建另一个描述符(dup系统调用族)或分支一个子进程(此后父进程和子进程都可以创建更多描述符)来创建更多描述符。子级可以访问相同的文件描述)。文件描述符可以显式关闭,也可以在其进程终止时关闭。关闭附加到文件的最后一个文件描述符时,文件描述将关闭。

以下是上述操作顺序如何影响文件描述。

  1. 重定向<$0将在子外壳中打开脚本文件,从而创建文件描述。此时,在描述中附加了一个文件描述符:子外壳中的描述符编号0。
  2. 子shell调用flock并等待其退出。当flock运行时,描述中附加了两个描述符:子外壳中的数字0和flock进程中的数字0。当flock取得锁定时,它将设置文件描述的属性。如果另一个文件描述已经对该文件进行了锁定,则flock无法获得该锁定,因为它是排他性锁定。
  3. 子外壳可以完成工作。由于它在具有锁的描述上仍然具有打开的文件描述符,因此该描述保持存在,并且由于没有人删除该锁,因此它保持了其锁。
  4. 子外壳在右括号处消失。这将关闭具有锁定的文件描述上的最后一个文件描述符,因此该锁定将在此时消失。

该脚本使用重定向自的原因$0是重定向是在shell中打开文件的唯一方法,而保持重定向活动是保持文件描述符打开的唯一方法。子外壳从不从其标准输入读取数据,只需要保持打开状态即可。在可以直接访问打开和关闭呼叫的语言中,您可以使用

fd = open($0)
flock(fd, LOCK_EX)
do stuff
close(fd)

如果您使用exec内置函数进行重定向,则实际上可以在Shell中获得相同的操作顺序。

exec <$0
flock -n -x 0
# do stuff
exec <&-

如果脚本想要继续访问原始标准输入,则可以使用其他文件描述符。

exec 3<$0
flock -n -x 0
# do stuff
exec 3<&-

或带有子shell:

(
  flock -n -x 3
  # do stuff
) 3<$0

锁不必在脚本文件上。它可以在可以打开以读取的任何文件上(因此必须存在,它必须是可以读取的文件类型,例如常规文件或命名管道,但不能是目录,并且脚本进程必须具有阅读权限)。脚本文件的优点是可以保证它的存在和可读性(在极端情况下,除非在脚本被调用到脚本到达<$0重定向时间之间在外部将其删除)。

只要flock成功,并且脚本位于锁定不是错误的文件系统上(某些网络文件系统,例如NFS可能是错误),我看不出如何使用不同的锁定文件会导致竞争状态。我怀疑您有操纵错误。


有一个竞争条件:您无法控制脚本的哪个实例获得锁。幸运的是,几乎所有目的都没关系。
Mark

4
@Mark争分夺秒,但这不是竞赛条件。一个竞争条件是,当时间可以让坏事发生,如两个过程在同一时间相同的关键部分是。不知道哪个进程将进入关键部分是预期的不确定性,这不是竞争条件。
吉尔斯(Gillles)“所以别再邪恶了”

1
仅供参考,“文件描述”中的链接指向Open Group规格索引页面,而不是对该概念的具体描述,这是我认为您打算做的。或者,您也可以在此处链接您的较旧答案以及unix.stackexchange.com/a/195164/85039
Sergiy Kolodyazhnyy

5

用于锁定的文件并不重要,脚本使用该脚本$0是因为已知该文件存在。

获取锁的顺序或多或少是随机的,具体取决于计算机能够启动两个任务的速度。

您可以使用任何文件描述符,不一定是0,锁被保留在文件打开的文件描述符,而不是描述符本身。

( flock -x 9 || exit 1
  echo 'Locking for 5 secs'; sleep 5; echo 'Done' ) 9>/tmp/lock &
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.