Bash: 静默杀死后台函数进程

76

各位 Shell 高手,

我有一个 Bash shell 脚本,在其中启动了一个后台函数 foo(),用于显示进度条以展示枯燥而耗时的命令的执行进度:

foo()
{
    while [ 1 ]
    do
        #massively cool progress bar display code
        sleep 1
    done
}

foo &
foo_pid=$!

boring_and_long_command
kill $foo_pid >/dev/null 2>&1
sleep 10

现在,当foo死亡时,我会看到以下文本:

/home/user/script: line XXX: 30290 Killed                  foo

这完全破坏了我的进度条显示的壮观效果,否则就会非常酷。

我该如何摆脱这个消息?


16
使用“非常酷”这个词来形容一个bash脚本,我给你点赞。 - pepoluan
即使将kill foo_pid更改为kill $foo_pid,我仍无法重现此问题。 - Tanktalus
@Tanktalus,我认为这是因为脚本可能在输出发送到stderr之前就已经停止了。我在伪代码的末尾添加了一个sleep,这应该能让你重新创建这个问题。 - rouble
3
while [ 1 ]; do 可以写成 while :; do - TrueY
3
这应该与 https://dev59.com/znVD5IYBdhLWcg3wHnyd 合并,那个问题更加专注但缺少一些这里的答案。 - tripleee
据我所知,只有在使用“kill”命令以交互方式终止作业时才会出现问题。在脚本内部,类似于问题中显示的作业控制消息是不会打印的(除非您从交互提示符中_source_脚本)。 - mklement0
11个回答

83

1
这对我不起作用。终端仍然显示“被信号15杀死”。我正在尝试通过ssh进行此操作-我启动会话并启动进程,然后稍后再次ssh并杀死进程。当我尝试'wait $pid'时,它说该进程不是子进程(我认为是因为它是不同的会话),然后“被信号15杀死。”仍然显示在终端上。在这种情况下有没有办法抑制它? - David Doria
5
很好的建议;我建议使用{ kill $foo_pid && wait $foo_pid; } 2>/dev/null,这样也可以消除目标进程已经不存在的情况。 - mklement0
1
孩子在 kill 命令执行完之前就死亡的(远程)可能性不是存在吗?这样终止报告就会在那时生成了吧? - Davis Herring
感谢@mklement0 - 您还可以使用作业ID的kill && wait模式:{ kill %1 && wait %1; } 2>/dev/null - jaygooby
实际上,我尝试过这个,但只有kill $foo_pid 2>/dev/null对我有效。 - Alaska

37

我刚刚偶然发现了这个方法,并意识到"disown"就是我们需要的。

foo &
foo_pid=$!
disown

boring_and_long_command
kill $foo_pid
sleep 10

出现死亡信息是因为该进程仍在shell的“作业”监视列表中。使用“disown”命令可以将最近生成的进程从此列表中删除,这样即使使用SIGKILL(-9)杀死进程也不会生成调试消息。


1
非常好用。我同意 - 这是 Bash 的最佳解决方案。然而,disown 是 Bash 内置命令,在大多数其他 shell 中不可用。 - mattst
1
@mattst:确实值得指出的是,disown不符合POSIX标准;然而,在kshzsh中也可以使用它。 - mklement0
5
此外,似乎使用disown不仅仅是将当前shell与后台进程解除关联,还有其他含义:http://unix.stackexchange.com/a/148698/54804 - mklement0
@mklement0 感谢您提供的信息和有趣的链接。nohup 看起来确实是我目前使用 disown 的一些脚本的绝佳解决方案。对于最初的问题,毫无疑问 disown 是要使用的,我相信您也意识到了这一点。 - mattst
1
@mattst: 对于这个问题来说,disown 是可以的(如果终端突然死掉,后台作业将在下一次尝试写入标准输出时死亡),但是鉴于问题的通用标题,值得指出 disown 的影响不仅仅是静音后续的 kill - mklement0

7

尝试用以下行替换您的行kill $foo_pid >/dev/null 2>&1

(kill $foo_pid 2>&1) >/dev/null

更新:

这个答案并不正确,原因在于@mklement0在他的评论中解释了:

这个答案对于后台作业并不有效的原因是Bash本身异步执行,在kill命令完成后,会输出一个关于被杀死的作业状态消息,你无法直接抑制它 - 除非你使用wait,就像接受的答案一样。


1
在尝试终止不存在的进程以防止“kill foo_pid failed: no such process”消息时,帮助了我。 - Koen.
@Koen。是的,但这与后台作业无关。您可以使用2>/dev/null静音由kill本身发出的任何错误消息 - 与任何命令一样。这个答案之所以对_后台作业_无效,是因为Bash本身在kill命令完成后_异步_输出有关已杀死作业的状态消息,您无法直接抑制该消息 - 除非您使用wait,就像接受的答案一样。 - mklement0
@mklement0,我今天实际尝试了一下,发现只有kill $foo_pid 2>/dev/null可以消除错误信息。 - Alaska
@Alaska,据我所知,只有在使用“kill”命令以交互方式杀死作业时才会出现问题。在脚本内部,类似于问题中显示的作业控制消息不会打印(除非您从交互提示符中_source_脚本)。 (这与问题相矛盾)。换句话说:如果您从(未经源)_脚本_调用kill $foo_pid,则kill $foo_pid 2> / dev / null就足够了(以覆盖$ foo_pid不存在的情况)。在_交互式_(或在交互式脚本中源代码),您需要{kill $foo_pid && wait $foo_pid;} 2> / dev / null - mklement0

6

这是我为类似问题想出的解决方案(希望在长时间运行的过程中显示时间戳)。它实现了一个killsub函数,只要你知道pid,就可以安静地杀死任何子shell。请注意,陷阱指令非常重要:以防脚本被中断,子shell将不会继续运行。

foo()
{
    while [ 1 ]
    do
        #massively cool progress bar display code
        sleep 1
    done
}

#Kills the sub process quietly
function killsub() 
{

    kill -9 ${1} 2>/dev/null
    wait ${1} 2>/dev/null

}

foo &
foo_pid=$!

#Add a trap incase of unexpected interruptions
trap 'killsub ${foo_pid}; exit' INT TERM EXIT

boring_and_long_command

#Kill foo after finished
killsub ${foo_pid}

#Reset trap
trap - INT TERM EXIT

5
这种“hack”似乎可行:
# Some trickery to hide killed message
exec 3>&2          # 3 is now a copy of 2
exec 2> /dev/null  # 2 now points to /dev/null
kill $foo_pid >/dev/null 2>&1
sleep 1            # sleep to wait for process to die
exec 2>&3          # restore stderr to saved
exec 3>&-          # close saved version

这个灵感来源于这里。世界秩序已经恢复。


这个方法是可行的,但在 kill $foo_pid 后面加上 >/dev/null 2>&1 是没有必要的,因为 stderr(不需要的文本来自此处)已经被重定向到了 /dev/null。 - Lee Netherton

2
在函数开始处添加:
trap 'exit 0' TERM

这个在 macOS 上可行。我正在使用它来终止 tail:trap 'exit 0' TERM; (killall -m tail 2>&1) >/dev/null - Tomachi

1
你可以在之前使用set +m来抑制它。有关更多信息,请点击这里

1
另一种禁用作业通知的方法是将要放到后台的命令放在 sh -c 'cmd &' 结构中。
#!/bin/bash

foo()
{
   while [ 1 ]
   do
       sleep 1
   done
}

#foo &
#foo_pid=$!

export -f foo
foo_pid=`sh -c 'foo & echo ${!}' | head -1`

# if shell does not support exporting functions (export -f foo)
#arg1='foo() { while [ 1 ]; do sleep 1; done; }'
#foo_pid=`sh -c 'eval "$1"; foo & echo ${!}' _ "$arg1" | head -1`


sleep 3
echo kill ${foo_pid}
kill ${foo_pid}
sleep 3
exit

0

另一种方法是:

    func_terminate_service(){

      [[ "$(pidof ${1})" ]] && killall ${1}
      sleep 2
      [[ "$(pidof ${1})" ]] && kill -9 "$(pidof ${1})" 

    }

使用以下方式调用

    func_terminate_service "firefox"

0
错误信息应该来自于默认信号处理程序,该程序会在脚本中转储信号源。我只在 bash 3.x 和 4.x 上遇到过类似的错误。为了在任何地方始终安静地终止子进程(在 bash 3/4/5、dash、ash、zsh 上测试过),我们可以在子进程的最开始处陷阱 TERM 信号:
#!/bin/sh

## assume script name is test.sh

foo() {
  trap 'exit 0' TERM ## here is the key
  while true; do sleep 1; done
}

echo before child
ps aux | grep 'test\.s[h]\|slee[p]'

foo &
foo_pid=$!

sleep 1 # wait trap is done

echo before kill
ps aux | grep 'test\.s[h]\|slee[p]'

kill $foo_pid

sleep 1 # wait kill is done

echo after kill
ps aux | grep 'test\.s[h]\|slee[p]'


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