rm 可以用来同步锁文件检查吗?
Can rm be used to synchronize lock file checks?
我一直在尝试改进一段 shell 代码,实现了一个损坏的锁定机制。
我的想法是通过在一个文件上调用 rm 只让一个调用者完成同步。
PIDFILE=/tmp/test.pid
flag=$PIDFILE.flag
touch $flag
if [ -f $PIDFILE ]; then
ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi
echo $$ > $PIDFILE
# this should succeed only for one process
rm $flag || exit
echo $$ > $PIDFILE
我已经完成了几个并发调用并竭尽全力反对它并且没有 运行 失败。
但实际上安全吗?
不安全。
假设脚本的三个副本(A、B 和 C)同时启动并且 /tmp/test.pid
最初不存在。
让A和B完成脚本的初始语句:
PIDFILE=/tmp/test.pid
flag=$PIDFILE.flag
touch $flag
if [ -f $PIDFILE ]; then
ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi
切换到A,让它运行再声明两句:
echo $$ > $PIDFILE
rm $flag || exit
这就成功了; $PIDFILE
现在包含 A 的 PID。
切换到 B 并让它 运行 相同的语句。 rm
失败,因此 B 退出,但 $PIDFILE
现在包含 B 的 PID。
切换到 C。C 刚刚开始 运行ning,所以它做的第一件事就是重新创建 $flag
:
PIDFILE=/tmp/test.pid
flag=$PIDFILE.flag
touch $flag
现在进行 PID 检查:
if [ -f $PIDFILE ]; then
ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi
这通过了,因为 $PIDFILE
包含 B 的 PID,但 B 不再是 运行ning。
现在我们到了
echo $$ > $PIDFILE
rm $flag || exit
这也通过了,因为 C 刚刚重新创建了 $flag
文件。
现在我们有 A 和 C 运行ning,互相竞争以再次覆盖 $PIDFILE
。
除此之外还有一个"false positive"问题:
if [ -f $PIDFILE ]; then
ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi
您可能有一个过时的 $PIDFILE
,但它包含的 PID 已被另一个进程重用。在那种情况下,您不会获得比赛(以及您的脚本实例太多),而是拒绝服务(您的脚本实例太少:0)。您的脚本将看到恰好具有 "wrong" PID 的 运行ning 进程并退出。
你的代码不安全,但可能不是你想的那样。
这是一个可能的顺序,它可能允许一个进程通过检查,而另一个进程仍然 运行:
- 两个进程 A 和 B 启动 运行 脚本,并同时在第一个
echo $$ > $PIDFILE
之前的点结束。由于某种原因,进程 B 暂时停止在这里。
- 进程 A 将其 PID 写入 pid 文件,成功取消链接标志文件,将其 PID 再次写入 pid 文件,然后继续 运行。 =23=]
- 现在进程 B 恢复 运行。它将其 PID 写入 pidfile,覆盖它,然后无法取消链接标志文件并退出。进程 A 仍然是 运行,但现在 pid 文件不再包含它的 PID!
- 现在进程 C 出现了,重新创建标志文件,注意到 pid 文件存在但包含进程 B 的 PID(不再是 运行),因此愉快地通过了所有检查。
也存在导致相同结果的其他可能事件序列。
我一直在尝试改进一段 shell 代码,实现了一个损坏的锁定机制。
我的想法是通过在一个文件上调用 rm 只让一个调用者完成同步。
PIDFILE=/tmp/test.pid
flag=$PIDFILE.flag
touch $flag
if [ -f $PIDFILE ]; then
ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi
echo $$ > $PIDFILE
# this should succeed only for one process
rm $flag || exit
echo $$ > $PIDFILE
我已经完成了几个并发调用并竭尽全力反对它并且没有 运行 失败。
但实际上安全吗?
不安全。
假设脚本的三个副本(A、B 和 C)同时启动并且 /tmp/test.pid
最初不存在。
让A和B完成脚本的初始语句:
PIDFILE=/tmp/test.pid
flag=$PIDFILE.flag
touch $flag
if [ -f $PIDFILE ]; then
ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi
切换到A,让它运行再声明两句:
echo $$ > $PIDFILE
rm $flag || exit
这就成功了; $PIDFILE
现在包含 A 的 PID。
切换到 B 并让它 运行 相同的语句。 rm
失败,因此 B 退出,但 $PIDFILE
现在包含 B 的 PID。
切换到 C。C 刚刚开始 运行ning,所以它做的第一件事就是重新创建 $flag
:
PIDFILE=/tmp/test.pid
flag=$PIDFILE.flag
touch $flag
现在进行 PID 检查:
if [ -f $PIDFILE ]; then
ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi
这通过了,因为 $PIDFILE
包含 B 的 PID,但 B 不再是 运行ning。
现在我们到了
echo $$ > $PIDFILE
rm $flag || exit
这也通过了,因为 C 刚刚重新创建了 $flag
文件。
现在我们有 A 和 C 运行ning,互相竞争以再次覆盖 $PIDFILE
。
除此之外还有一个"false positive"问题:
if [ -f $PIDFILE ]; then
ps | grep -qE '^\s*'$(cat $PIDFILE) && exit
fi
您可能有一个过时的 $PIDFILE
,但它包含的 PID 已被另一个进程重用。在那种情况下,您不会获得比赛(以及您的脚本实例太多),而是拒绝服务(您的脚本实例太少:0)。您的脚本将看到恰好具有 "wrong" PID 的 运行ning 进程并退出。
你的代码不安全,但可能不是你想的那样。
这是一个可能的顺序,它可能允许一个进程通过检查,而另一个进程仍然 运行:
- 两个进程 A 和 B 启动 运行 脚本,并同时在第一个
echo $$ > $PIDFILE
之前的点结束。由于某种原因,进程 B 暂时停止在这里。 - 进程 A 将其 PID 写入 pid 文件,成功取消链接标志文件,将其 PID 再次写入 pid 文件,然后继续 运行。 =23=]
- 现在进程 B 恢复 运行。它将其 PID 写入 pidfile,覆盖它,然后无法取消链接标志文件并退出。进程 A 仍然是 运行,但现在 pid 文件不再包含它的 PID!
- 现在进程 C 出现了,重新创建标志文件,注意到 pid 文件存在但包含进程 B 的 PID(不再是 运行),因此愉快地通过了所有检查。
也存在导致相同结果的其他可能事件序列。