如何避免 bash 脚本中的竞争条件?

How to avoid race conditions in a bash script?

#!/bin/bash
if [ ! -f numbers ]; then echo 0 > numbers; fi
count=0
touch numbers
echo $count > numbers
while [[ $count != 100 ]]; do
  if ln numbers numbers.lock
  then
    count=`expr $count + 1`
    n=`tail -1 numbers`
    expr $n + 1 >> numbers
    rm numbers.lock
  fi
done

我能做些什么来避免 count=`expr $count + 1`n=`tail -1 numbers` 的竞争条件,这样当我同时 运行 两个脚本时,它只会转到100,而不是200。我查了很多网站,但没有做一个巨大的功能就没有简洁的答案。

您已经安全地避免了与锁定文件的实际竞争条件。您描述的问题可以通过两种方式避免。

(1) 将锁定文件移到主循环之外,这样您的程序的两个实例就不能同时 运行 它们的主循环。如果一个是运行ning,另一个必须等​​到它完成,然后开始替换输出文件。

#!/bin/bash

# FIXME: broken, see comments

while true; do
    if ! ln numbers numbers.lock
    then
       sleep 1
    else
        if [ ! -f numbers ]; then echo 0 > numbers; fi
        count=0
        touch numbers
        #echo $count > numbers   # needless, isn't it?
        while [[ $count != 100 ]]; do
            count=`expr $count + 1`
            n=`tail -1 numbers`
            expr $n + 1 >> numbers
            rm numbers.lock
        done
        break
    fi
done

(2) 通过检查文件内容使两个实例合作。换句话说,当数字达到 100 时强制它们停止循环,而不管有多少其他进程正在写入此文件。 (我想当有超过 100 个实例 运行ning 时会有一个不确定的极端情况。)

#!/bin/bash
# FIXME: should properly lock here, too
if [ ! -f numbers ]; then echo 0 > numbers; fi
n=0
touch numbers
while [[ $n -lt 100 ]]; do
  if ln numbers numbers.lock
  then
    n=$(expr $(tail -1 numbers) + 1 | tee numbers)
    rm numbers.lock
  fi
done

根据您的要求,您可能实际上希望脚本在脚本的新实例启动时破坏文件中的任何先前值,但如果不是这样,echo 0 > numbers 应该由锁控制文件也是。

您确实想在 Bash 脚本中避免 expr; Bash 具有内置算术运算符。我没有尝试在此处重构该部分,但您可能应该这样做。也许更喜欢 Awk,这样您也可以分解出 tailawk '{ i=[=15=] } END { print 1+i }' numbers

我将这一行放在我的脚本的顶部以使其竞争条件安全:

if [[ -d "/tmp/${0//\//_}" ]] || ! mkdir "/tmp/${0//\//_}"; then echo "Script is already running!" && exit 1; fi; trap 'rmdir "/tmp/${0//\//_}"' EXIT;

因此,我不需要在我的进一步代码中考虑任何竞争条件。

分解代码:

  1. [[ -d "/tmp/${0//\//_}" ]] 检查锁定目录 /tmp/_path_to_script_scriptname.sh/ 是否存在。注:[=13=] contains the name of the script.
  2. mkdir "/tmp/${0//\//_}" 如果目录不存在则创建目录
  3. then ... exit 1 如果锁定目录已经存在则中止脚本(这意味着脚本已经 运行)
  4. 如果脚本退出,
  5. trap 'rmdir "/tmp/${0//\//_}"' EXIT 会自动删除锁定目录(这不会触发竞争条件,因为稍后定义了 trap 命令。

注意:在极少数情况下,例如服务器崩溃,锁定目录不会被删除。为此,您可以考虑一个检查过时锁目录的 cronjob。如果您的脚本中需要 trap(不能设置两次),请使用 one of the different multi trap solutions.