用 ssh 登录远程的 Linux 服务器,某些程序或脚本会在前台持续运行、占用 shell 窗口,且终端连接断开时任务也会中止。如何让运行命令提交后不受本地关闭终端窗口或网络断开连接的干扰呢?本文列举了一些实用的命令。

省流版

nohop <command> &

nohup

场景

如果只是临时有一个命令需要长时间运行,什么方法能最简便的保证它在后台稳定运行呢?

解决方法

我们知道,当用户注销 (logout) 或者网络断开时,终端会收到 HUP (hangup) 信号从而关闭其所有子进程。因此,我们的解决办法就有两种途径:要么让进程忽略 HUP 信号,要么让进程运行在新的会话里从而成为不属于此终端的子进程。

nohup 无疑是我们首先想到的办法。顾名思义,nohup 的用途就是让提交的命令忽略 hangup 信号。让我们先来看一下 nohup 的帮助信息:

1
2
3
4
5
6
7
8
9
10
11
NOHUP(1)                        User Commands                        NOHUP(1)
NAME
nohup - run a command immune to hangups, with output to a non-tty
SYNOPSIS
nohup COMMAND [ARG]...
nohup OPTION
DESCRIPTION
Run COMMAND, ignoring hangup signals.
--help display this help and exit
--version
output version information and exit

nohup 的使用只需在要处理的命令前加上 nohup 即可,标准输出和标准错误默认会被重定向到当前目录下的 nohup.out 文件中。此时,终端将不再打印输入输出,但终端依然被占用着。退出终端时,程序将继续运行。

一般我们可以在结尾加上 & 将命令放入后台运行,终端将不再被占用。

后台进程管理

jobs 管理作业

通过 jobs 命令可以看到由当前终端创建的后台作业的运行状况。编号为作业号(jobspec)。

  • bg %JOBSPEC 可以让后台 Stopped 的命令继续 Running

  • fg %JOBSPEC 可以让后台的作业来前台执行

  • Ctrl+Z 可以把前台执行的作业送进后台并挂起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
azure_root@UbuntuVM:~$ jobs
[1]- Running nohup ping www.ibm.com & (wd: ~/mirai-api-http/mcl-installer)
[2]+ Running nohup ping www.baidu.com &
azure_root@UbuntuVM:~$ fg %1
nohup ping www.ibm.com (wd: ~/mirai-api-http/mcl-installer)
^Cazure_root@UbuntuVM:~$ jobs
[2]+ Running nohup ping www.baidu.com &
azure_root@UbuntuVM:~$ fg %2
nohup ping www.baidu.com
^Z
[2]+ Stopped nohup ping www.baidu.com
azure_root@UbuntuVM:~$ bg %2
[2]+ nohup ping www.baidu.com &
azure_root@UbuntuVM:~$ jobs
[2]+ Running nohup ping www.baidu.com &

jobs 的缺点在于,一旦丢失 ssh 连接,将再也无法管理作业。

ps 搜索进程

Process Status 类似于 Windows 的任务管理器,可以显示系统的所有进程。编号为 PID。

1
2
3
4
5
6
7
azure_root@UbuntuVM:~$ nohup ping www.ibm.com &
[1] 1408536
azure_root@UbuntuVM:~$ nohup: ignoring input and appending output to 'nohup.out'
^C
azure_root@UbuntuVM:~$ ps -ef | grep 1408536
azure_r+ 1408536 1407733 0 19:01 pts/0 00:00:00 ping www.ibm.com
azure_r+ 1408544 1407733 0 19:01 pts/0 00:00:00 grep --color=auto 1408536

可以附加 grep -v grep 来忽略 grep 自身进程。

1
2
azure_root@UbuntuVM:~$ ps -ux | grep ping | grep -v grep
azure_r+ 1408536 0.0 0.2 8964 2700 pts/0 S 19:01 0:00 ping www.ibm.com

其中,ps -efps -ux 的区别在于输出风格和内容的不同,ps -uxps -aux 的区别在于进程所属用户不同。对我而言,ps -ux 获得的信息更直接。

killl 结束进程

1
kill -9 PID
1
kill %JOBSPEC

-9 是可选的,用于强制杀死进程。若要使用作业号,需要在 jobspec 前加上 %。纯数字会被认为是 PID。

1
2
3
4
5
6
7
azure_root@UbuntuVM:~$ kill %2
azure_root@UbuntuVM:~$ jobs
[2]+ Terminated nohup ping www.baidu.com
azure_root@UbuntuVM:~$ kill -9 %2
-bash: kill: %2: no such job
azure_root@UbuntuVM:~$ jobs
azure_root@UbuntuVM:~$

更改默认的 nohup.out

1
nohup python file.py > log.txt 2>&1 &

> 表示重定向标准输出(STDOUT),这里重定向到了当前目录下的 log.txt,不存在此文件时会自动创建。

若无 2>&1 则错误信息不会重定向。

setsid

nohup 无疑能通过忽略 HUP 信号来使我们的进程避免中途被中断。除此之外,如果我们的进程不属于接受 HUP 信号的终端的子进程,那么自然也就不会受到 HUP 信号的影响了。setsid 能帮助我们做到这一点。让我们先来看一下 setsid 的帮助信息:

1
2
3
4
5
6
7
SETSID(8)                 Linux Programmer’s Manual                 SETSID(8)
NAME
setsid - run a program in a new session
SYNOPSIS
setsid program [ arg ... ]
DESCRIPTION
setsid runs a program in a new session.

可见 setsid 的使用也是非常方便的,也是只需要在要处理的命令前加上 setsid 即可,不过 shell 依然会被占用,Ctrl+CCtrl+Z 也不会生效。

1
2
3
azure_root@UbuntuVM:~$ setsid ping www.google.com
azure_root@UbuntuVM:~$ ps -ef | grep www.google.com | grep -v grep
azure_r+ 1409166 1 0 19:43 ? 00:00:00 ping www.google.com

值得注意的是,此例中我们的 PID 为 31094,而它的 PPID(父 ID)为 1(即 init 进程 ID),并不是当前终端的进程 ID。这一点与 nohup 例不同。

&

这里还有一个关于 subshell 的小技巧。我们知道,将一个或多个命名包含在 () 中就能让这些命令在子 shell 中运行,从而扩展出很多有趣的功能,我们现在讨论的就是其中之一。

当我们将 & 也放入 () 内之后,我们就会发现所提交的作业并不在作业列表中,也就是说,是无法通过 jobs 来查看的,Ctrl+CCtrl+Z 同样不会生效。

1
2
3
azure_root@UbuntuVM:~$ (ping www.office.com &)
azure_root@UbuntuVM:~$ ps -ef | grep www.office.com | grep -v grep
azure_r+ 1409270 1 0 19:56 pts/0 00:00:00 ping www.office.com

disown

场景

我们已经知道,如果事先在命令前加上 nohup 或者 setsid 就可以避免 HUP 信号的影响。但是如果我们未加任何处理就已经提交了命令,该如何补救才能让它避免 HUP 信号的影响呢?

解决方法

此时再想用 nohup 或者 setsid 已经为时已晚,只能用作业调度和 disown 来解决了。让我们来看一下 disown 的帮助信息:

1
2
3
4
5
6
7
8
9
10
11
disown [-ar] [-h] [jobspec ...]
Without options, each jobspec is removed from the table of
active jobs. If the -h option is given, each jobspec is not
removed from the table, but is marked so that SIGHUP is not
sent to the job if the shell receives a SIGHUP. If no jobspec
is present, and neither the -a nor the -r option is supplied,
the current job is used. If no jobspec is supplied, the -a
option means to remove or mark all jobs; the -r option without
a jobspec argument restricts operation to running jobs. The
return value is 0 unless a jobspec does not specify a valid
job.
  • disown -h JOBSPEC 来使某个作业忽略 HUP 信号。
  • disown -ah 来使所有的作业都忽略 HUP 信号。
  • disown -rh 来使正在运行的作业忽略 HUP 信号。

需要注意的是,当使用过 disown 之后,会将把目标作业从作业列表中移除,我们将不能再使用 jobs 来查看它,但 ps 依然可以查找到它。

这种方法的操作对象是作业,如果需要通过 jobs 命令得到作业列表,在运行命令时应该在结尾添加 & 来使命令成为一个作业并在后台运行。

如果没有把命令作为作业来运行,我们需要按下 Ctrl+Z 将前台进程送入后台并挂起,然后才可以用 jobs 命令查询它的作业号,接着用 bg %JOBSPEC 让它继续运行。需要注意的是,如果挂起会影响当前进程的运行结果,请慎用此方法。