linux 在终端打开程序后关闭终端,程序也跟着关闭了怎么办?

本文由 简悦 SimpRead 转码, 原文地址 www.zhihu.com https://pic2.zhimg.com/c3d70d48f_xs.jpg?source=1940ef5c程序猎人​

现有很多答案都说的不错,不过有一个共同点就是都是告诉你一开始你就打算后台运行的时候怎么做。

作为一个经常掉链子的蠢人,我是来给你送后悔药的。

当你一开始没打算在后台长期执行一个命令,但是运行了之后发现花很多时间,但又不想在那里傻等,怎么办?(这种事通常发生在下班前 2 小时执行了一个命令,然后跑去撩漂亮的前台姑娘,快下班回来一看还没完,一打听才发现这货居然要执行超过 8 小时…… 知识都是在惨痛的教训中获得的!)

首先,你要是还想要程序的输出,那你要么继续傻等,要么停掉现在执行的进程,按照其他答案说的,用 nohup 或者用 tmux/screen 一类的工具重新来过。

如果程序的输出无所谓,你只要程序能继续执行下去就好——典型的例子是你压缩一个文件或者编译一个大软件,中途发现需要花很长时间——那你接着看我给你的秘方。

  1. 按下 Ctrl-z,让程序进入 suspend(这个词中文是——挂起?)状态。
  2. 这个时候你应该会看到 [1]+ Stopped xxxx 这样的输出。
  3. 上面那个 [] 里的数字,我们记为 n,然后执行 bg %n ,让暂时停掉的进程在后台运行起来。执行之前,如果不放心,想确认一下,可以用 jobs 命令看看被 suspend 的任务列表(严格地说,jobs 看的不仅仅是 suspend 的任务,所以,如果你有其他后台任务,也会列出来。)
  4. 然后再执行 disown ,解除你现在的 shell 跟刚才这个进程的所属关系。这个时候再执行 jobs,就看不到那个进程了。
  5. 现在就可以关掉终端,下班回家了。

下面是一个实例,你可以看看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
bash-3.2$ sleep 3600                                            # 要执行很久的命令
^Z
[1]+  Stopped                 sleep 3600
bash-3.2$ jobs
[1]+  Stopped                 sleep 3600
bash-3.2$ bg %1
[1]+ sleep 3600 &
bash-3.2$ jobs
[1]+  Running                 sleep 3600 &
bash-3.2$ disown
bash-3.2$ jobs
bash-3.2$ ps -ef | grep sleep                                    # 此处输出可知,那个命令还在执行
  501 30787 30419   0  6:00PM ttys000    0:00.00 sleep 3600
  501 33681 30419   0  6:02PM ttys000    0:00.00 grep sleep
bash-3.2$ exit

此处重启一个终端窗口

1
2
3
4
bash-3.2$ ps -ef|grep sleep
  501 30787     1   0  6:00PM ??         0:00.00 sleep 3600
  501 36701 36592   0  6:05PM ttys001    0:00.00 grep sleep
bash-3.2$

可以看到刚才的命令还在执行。

这个方法,可以让你在后知后觉发现一个命令要执行很久的时候,也可以半路让它改成后台执行。

还有一个好处是,对于一些初期有些需要输入交互的程序,用这个方法在后期长期执行的时候转入后台,可以免去一开始就后台执行导致无法输入信息的烦恼。至于怎么在后台交互(尤其是 sudo 之类需要输入密码的),那就是另一个问题了…… 不过,一定要注意,要确定所有交互都结束了再转入后台,不然那个进程会停在等待输入的地方直到被人杀掉或者系统关闭……

但是,这个方法的缺点就是无法获取程序输出了。所以,如果你用 bash 或者类似兼容的 shell,在执行命令后面加一个 |& tee program.log 是个好习惯。随时把标准和错误输出导入到日志文件中,一方面可以防翻页太快缓冲太少,一方面可以半路后悔切后台,还有一个终极作用是可以留下证据防坏人。

哦,对了,提醒一下。如果你不是连的远程服务器,而是本地计算机。那么关了终端窗口,但不要关机,关机了,你的程序还是会停的……(感觉我在说废话…… 不过,做过各种错事之后,你会发现有些废话还是很重要的。)也不要休眠,休眠了程序就暂停了…… 虽然不至于彻底灰飞烟灭。


重新看了一眼其他答案,我来补充一下 nohup 的正确使用方法。(发现现有的某些答案很是蔫损坏……)

1
nohup COMMAND > stdout.log 2> stderr.log &

上面这种,将会把 COMMAND 命令的标准输出输出到 stdout.log 中,错误输出输出到 stderr.log 中。

1
nohup COMMAND > output.log 2>&1 &

这个是把标准输出和错误输出都一股脑地输出到 output.log 文件中。

推荐上面两种写法,自己指定输出文件。如果你确实很懒,那就直接

1
nohup COMMAND &

这个时候,nohup 命令会默认把标准输出写入 nohup.out 文件,文件在你执行命令的路径下,也就是 Working Directory。至于错误信息…… 忘了输出到哪里了,好像是 nohup.err 还是 nohup.error 的…… 因为不常用,不记得了。

之所以不推荐默认,是因为会出现预期外的文件,另外,你要是执行好几个,输出文件里就是一锅粥了……

ps. nohup 是 no hang up 的缩写。hang up 就是当你关终端的时候,会发给进程的信号名称(通常记作 SIGHUP)。no hang up 就是不要发这个信号的意思。

pps. 某答案里的那个 /dev/null 是 Linux 里的黑洞,啥玩意儿扔进去都没了。所以,你不想要输出的时候,可以用。

至于 tmux 和 screen,这两个东西的使用方法需要一本手册,所以有兴趣的人自己去查吧。(其实是我自己也没记全…… 就记得了一个 Ctrl-B 了……)


另外,如果你拥有那台服务器的全权管理权,记得应该有个什么设置,让终端退出的时候可以不发 HUP 信号。我在一台服务器上见过这种现象,不需要输入 disown ,或者不用 nohup,直接后台就可以退出不死(前台程序还是会死的)。但我不知道是怎么设置的,等我哪天有兴趣查明白了再回来补充吧…… 或者哪位高人评论补充一下。

这个问题我查明白了,回来更新一波。

首先,这个是基于 bash 来说的,其他 shell 可能情况不同,目前我也只查了 bash 的情况。

bash 里有一个选项,叫 huponexit 如果这个选项设置成 off ,当且仅当正常退出 shell 的时候(输入 exit 命令退出或者以 Ctrl-d 退出)不会像后台进程发送 SIGHUP(hang up),进程会得到保留继续执行。如果是异常掉线,强行关闭终端窗口,则会发送 SIGHUP,导致后台进程被杀。

貌似默认的 bash 是 huponexit=off 的,所以有些答案告诉只要在命令后面加 & 就可以了。就是基于此的。然而,无法防止意外掉线,尤其是网络不稳定的地方,这个事儿很不靠谱。因此,上面的答案还是比较推荐的做法。

再说说这个选项如何查看和设置。

1
2
$ shopt huponexit
huponexit       off

上面这个命令可以查看当前设置,第二行是结果。目前是 off。

1
2
3
$ shopt -s huponexit
$ shopt huponexit
huponexit       on

上面这段是把这个选项开启,设置为 on,并查看确认。

1
2
3
$ shopt -u huponexit
$ shopt huponexit
huponexit       off

这段则是把这个选项关闭,设置为 off,并查看确认。

参考链接:

How bash handles the jobs when logout?

What happens to background jobs after exiting the shell?

In which cases is SIGHUP not sent to a job when you log out?


又想起一个可以完成这个任务的方法,更新补充一下。虽然这个不是专门干这个用的。

可以用 at 命令。at 命令原本是用来定时执行任务的,但可以使用 at now 来让任务立刻执行,这个时候执行的进程也是在后台而不依赖于当前 shell。

具体写法如下:

1
2
3
4
5
6
7
8
$ at now
at> sleep 300
at> <EOT>   # ← 此处按下Ctrl-d结束输入
job 625 at Tue Feb  2 10:24:00 2021
acros800@CY-UATBAT:~
$ ps -ef|grep sleep
acros800 23305 23304  0 10:24 ?        00:00:00 sleep 300
acros800 23311 23218  0 10:24 pts/0    00:00:00 grep --color=auto sleep

任务执行后,会将执行时的输出以邮件的形式发到执行者的服务期内邮箱中,可以用 mail 命令查看。

缺点是,执行的时候的环境可能跟 shell 下直接执行会有所不同(具体它加载的是什么环境,记不清了…… 可以用 atq 来查看在执行或预订执行的列表,然后用 at -c JOB_ID 来查看具体环境配置及命令)。如果执行的是 bash 的 shell script,可以使用 bash -l SCRIPT_FILE 来带上登录时加载的环境项。


忽然发现被推荐为优质内容了,是不是应该加个广告?

比如……

跟老韩学 Linux 运维京东¥168.00去购买​鸟哥的 Linux 私房菜 基础学习篇 第四版京东¥87.80去购买​学习 bash:Learning the bash Shell, Second Edition京东¥145.63去购买​Bash Cookbook 中文版 9787115527011京东¥46.02去购买​

不过,我都没看过,好不好用,大家自行甄别。实在人不打诳语。

话说回来,开卷有益。

https://pic1.zhimg.com/310d85e8d_xs.jpg?source=1940ef5cpansz​​

个人建议是:永远养成进终端就开 screen 的习惯。或者干脆直接以 screen 作为终端。

当然,tmux 之类也是可以的,只不过 screen 对我来说能用,够用,后来就懒得切换到 tmux 了。

至于 nohup & 之类的后台手段,总归有诸多限制。最大问题自然就是输入输出不可交互。


为什么会有这个回答呢?因为有朋友表示 screen 的使用需要一本手册。。。

那么,请让我告诉你最简单的 screen 使用手册吧,一共,只有两项

进入 screen:

1
screen -xRR

(上面的意思是:如果后台有一个现有的 screen,则连上去,否则创建一个新的。)

退出 screen:

1
Ctrl-A, d

(上面的意思是:按 Ctrl-A,抬手,然后按 d 键。功能是:退出 screen 并将当前 screen 放到后台。)


没有了,常规的使用只需要这两个就够了。反正你的目的只是为了将应用放在后台嘛。请读到这里的读者,马上测试一下上面两个用法的实际效果。


进阶的使用大致需要掌握 Ctrl-A, a (创建新标签),Ctrl-A, 数字(切换到指定标签)以及 screenrc 的撰写(设计标签栏的显示样式)这三项也就能满足基本使用,这是在一个 screen 内部使用多标签页的办法。

真的,不需要一本书,只需要掌握上面几个用法,就足够了。

https://pic1.zhimg.com/906d6f684592279a066a676fcaa2af54_xs.jpg?source=1940ef5c妄想江南

恕我直言,所有诸如 nohup 之类的本质上以「让程序收不到 sighup」为手段的方法都是凑合用用的 workaround,权宜之计而已,并没有从根源上优美地解决问题。

以 bash 为例,之所以关闭终端后程序会跟着关闭,是因为你关闭终端窗口时,虚拟终端进程给 bash 进程发了一个 sighup,而根据 bash 文档

Before exiting, an interactive shell resends theSIGHUPto all jobs, running or stopped. Stopped jobs are sentSIGCONTto ensure that they receive theSIGHUP.

bash 给自己的子进程(即你运行的程序)转发了这个 sighup,你的程序收到 sighup 未捕获,就自动退了。

那么这是谁的错呢?

是你的程序的错吗?当然不是。你的程序收到 sighup 本来就应该退出。

是 bash 的错吗?当然也不是,bash 之所以给你的程序发 sighup 并不是故意想害你的程序,而是因为 bash 自己收到了 sighup,人家有义务转发给自己的衍生进程。

那是谁的错呢?当然是你的错。你明明只是想正常退出 bash,却非要直接粗暴地关窗口,害得 bash 收到一个 sighup。

何谓「优美」?优美就是「语义与实现一致」。

既然你在语义上明明是「事情干完了想让 bash 寿终正寝」,为什么实现上非要「发个中断让 bash 中道崩殂」呢?sighup 的意思是「signal of hanging up」,既然你在语义上并没有想「hang up the connection between the terminal and bash」,那你为什么在实现上非要发一个「signal of hanging up」呢?

所以正确的做法是用 & 运行你的程序,然后用 Ctrl+D 退出终端,让 bash 读自己的 stdin 读到一个 EOT,意识到事儿干完了于是自己退了。这才是真正的寿终正寝。

整个问题的错误根源在于「不该让 bash 收到 sighup」,而不在于「bash 不该转发 sighup」,更不在于「不该让你的程序收到 sighup」。明明是用户不该给 bash 发 sighup,却不检讨自己反而怪 bash 把 sighup 转发给程序。是何异于自己太丑不检讨自己反而怪女人肤浅。