这个脚本如何确保只有一个它自己的实例在运行?

23 linux io-redirection shell-script lock subshell

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
Run Code Online (Sandbox Code Playgroud)

它似乎像宣传的那样工作:

$ ./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
$
Run Code Online (Sandbox Code Playgroud)

以下是我的理解:

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

以下是我的问题:

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

ilk*_*chu 21

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

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

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

(当然,如果某个进程运行脚本并为其提供一个虚构的值作为第零个参数而不是实际路径,那么这会中断。但很少这样做。)

(我尝试使用不同的文件并重新运行如上,执行顺序改变了)

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

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

看起来是这样,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
Run Code Online (Sandbox Code Playgroud)


Gil*_*il' 9

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

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

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

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

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

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

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

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

fd = open($0)
flock(fd, LOCK_EX)
do stuff
close(fd)
Run Code Online (Sandbox Code Playgroud)

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

exec <$0
flock -n -x 0
# do stuff
exec <&-
Run Code Online (Sandbox Code Playgroud)

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

exec 3<$0
flock -n -x 0
# do stuff
exec 3<&-
Run Code Online (Sandbox Code Playgroud)

或使用子外壳:

(
  flock -n -x 3
  # do stuff
) 3<$0
Run Code Online (Sandbox Code Playgroud)

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

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

  • @Mark 存在锁定竞争,但这不是竞争条件。_race condition_ 是指当时间允许一些不好的事情发生时,例如两个进程同时处于同一个临界区。不知道哪个进程将进入临界区是预期的不确定性,它不是竞争条件。 (4认同)