Bash:等待超时
- 2024-11-07 08:55:00
- admin 原创
- 25
问题描述:
在 Bash 脚本中,我想执行如下操作:
app1 &
pidApp1=$!
app2 &
pidApp2=$!
timeout 60 wait $pidApp1 $pidApp2
kill -9 $pidApp1 $pidApp2
即在后台启动两个应用程序,并给它们 60 秒的时间来完成工作。然后,如果它们没有在这段时间内完成,就终止它们。
不幸的是,上面的代码不起作用,因为timeout
是可执行文件,而wait
是 shell 命令。我尝试将其更改为:
timeout 60 bash -c wait $pidApp1 $pidApp2
但这仍然不起作用,因为wait
只能在同一个 shell 内启动的 PID 上调用。
有什么想法吗?
解决方案 1:
您的示例和接受的答案都过于复杂,为什么您不只使用,timeout
因为这正是它的用例?该timeout
命令甚至有一个内置选项(-k
),如果命令在发送SIGKILL
初始信号SIGTERM
后仍在运行,则在发送初始信号后发送以终止命令(默认情况下)(请参阅)man timeout
。
如果脚本不一定需要wait
等待后恢复控制流,那么只需
timeout -k 60s 60s app1 &
timeout -k 60s 60s app2 &
# [...]
但是,如果确实如此,那么只需保存timeout
PID 即可轻松完成:
pids=()
timeout -k 60s 60s app1 &
pids+=($!)
timeout -k 60s 60s app2 &
pids+=($!)
wait "${pids[@]}"
# [...]
例如
$ cat t.sh
#!/bin/bash
echo "$(date +%H:%M:%S): start"
pids=()
timeout 10 bash -c 'sleep 5; echo "$(date +%H:%M:%S): job 1 terminated successfully"' &
pids+=($!)
timeout 2 bash -c 'sleep 5; echo "$(date +%H:%M:%S): job 2 terminated successfully"' &
pids+=($!)
wait "${pids[@]}"
echo "$(date +%H:%M:%S): done waiting. both jobs terminated on their own or via timeout; resuming script"
。
$ ./t.sh
08:59:42: start
08:59:47: job 1 terminated successfully
08:59:47: done waiting. both jobs terminated on their own or via timeout; resuming script
解决方案 2:
将 PID 写入文件并启动应用程序,如下所示:
pidFile=...
( app ; rm $pidFile ; ) &
pid=$!
echo $pid > $pidFile
( sleep 60 ; if [[ -e $pidFile ]]; then killChildrenOf $pid ; fi ; ) &
killerPid=$!
wait $pid
kill $killerPid
这将创建另一个进程,该进程会因超时而处于休眠状态,如果该进程尚未完成,则会终止该进程。
如果该进程完成得更快,则删除 PID 文件并终止杀手进程。
killChildrenOf
是一个获取所有进程并终止特定 PID 的所有子进程的脚本。请参阅此问题的答案,了解实现此功能的不同方法:终止所有子进程的最佳方法
如果您想脱离 BASH,可以将 PID 和超时写入目录并监视该目录。每隔一分钟左右,读取条目并检查哪些进程仍然存在以及它们是否已超时。
编辑如果您想知道该进程是否已成功终止,您可以使用kill -0 $pid
EDIT2或者您可以尝试进程组。kevinarpe说:要获取 PID(146322) 的 PGID :
ps -fjww -p 146322 | tail -n 1 | awk '{ print $4 }'
在我的情况下是:145974。然后可以使用 PGID 和特殊选项 kill 来终止组中的所有进程:kill -- -145974
解决方案 3:
以下是 Aaron Digulla 答案的简化版本,它使用了kill -0
Aaron Digulla 在评论中留下的技巧:
app &
pidApp=$!
( sleep 60 ; echo 'timeout'; kill $pidApp ) &
killerPid=$!
wait $pidApp
kill -0 $killerPid && kill $killerPid
就我而言,我希望既set -e -x
安全又返回状态代码,因此我使用了:
set -e -x
app &
pidApp=$!
( sleep 45 ; echo 'timeout'; kill $pidApp ) &
killerPid=$!
wait $pidApp
status=$?
(kill -0 $killerPid && kill $killerPid) || true
exit $status
退出状态 143 表示 SIGTERM,几乎肯定是由于我们的超时。
解决方案 4:
app1 &
app2 &
sleep 60 &
wait -n
解决方案 5:
出于我个人的观点,我们可以将 Teixeira 的解决方案归结为:
try_wait() {
# Usage: [PID]...
for ((i = 0; i < $#; i += 1)); do
kill -0 $@ && sleep 0.001 || return 0
done
return 1 # timeout or no PIDs
} &>/dev/null
Bashsleep
接受秒的小数部分,0.001s = 1 ms = 1 KHz = 足够的时间。但是,UNIX 在文件和进程方面没有漏洞。try_wait
完成得很少。
$ cat &
[1] 16574
$ try_wait %1 && echo 'exited' || echo 'timeout'
timeout
$ kill %1
$ try_wait %1 && echo 'exited' || echo 'timeout'
exited
我们必须回答一些难题才能取得进一步的进展。
为什么没有wait
超时参数?可能是因为、、和timeout
命令kill -0
可以更精确地告诉机器我们想要什么。wait
`wait -n`
为什么它wait
首先内置于 Bash,所以它timeout wait PID
不起作用?也许只是为了让 Bash 能够实现正确的信号处理。
考虑:
$ timeout 30s cat &
[1] 6680
$ jobs
[1]+ Running timeout 30s cat &
$ kill -0 %1 && echo 'running'
running
$ # now meditate a bit and then...
$ kill -0 %1 && echo 'running' || echo 'vanished'
bash: kill: (NNN) - No such process
vanished
无论是在物质世界还是在机器中,我们都需要一些地面来奔跑,我们也需要一些地面来等待。
当
kill
失败时,您几乎不知道原因。除非您编写了该过程,或者其手册中指明了具体情况,否则没有办法确定合理的超时值。try_wait
编写完该过程后,您可以实现适当的 TERM 处理程序,甚至可以响应通过命名管道发送给它的“Auf Wiedersehen!”。然后,您甚至可以使用类似:-) 的咒语
解决方案 6:
我编写了一个 bash 函数,它将等待直到 PID 完成或超时,如果超过超时则返回非零并打印所有未完成的 PID。
function wait_timeout {
local limit=${@:1:1}
local pids=${@:2}
local count=0
while true
do
local have_to_wait=false
for pid in ${pids}; do
if kill -0 ${pid} &>/dev/null; then
have_to_wait=true
else
pids=`echo ${pids} | sed -e "s/^${pid}$//g"`
fi
done
if ${have_to_wait} && (( $count < $limit )); then
count=$(( count + 1 ))
sleep 1
else
echo ${pids}
return 1
fi
done
return 0
}
要使用这个只是wait_timeout $timeout $PID1 $PID2 ...
解决方案 7:
您可以使用“读取”内部命令的超时。
下面的操作将终止未终止的作业并在最多 60 秒后显示已完成作业的名称:
( (job1; echo -n "job1 ")& (job2; echo -n "job2 ")&) | (read -t 60 -a jobarr; echo ${jobarr[*]} ${#jobarr[*]} )
它的工作原理是创建一个包含所有后台作业的子 shell。此子 shell 的输出被读入 bash 数组变量,可根据需要使用该变量(在此示例中,通过打印数组 + 元素计数)。
确保在与读取命令相同的子 shell 中引用 ${jobarr}(因此带有括号),否则 ${jobarr} 将为空。
读取命令终止后,所有子 shell 将自动静音(而不是被杀死)。您必须自行杀死它们。
解决方案 8:
又一次超时 狂欢的剧本
运行多个子进程,总超时。使用最近的狂欢功能,我写了这个:
#!/bin/bash
maxTime=5.0 jobs=() pids=() cnt=1 Started=${EPOCHREALTIME/.}
if [[ $1 == -m ]] ;then maxTime=$2; shift 2; fi
for cmd ;do # $cmd is unquoted in order to use strings as command + args
$cmd &
jobs[$!]=$cnt pids[cnt++]=$!
done
printf -v endTime %.6f $maxTime
endTime=$(( Started + 10#${endTime/.} ))
exec {pio}<> <(:) # Pseudo FD for "builtin sleep" by using "read -t"
while ((${#jobs[@]})) && (( ${EPOCHREALTIME/.} < endTime ));do
for cnt in ${jobs[@]};do
if ! jobs $cnt &>/dev/null;then
Elap=00000$(( ${EPOCHREALTIME/.} - Started ))
printf 'Job %d (%d) ended after %.4f secs.
' \n $cnt ${pids[cnt]} ${Elap::-6}.${Elap: -6}
unset jobs[${pids[cnt]}] pids[cnt]
fi
done
read -ru $pio -t .02 _
done
if ((${#jobs[@]})) ;then
Elap=00000$(( ${EPOCHREALTIME/.} - Started ))
for cnt in ${jobs[@]};do
printf 'Job %d (%d) killed after %.4f secs.
' \n $cnt ${pids[cnt]} ${Elap::-6}.${Elap: -6}
done
kill ${pids[@]}
fi
示例运行:
带参数的命令可以作为字符串提交
-m
开关让您选择一个浮点数作为以秒为单位的最大时间。
$ ./execTimeout.sh -m 2.3 "sleep 1" 'sleep 2' sleep {3,4} 'cat /dev/tty'
Job 1 (460668) ended after 1.0223 secs.
Job 2 (460669) ended after 2.0424 secs.
Job 3 (460670) killed after 2.3100 secs.
Job 4 (460671) killed after 2.3100 secs.
Job 5 (460672) killed after 2.3100 secs.
为了测试这一点,我编写了这个脚本
选择介于
1.0000
和9.9999
秒之间的随机持续时间输出
0
和之间的随机行数8
。(它们无法输出任何内容)。行输出包含进程 ID(
$$
)、剩余要打印的行数和总持续时间(秒)。
#!/bin/bash
tslp=$RANDOM lnes=${RANDOM: -1}
printf -v tslp %.6f ${tslp::1}.${tslp:1}
slp=00$((${tslp/.}/($lnes?$lnes:1)))
printf -v slp %.6f ${slp::-6}.${slp: -6}
# echo >&2 Slp $lnes x $slp == $tslp
exec {dummy}<> <(: -O)
while read -rt $slp -u $dummy; ((--lnes>0)); do
echo $$ $lnes $tslp
done
一次运行此脚本 5 次,超时时间为 5.0 秒:
$ ./execTimeout.sh -m 5.0 ./tstscript.sh{,,,,}
2869814 6 2.416700
2869815 5 3.645000
2869814 5 2.416700
2869814 4 2.416700
2869815 4 3.645000
2869814 3 2.416700
2869813 5 8.414000
2869812 1 3.408000
2869814 2 2.416700
2869815 3 3.645000
2869814 1 2.416700
2869815 2 3.645000
Job 3 (2869814) ended after 2.4511 secs.
2869813 4 8.414000
2869815 1 3.645000
Job 1 (2869812) ended after 3.4518 secs.
Job 4 (2869815) ended after 3.6757 secs.
2869813 3 8.414000
Job 2 (2869813) killed after 5.0159 secs.
Job 5 (2869816) killed after 5.0159 secs.
解决方案 9:
有些进程在超时调用时无法正常工作。我遇到了一个问题,需要在 qemu 实例周围放置一个超时捕获,如果你调用
timeout 900 qemu
它将永远挂起。
我的解决方案
./qemu_cmd &
qemuPid=$!
timeout 900 tail --pid=$qemuPid -f /dev/null
ret=$?
if [ "$ret" != "0" ]; then
allpids=()
descendent_pids $tracePid
for pids in ${allpids[@]};do
kill -9 $pids
done
fi
descendent_pids(){
allpids=("${allpids[@]}" $1)
pids=$(pgrep -P $1)
for pid in $pids; do
descendent_pids $pid
done
}
还要注意的是,超时并不总是会终止后代进程,这取决于您从超时中产生的 cmd 的复杂程度。
解决方案 10:
迟做总比不做好,解决方案是使用等待而不是轮询(虽然仍然是一个循环),但仍会尽快停止。
app1 &
pidApp1=$!
app2 &
pidApp2=$!
# timeout 60 wait $pidApp1 $pidApp2
declare -A pidApps=( [$pidApp1]=running [$pidApp2]=running )
{ sleep 60; echo "stop"; } | read &
pidTmout=$!
while [[ ${#pidApps[@]} -gt 0 ]]; do
wait -np pidStop
[[ $pidStop == $pidTmout ]] && break
unset pidApps[$pidStop]
done
[[ ${pidApps[$pidApp1]} == running ]] && kill -9 $pidApp1
[[ ${pidApps[$pidApp2]} == running ]] && kill -9 $pidApp2
解决方案 11:
使用wait -p
arg,您可以等待多个进程并确定哪个进程先完成。通过让其中一个进程超时,您可以知道是否发生了超时或成功。
这确实有点奇怪,但可以让你更明确地处理错误。如果你需要这种级别的控制,最好转向更完善的语言,比如 Python。
就像上面的答案所说的那样,timeout
对于 99% 的简单情况来说,使用可能就足够了。当需要一个更丰富的看门狗进程来观察心跳而不是简单的超时时,我想出了下面的解决方案的部分内容。不要把它看作是一个完整的替代品,更像是从中复制一些片段的东西。
#!/bin/bash
# Start 2 processes to wait for, for sake of example, use sleep and let one of them fail
remaining=()
sleep 1 && false &
remaining+=($!)
sleep 2 && true &
remaining+=($!)
# Start the watchdog process, for sake of example, use sleep as a fixed timeout
sleep 2 &
pid_timeout=$!
# Keep looping until no more processes left
while [ ${#remaining[@]} != 0 ]
do
echo "Waiting for remaining processes ${remaining[*]}"
wait -n -p firstFinished "${remaining[@]}" $pid_timeout
first_exit_code=$?
if [ $firstFinished = $pid_timeout ]
then
echo "Timeout, killing remaining processes ${remaining[*]}"
kill "${remaining[@]}"
exit 124
else
# Remove the finished process from remaining list
echo "Process $firstFinished finished with code $first_exit_code"
new_array=()
for rem_pid in "${remaining[@]}"
do
if [ "$rem_pid" = "$firstFinished" ]
then
echo "Process $rem_pid reported finished by wait, remove from list"
continue
fi
if ! kill -0 "$rem_pid"
then
echo "Process $rem_pid no longer running, removing from wait list"
# potential race condition when multiple processes finish before next iteration of loop
# TODO: How to get exit code?
continue
fi
new_array+=("$rem_pid")
done
remaining=("${new_array[@]}")
unset new_array
fi
done
kill $pid_timeout || true
echo "all done"
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件