我目前正在重构一个脚本,如果直接在终端中执行,该脚本会运行良好,但如果从 crontab 执行,则会由于进程检查而提前退出。此提前终止是由使用 ps 命令通过管道传输到多个 grep/grep -v 命令的代码引起的。目的是检查进程是否已在运行,如果是,则不再执行此脚本。我知道此代码不起作用的原因是因为它试图捕获所有进程,但没有 grep -v 出crontab 始终用于最初调用脚本的进程。在重构此代码时,使用类似于通过管道传输到多个 grep 的/bin/sh -c <script name>
东西是有意义的。pgrep
ps
这就是我的问题所在。我的 pgrep 代码可以工作,但我并不完全理解它为什么可以工作。比较输出时,发现pgrep pgrep_test.sh | grep -v $$
pgrepps -ef | grep pgrep_test.sh
命令似乎删除了其他进程。在我看来,pgrep 将多个 PID 组合在一起,就像它理解并遵循 PID/PPID 关系一样。问题是我在 pgrep 手册页中没有看到任何关于此内容的内容。
我认为要理解为什么 pgrep 在我的代码中起作用,我需要更好地理解 pgrep 如何分组 PID/PPID。这是我用来测试的代码,它通过 crontab 执行:
* * * * * user1 /tmp/inferencing/pgrep_test.sh 2>&1 >> /tmp/inferencing/test.log
代码本身:
#!/bin/bash
# Test how grepping for PIDs works when script is called from crontab
echo "+++++$(date +"%b %H:%M:%S") Beginning pgrep script+++++"
pgrep pgrep_test.sh | grep -v "$$" > /dev/null 2>&1
# RC=1 -- No additional processes running
# RC=0 -- Additional processes running
RETCODE=$?
echo "Return code: $RETCODE"
if [ $RETCODE -eq 0 ]; then
echo "Additional test processes exist, exiting script"
echo "$(ps -ef | grep pgrep_test.sh)" ; sleep 1
echo "$(pgrep -a pgrep_test.sh | grep -v \"$$\")"
exit 1
else
echo "No additional processes found, continuing execution"
echo "$(ps -ef | grep pgrep_test.sh)" ; sleep 1
echo "$(pgrep -a pgrep_test.sh | grep -v \"$$\")"
sleep 90
fi
我在代码中使用了 90 秒的休眠时间,以确保每分钟运行一次的 cronjob 每隔一次都会失败。以下是日志文件的样子,其中包含一些以注释形式出现的附加注释。
首先,没有运行任何附加进程:
"+++++Nov 17:04:01 Beginning pgrep script+++++"
# Sleeping every 90s means we should have alternating "no additional
# processes found" and "additional processes found" logs each execution
Return code: 1
No additional processes found, continuing execution
# ps -ef | grep pgrep_test.sh
# Initial /bin/sh -c call crontab executes
user1 12956 12954 0 17:04 ? 00:00:00 /bin/sh -c /tmp/inferencing/pgrep_test.sh 2>&1 >> /tmp/inferencing/test.log
# Child process spawned from 12956 (Shouldn't this be PID for $$?)
user1 12957 12956 0 17:04 ? 00:00:00 /bin/bash /tmp/inferencing/pgrep_test.sh
# What even is PID 12961? PPID 12957 is the /bin/bash call but these two commands are identical otherwise
# Can't be the pgrep or sleep as these have not executed yet
# Technically there's more than 1 process here now so pgrep should be giving a return code of 0 and exiting the script
# Why does it work correctly here? How does pgrep know to group these?
user1 12961 12957 0 17:04 ? 00:00:00 /bin/bash /tmp/inferencing/pgrep_test.sh
user1 12963 12961 0 17:04 ? 00:00:00 grep pgrep_test.sh
# pgrep -a pgrep_test.sh | grep -v $$
12957 /bin/bash /tmp/inferencing/pgrep_test.sh
12965 /bin/bash /tmp/inferencing/pgrep_test.sh
接下来有一个匹配的进程已经在运行:
"+++++Nov 17:05:01 Beginning pgrep script+++++"
# Since other process is still sleeping, we correctly get a return code of 0 and stop script execution
Return code: 0
Additional test processes exist, exiting script
# crontab process for (now sleeping) original script call
user1 12956 12954 0 17:04 ? 00:00:00 /bin/sh -c /tmp/inferencing/pgrep_test.sh 2>&1 >> /tmp/inferencing/test.log
# Sleeping process
user1 12957 12956 0 17:04 ? 00:00:00 /bin/bash /tmp/inferencing/pgrep_test.sh
# New crontab process
user1 13733 13594 0 17:05 ? 00:00:00 /bin/sh -c /tmp/inferencing/pgrep_test.sh 2>&1 >> /tmp/inferencing/test.log
# New main bash process
user1 13734 13733 0 17:05 ? 00:00:00 /bin/bash /tmp/inferencing/pgrep_test.sh
# Second main bash process again -- this happens every time
user1 13738 13734 0 17:05 ? 00:00:00 /bin/bash /tmp/inferencing/pgrep_test.sh
# No grep -v grep, this doesn't show up to pgrep anyways
user1 13740 13738 0 17:05 ? 00:00:00 grep pgrep_test.sh
# pgrep -a pgrep_test.sh | grep -v $$
12957 /bin/bash /tmp/inferencing/pgrep_test.sh
13734 /bin/bash /tmp/inferencing/pgrep_test.sh
14105 /bin/bash /tmp/inferencing/pgrep_test.sh
为什么当我执行命令时pgrep
,它似乎知道如何过滤掉与 $$ 相关的附加子进程,而通过管道传输到 greps 的 ps -ef 却无法做到这一点?
答案1
此提前终止是由代码使用 ps 命令通过管道传输到多个 grep/grep -v 命令引起的。目的是检查进程是否已在运行,如果是,则不再执行此脚本。
不要这样做。这是一种糟糕的方法,如果您甚至懒得检查“grep”是否与输入完全匹配,而不仅仅是子字符串,那就更糟糕了。例如,如果您保持打开vim pgrep_test.sh
状态,您的脚本将认为它已在运行。
有更好的方法来制作单实例脚本:
将脚本作为 systemd .service 运行(通过让你的 cronjob 调用“systemctl start”或者通过使用 systemd .timer 来调用它),因为同一个服务不能启动两次;
[Service] Type=oneshot User=user1 ExecStart=/tmp/inferencing/pgrep_test.sh
或者通过 使用锁文件
flock
,它使用基于内核的排他锁来保证单个实例。* * * * * user1 flock -n /tmp/inferencing/lock /tmp/inferencing/pgrep_test.sh
PID 12961 到底是什么?PPID 12957 是 /bin/bash 调用,但这两个命令在其他方面是相同的
它是处理 中的命令的“子 shell” $( ... )
。每次使用命令替换时,bash 都会生成一个子进程来处理它。如果替换的是简单命令,该子 shell 进程可能会直接就地“执行”该命令(例如 的情况$(ps -ef)
),但如果替换的是整个管道,则不一定会发生这种情况。
虽然$$
总是扩展到主 shell 进程的 PID(即,当 bash 生成子 shell 时,它的值被克隆),但你可以使用$BASHPID
它来获取真实的当前解释器的进程 ID。例如:
$ echo $$, $BASHPID; ps $$ $BASHPID
208231, 208231
PID TTY STAT TIME COMMAND
208231 pts/3 Ss 0:00 bash
$ (echo $$, $BASHPID; ps $$ $BASHPID)
208231, 208287
PID TTY STAT TIME COMMAND
208231 pts/3 Ss 0:00 bash
208287 pts/3 S+ 0:00 bash
$ { echo $$, $BASHPID; ps $$ $BASHPID; }
208231, 208231
PID TTY STAT TIME COMMAND
208231 pts/3 Ss 0:00 bash
$ var=$(echo $$, $BASHPID; ps $$ $BASHPID); echo "$var"
208231, 208294
PID TTY STAT TIME COMMAND
208231 pts/3 Ss+ 0:00 bash
208294 pts/3 R+ 0:00 ps 208231 208294
第 2 和第 4 个示例使用了子 shell(检测这一点的另一种简单方法是注意在子 shell 中设置的变量不会传播回主 shell),而第 1 和第 3 个示例则没有。