【问题标题】:How to avoid race conditions in a bash script?如何避免 bash 脚本中的竞争条件?
【发布时间】:2022-01-14 17:03:48
【问题描述】:
#!/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。我研究过在多个网站上,但没有做出巨大的功能就没有简洁的答案。

【问题讨论】:

  • 你为什么要同时运行这个脚本两次?为什么不使用安全的临时文件进行存储?
  • util-linux使用flock
  • @EtanReisner 它是一个所以我们知道存在竞争条件并且需要避免它们
  • 那么你试图阻止你的脚本同时运行两次?
  • 没有我试图确保它们都可以运行,防止出现竞争条件

标签: bash race-condition


【解决方案1】:

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

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

#!/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 个实例在运行时,存在一个不确定的极端情况。)

#!/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=$0 } END { print 1+i }' numbers

【讨论】:

  • 第一个例子似乎需要touch numberswhile true 循环之前。如果数字不存在,ln 命令将失败,因此如果它不存在,则该代码永远无法创建它
  • 我认为使用ln -s 代替普通的ln 也应该适用于第一个示例中的第三行代码——然后如果numbers.lock 存在,该命令将失败,但愉快地进行即使numbers 尚不存在也开启
  • 这样做有什么意义?操作的目的是如果出现问题则失败。但是,是的,touch 不见了,感谢您注意到这一点。
  • 但是缺少的numbers 文件在您的代码中不会被视为失败——您可以显式检查它的存在并在必要时在if [ ! -f numbers ] 行中创建它。如果您保持ln 原样(不是ln -s),那么存在性检查完全没有意义,因为它已经包含在if ! ln ... 中。此外,rm numbers.lock 应该在循环之外 (while [[ $count != 100 ]]),否则您将重新引入竞争条件(并向终端生成 99 条错误消息)。
  • 你是对的......我不确定我当时在想什么;我根本无法让第一个工作。
【解决方案2】:

我将这个单行代码放在脚本的顶部,以确保竞争条件安全:

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/ 是否存在。注意:$0contains the name of the script
  2. mkdir "/tmp/${0//\//_}" 如果目录不存在则创建目录
  3. then ... exit 1 abort script if lock dir 已经存在(这意味着脚本已经在运行)
  4. 如果脚本退出,trap 'rmdir "/tmp/${0//\//_}"' EXIT 会自动删除锁定目录(由于稍后定义了 trap 命令,因此不会遇到竞争条件。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多