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

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

2013 年 8 月 19 日,兰德尔·施瓦茨发布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
$

这是我的理解:

  • 该脚本将<其自身内容(即 from )的副本重定向 ( ) 到子 shell 的$0STDIN(即文件描述符)。0
  • flock -n -x在子 shell 内,脚本尝试获取文件描述符上的非阻塞、独占锁 ( ) 0
    • 如果该尝试失败,子 shell 就会退出(主脚本也会退出,因为它没有其他事情可做)。
    • 如果尝试成功,子 shell 就会运行所需的任务。

这是我的问题:

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

答案1

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

您可以使用任何文件,只要脚本的所有副本都使用同一个文件即可。使用$0只会将锁与脚本本身绑定:如果您复制脚本并将其修改为其他用途,则无需为锁文件想出新名称。这很方便。

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

(当然,如果某个进程运行脚本并给它一个虚构的值作为第零个参数而不是实际路径,那么这就会中断。但这种情况很少发生。)

(我尝试使用不同的文件并按照上面的方式重新运行,并且执行顺序发生了变化)

您确定这是因为使用的文件,而不仅仅是随机变化吗?与管道一样,实际上无法确定命令在cmd1 & cmd.这主要取决于操作系统调度程序。我的系统出现随机变化。

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

看起来外壳本身保存了持有锁的文件描述的副本,而不仅仅是flock持有它的实用程序。flock(2)当拥有该锁的文件描述符被关闭时,使用该锁创建的锁就会被释放。

flock有两种模式,要么根据文件名获取锁定,然后运行外部命令(在这种情况下flock保存所需的打开文件描述符),要么从外部获取文件描述符,因此外部进程负责保存它。

请注意,该文件的内容与此处无关,并且没有制作任何副本。到子 shell 的重定向本身不会复制任何数据,它只是打开文件的句柄。

为什么在一个 shell 中持有文件描述符 0 上的独占锁会阻止在不同 shell 中运行的同一脚本的副本获得文件描述符 0 上的独占锁? shell 没有自己的、独立的标准文件描述符副本(0、1 和 2,即 STDIN、STDOUT 和 STDERR)吗?

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


我认为您应该能够在没有子 shell 的情况下通过打开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

答案2

附有文件锁一个文件,通过文件描述。概括地说,脚本的一个实例中的操作顺序是:

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

持有锁可以防止同一脚本的另一个副本运行,因为这就是锁的作用。只要系统上某个文件存在独占锁,就不可能创建同一锁的第二个实例,即使通过不同的文件描述也是如此。

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

开始时,当打开文件时,文件描述具有单个文件描述符,但是可以通过创建另一个描述符(dup系统调用系列)或通过分叉子进程(之后父进程和子进程)来创建更多描述符。孩子可以访问相同的文件描述)。文件描述符可以显式关闭或当它所在的进程终止时关闭。当附加到文件的最后一个文件描述符关闭时,文件描述也被关闭。

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

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

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

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

如果使用内置命令进行重定向,实际上可以在 shell 中获得相同的操作序列exec

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

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

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

或使用子外壳:

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

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

只要flock成功,并且脚本位于锁没有错误的文件系统上(某些网络文件系统,例如 NFS 可能有错误),我不明白使用不同的锁文件如何允许竞争条件。我怀疑你的操作失误。

答案3

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

获取锁的顺序或多或少是随机的,具体取决于您的机器启动这两个任务的速度。

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

( flock -x 9 || exit 1
  echo 'Locking for 5 secs'; sleep 5; echo 'Done' ) 9>/tmp/lock &

相关内容