Linux flock 防任务重叠:锁文件存在,不代表锁仍被占用
定时脚本每分钟启动一次,但一次处理偶尔超过一分钟,就会出现任务重叠。仅检查一个标记文件是否存在,容易留下过期状态,也有检查与创建之间的竞态。Linux 上可用 flock 让协作进程竞争同一把文件锁。本文在本地文件系统、Bash 5.2.37 和 util-linux 2.41.5 上执行示例。
AI生成概念图:持有唯一钥匙的任务进入执行区域,另一个任务在门外等待。仅作概念说明,不代表实际界面或实测数据。
先验证竞争,而不是只创建文件
下面先通过文件描述符九获取锁,再让另一次独立打开尝试非阻塞获取。第二次应返回专门选定的七十五,随后释放第一把锁,再次获取应成功。所有路径位于新建临时目录中;只在整个实验结束后清理目录,运行中不删除锁文件。
( set -eu work=$(mktemp -d) trap 'rm -rf -- "$work"' EXIT lock="$work/job.lock" exec 9>"$lock" flock -n 9 if flock -n -E 75 "$lock" true 9>&-; then printf 'unexpected lock acquisition\n' >&2 exit 1 else rc=$? test "$rc" -eq 75 printf 'second attempt: busy (%s)\n' "$rc" fi flock -u 9 exec 9>&- flock -n -E 75 "$lock" printf 'run after release\n' test -f "$lock" printf 'lock file still exists\n' )
输出依次为第二次尝试繁忙、释放后能够执行、锁文件仍存在。最后一条是重要证据:磁盘上的路径继续存在,锁却已经释放。文件存在与否不是持锁状态的查询接口,所以不要看到遗留锁文件就手动删除,也不要依靠它的修改时间判断任务还在不在。
非阻塞策略决定本轮如何处理
flock 默认竞争不到锁时会等待。参数 n 改为立即失败,适合“不排队,本轮跳过”的任务;参数 E 让锁冲突使用指定退出码。示例只把七十五认作预期竞争,其他错误仍导致失败。生产日志也应区分锁冲突、权限问题、程序退出和启动错误,不能把任何非零状态都写成“已有任务运行”。
若业务必须等待,可以选择带上限的等待时间,但需要同时考虑任务周期和积压。跳过一轮、排队执行和合并重复工作是三种业务策略,锁本身不会替你决定。对于会返回相同退出码的子程序,还应另外设计可辨认的日志或状态记录,避免只靠一个数字归因。
锁的生命周期跟打开资源有关
示例中的 exec 打开描述符,让外层 shell 在两条 flock 命令之间继续持有资源。第二次竞争前关闭它在子进程中的继承副本,使演示关系更清楚。真正使用时也要注意子进程和后台任务是否继承锁描述符;主脚本返回了,不代表所有副本已经关闭。
命令包装形式可以让锁覆盖一个前台命令的运行时间。若该命令又把工作放到后台,必须重新审视谁仍在持锁、锁何时释放。不要在尚未完成共享资源修改时提前解锁,也不要靠随意杀进程来清理状态。本文显式解锁只是为了在同一小实验里展示前后两次获取。
所有竞争者必须指向同一个对象
生产锁路径应放在任务可访问的受控目录,并被所有启动入口一致使用。运行中删除再重建同名文件,可能让新的进程锁住另一份文件对象,而旧进程仍持有原对象的锁,互斥约定就被破坏。示例使用随机目录是为了隔离实验,真实任务不能每次启动都换一条锁路径。
在常见本地文件系统上,这是一种需要各方主动配合的锁:绕过 flock 的程序仍可能修改同一份数据。网络文件系统上的支持、挂载选项和语义也需要单独验证,不应直接把本机结果推广成跨主机协调方案。分布式任务还可能需要租约、故障恢复与业务去重。
上线前至少验证两次同时启动、正常结束后再启动、工作程序失败,以及后台子进程的边界。互斥只解决并发进入,不能证明任务恰好执行一次,也不能回滚已经写出的部分结果。将锁竞争日志与业务完成标记分开,才能知道这一轮到底被跳过、失败了,还是已完成。


