只有在任务没有运行时才运行cron job。

178

我正在尝试设置一个cron job作为我创建的守护进程的看门狗。如果守护进程出错并失败,我希望cron job周期性地重新启动它...我不确定这是否可行,但我阅读了几篇cron教程,没有找到任何能做我所需功能的内容...

我的守护进程是从shell脚本启动的,所以我只是想找到一种方式,只有在上一个作业运行完毕后才能运行cron job。

我发现了这篇文章,它提供了使用锁文件来实现我想做的事情的解决方案,但我不确定是否有更好的方法来完成这项工作...

16个回答

184

使用flock。它是新的。它更好。

现在你不必亲自编写代码。在这里查看更多原因:https://serverfault.com/a/82863

/usr/bin/flock -n /tmp/my.lockfile /usr/local/bin/my_script

1
我还在这里创建了一个不错的cron模板gist:https://gist.github.com/jesslilly/315132a59f749c11b7c6 - Jess
3
setlocks6-setlockchpstrunlock在它们的非阻塞模式下是可用于不只是Linux的替代工具。 - JdeBP
flock只能与脚本一起使用,还是也可以与二进制文件(如ansible-pull)一起使用? - majorgear
1
@majorgear 它可以执行任何东西,但通常我看到人们运行脚本以便设置环境。 - Jess
谢谢。我已经设置了Ansible-pull,每10分钟运行一次,以保持我的服务器配置最新。我在命令前面添加了"flock -on /tmp/ansible-pull.lock",它可以防止重复实例运行。 - majorgear
显示剩余2条评论

129

我为自己编写的打印池程序这样做,它只是一个Shell脚本:

#!/bin/sh
if ps -ef | grep -v grep | grep doctype.php ; then
        exit 0
else
        /home/user/bin/doctype.php >> /home/user/bin/spooler.log &
        #mailing program
        /home/user/bin/simplemail.php "Print spooler was not running...  Restarted." 
        exit 0
fi

它每两分钟运行一次,非常有效。如果由于某种原因该过程未运行,我会通过电子邮件接收特殊信息。


4
虽然不是非常安全的解决方案,但如果在grep中匹配到其他进程会怎样呢?rsanden的答案使用pidfile可以避免这种问题。 - Elias Dorneles
17
这个轮子已经在别处被发明了 :) 例如,http://serverfault.com/a/82863/108394 - Filipe Correia
5
你可以使用 grep [d]octype.php 而不是 grep -v grep | grep doctype.php - AlexT
请注意,如果是 cron 运行脚本,则无需使用 & - lainatnavi
这是一个绝佳的解决方案。运行起来非常顺利,如果在程序名中包含完全限定路径,grep 就会进行唯一的搜索。一个小的改进可能是在最右边的 grep 上添加 --quiet 选项。 - undefined

65

正如其他人所说,编写和检查PID文件是一个不错的解决方案。这是我的bash实现:

#!/bin/bash

mkdir -p "$HOME/tmp"
PIDFILE="$HOME/tmp/myprogram.pid"

if [ -e "${PIDFILE}" ] && (ps -u $(whoami) -opid= |
                           grep -P "^\s*$(cat ${PIDFILE})$" &> /dev/null); then
  echo "Already running."
  exit 99
fi

/path/to/myprogram > $HOME/tmp/myprogram.log &

echo $! > "${PIDFILE}"
chmod 644 "${PIDFILE}"

3
使用 pidfile 可能比使用相同名称的进程进行搜索要安全得多。 - Elias Dorneles
1
脚本完成后,文件不应该被删除吗?或者我漏掉了什么非常明显的东西吗? - Hamzahfrq
@Hamzahfrq:你是指PID文件吗?我不认为删除它有任何好处,因为你不能依赖这种结果(你仍然必须考虑当它无论如何都不会被删除的边缘情况)。可预测的行为更好。 - rsanden
1
@matteo:是的,你说得对。我几年前就在我的笔记中修复了这个问题,但忘记在这里更新它。更糟糕的是,我在你的评论中也错过了它,只注意到了“>”和“>>`”。对此感到抱歉。 - rsanden
5
这是它的工作原理:脚本首先检查 PID 文件是否存在("[ -e "${PIDFILE}" ]")。如果不存在,则会在后台启动程序,将其 PID 写入文件("echo $! > "${PIDFILE}""),然后退出。如果 PID 文件存在,则脚本将检查您自己的进程("ps -u $(whoami) -opid=")并查看是否正在运行具有相同 PID 的进程("grep -P "^\s*$(cat ${PIDFILE})$"")。如果没有运行,则会像之前一样开始程序,用新的 PID 覆盖 PID 文件,然后退出。我认为没有修改脚本的必要,你觉得呢? - rsanden
显示剩余5条评论

58

令人惊讶的是没有人提到run-one。我用它解决了我的问题。

 apt-get install run-one

然后在您的crontab脚本之前添加run-one

*/20 * * * * * run-one python /script/to/run/awesome.py

请查看这个askubuntu的答案,你也可以在那里找到详细信息的链接。


4
需要翻译的内容:What's important to mention is that this tool is available out of the box in Ubuntu 20 (and maybe the versions prior as well)重要的是要提到,这个工具在Ubuntu 20(以及可能之前的版本)中可以直接使用。 - Adam Sibik
这真是非常有帮助。 - kokko1G
这里提供了适用于Debian、BSD和可能其他*nix的便携式重写版本:https://github.com/Freaky/run-one - korkman

28

不要尝试通过cron来执行此操作。让cron无论如何都运行一个脚本,然后由脚本决定程序是否正在运行,并在必要时启动它(请注意,您可以使用Ruby或Python或您喜欢的脚本语言来完成此操作)。


7
传统的方法是读取服务启动时创建的 PID 文件,检查该 PID 对应的进程是否仍在运行,如若未运行则重新启动。 - tvanfosson

9

当我运行php脚本时,我的做法是:

cron表:

* * * * * php /path/to/php/script.php &

PHP代码:

<?php
if (shell_exec('ps aux | grep ' . __FILE__ . ' | wc  -l') > 1) {
    exit('already running...');
}
// do stuff

这个命令在系统进程列表中搜索当前的php文件名,如果存在,行数计数器(wc -l)将大于1,因为搜索命令本身包含文件名。因此,如果你运行php crons,请将上述代码添加到你的php代码开头,它只会运行一次。

这正是我所需要的,因为所有其他解决方案都需要在客户端服务器上安装某些东西,而我无法访问。 - Jeff Davis

9

你也可以在crontab中直接使用一行命令:

* * * * * [ `ps -ef|grep -v grep|grep <command>` -eq 0 ] && <command>

5
不太安全,如果有其他命令与grep搜索匹配怎么办? - Elias Dorneles
1
这也可以写成
          • [ ps -ef|grep [c]ommand -eq 0 ] && <command>
在方括号中将命令的第一个字母括起来,可以将其排除在grep结果之外。
- Jim Clouse
我不得不使用以下语法:[ "$(ps -ef|grep [c]ommand|wc -l)" -eq 0 ] && <command> - thameera
3
这很丑陋。[ $(grep something | wc -l) -eq 0 ]是一个非常绕弯子的写法,其实可以用! grep -q something来代替。因此,你只需要使用ps -ef | grep '[c]ommand' || command就可以了。 - tripleee
(另外,如果你真的想要计算匹配行数,可以使用“grep -c”命令。) - tripleee

6
作为Earlz回答的后续,您需要一个包装脚本,在启动时创建一个$PID.running文件,并在结束时删除它。包装脚本调用您希望运行的脚本。包装器是必需的,以防目标脚本失败或出错,pid文件被删除。

哦,酷!我从没想过使用一个包装器(wrapper)……我无法通过使用锁文件找出一种方法来解决它,因为我不能保证如果守护进程出错时该文件将被删除……一个包装器会完美地解决问题,如果 jjclarkson 的解决方案不起作用,我会尝试这个方法。 - LorenVS

5

4
# one instance only (works unless your cmd has 'grep' in it)
ALREADY_RUNNING_EXIT_STATUS=0
bn=`basename $0`
proc=`ps -ef | grep -v grep | grep "$bn" | grep -v " $$ "`
[ $? -eq 0 ] && {
    pid=`echo $proc | awk '{print $2}'`
    echo "$bn already running with pid $pid"
    exit $ALREADY_RUNNING_EXIT_STATUS
}

使用flock的更好方法进行更新:

/usr/bin/flock -n /tmp/your-app.lock /path/your-app args 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接