Effective Shell 第 6 部分:关于作业控制您需要知道的一切
关于作业控制您不需要知道的一切。

作业控制(Job control)是大多数 shell 都具备的一个功能,但通常使用起来并不那么直观。不过,了解一些基础知识可以帮助你避免陷入困境,并且偶尔会让某些任务变得简单一些。
在本章中,我们将探讨作业控制的主要功能、它可能带来的问题以及一些替代方案。
什么是作业控制?
让我们从一个例子开始。我正在构建一个简单的网页。它有一个 index.html 文件、一个 styles.css 文件和一个 code.js 文件。index.html 文件的内容如下
<head>
<title>My New Project</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<script src="code.js"></script>
</head>
<body>
<!-- Snip... -->
</body>
</html>
在浏览器中打开这个文件并不太行,因为它无法加载代码和样式。我们需要一个 Web 服务器来提供样式和代码。
在任何安装了 Python 的机器上,运行一个 Web 服务器的超级有用的单行命令是
python -m SimpleHTTPServer 3000
事实上,这个命令非常有用,以至于我通常会给它设置一个别名(alias),这样我只需输入 serve
即可。我们将在后面的章节中介绍别名。
现在,如果我们运行这个命令(如果你想自己尝试,可以在这里获取这三个示例文件),那么我们就可以在浏览器中打开网页,并且样式和代码都能成功加载

我们还可以看到服务器已经提供了 HTML、JavaScript 和 CSS 文件

到目前为止一切顺利。
问题
假设我们现在想继续使用我们的 shell,比如用 Vim 或 Emacs 这样的终端编辑器来编辑网站,或者我们想把网站打包成 zip 文件,或者只是运行任何 shell 命令[1]。
我们遇到了一个问题。python
进程仍在运行——它正在为网站提供服务。我们的 shell 基本上毫无用处,直到我们停止服务器。看看我尝试编辑文件时会发生什么

在上面的例子中,我尝试运行 vi
,但没有任何反应。标准输入既没有被服务器读取,也没有被 shell 解释。
我必须按 Ctrl+C 来终止服务器(这会发送一个 SIGINT
[2]信号——我们稍后会更多地了解信号),清除屏幕以去掉所有的错误信息,然后重新开始。
这显然不是最佳方案。让我们看看一些解决方案。
解决方案 1:在后台启动服务器
在大多数 shell 中,你可以运行一个命令并指示 shell 在后台运行它。要做到这一点,你需要在行尾加上一个 & 符号。下面是这个例子在这种情况下会是什么样子

通过在命令末尾加上 &
符号,我们指示 shell 将该命令作为后台作业运行。这意味着我们的 shell 仍然可用。shell 还通知我们,这个命令正在作为一个具有特定作业编号的后台作业运行
% python -m SimpleHTTPServer 3000 &
[1] 19372
用稍微晦涩的语言来说,shell 通知我们它在后台启动了一个作业,作业编号为 1
,并且这个作业当前正在处理 ID 为 19372
的进程。
使用 & 符号的解决方案是在日常工作中一种相当常见的模式。
解决方案 2:将服务器移至后台
假设你忘记在后台启动命令了。在这种情况下,你很可能会用 Ctrl+C
终止服务器,然后用 &
选项再次启动它。但是,如果这是一个大型文件下载或者一个你不想中止的任务呢?
在下面的例子中,我们会将作业移到后台

该进程当前在前台运行,所以我的 shell 处于非活动状态。按下 Ctrl+Z 会向该进程发送一个“暂停”(suspend)信号[3],暂停它并将其移到后台。
让我们来剖析一下这个过程
% python -m SimpleHTTPServer 3000
Serving HTTP on 0.0.0.0 port 3000 ...
127.0.0.1 - - [03/Jun/2019 13:38:45] "GET / HTTP/1.1" 200 -
^Z
[1] + 21268 suspended python -m SimpleHTTPServer 3000
shell 会回显我输入的内容,所以我们看到了 ^Z
(即我输入的 Ctrl+Z
组合键)。shell 的响应是将进程移动到一个后台作业并暂停它。
这里的关键是它被暂停了。进程被暂停了。所以 Web 服务器不再提供服务。如果你正在跟着示例操作,刷新你的浏览器。网页将无法加载,因为服务器进程无法响应请求。
要在后台继续(continue)这个作业,我们使用 bg
('background')命令,并带上一个作业标识符(它总是以 %
符号开头——我们很快就会看到原因)来告诉 shell 继续这个作业
% bg %1
[1] + 21268 continued python -m SimpleHTTPServer 3000
shell 告诉我们作业正在继续运行,如果我们再次加载网页,内容会如期显示。
作为最后的检查,我们运行 jobs
命令来看看 shell 正在运行哪些作业
% jobs
[1] + running python -m SimpleHTTPServer 3000
就这样——我们的服务器正在作为后台作业运行。这和我们在用 &
结尾启动服务器后运行 jobs
命令看到的结果完全一样。事实上,使用 &
也许是记住如何继续一个已暂停作业的更简单方法
% %1 &
[1] + 21268 continued python -m SimpleHTTPServer 3000
就像在命令末尾加上 &
会在后台运行它一样,在作业标识符末尾加上 &
也会在后台继续它。
至少还有一种方法可以将作业移到后台[4],但我还没发现在任何场景下它是有用的,而且解释起来过于复杂。如果你感兴趣,可以查看脚注了解详情。
将后台作业移至前台
如果你有一个后台作业,你可以用 fg
('foreground')命令将它带回到前台。让我们用 jobs
命令显示一下作业
% jobs
[1] + running python -m SimpleHTTPServer 3000
在这里,我有一个后台作业在运行服务器。以下任何一个命令都可以将它带回到前台
fg %1 # Explicitly bring Job 1 into the foreground
%1 # ...or in shorthand, just enter the job id...
fg # ...if not given an id, fg and bg assume the most recent job.
现在作业位于前台,你可以再次随心所欲地与该进程交互。
清理作业
你可能会意识到无法继续你正在做的事情,因为一个旧的作业仍在运行。这里有一个例子

我试图运行我的 Web 服务器,但还有一个作为后台作业在运行。服务器启动失败,因为端口已被占用。
为了清理它,我运行 jobs
命令来列出作业
% jobs
[1] + suspended python -m SimpleHTTPServer 3000
那是我的旧 Web 服务器。请注意,即使它被暂停了,它仍然会阻塞它所服务的端口[5]。进程被暂停了,但它仍然持有着它正在使用的所有资源。
现在我知道了作业标识符(在本例中是 %1
),我就可以终止这个作业了
% kill %1
[1] + 22843 terminated python -m SimpleHTTPServer 3000
这就是为什么作业标识符以百分号开头! 我使用的 kill
命令不是一个特殊的作业控制命令(比如 bg
或 fg
)。它是普通的 kill
命令,用来终止一个进程。但是支持作业控制的 shell 通常可以使用作业标识符来代替进程标识符。所以,我不需要找出需要终止的进程标识符是什么,我可以直接使用作业标识符[6]。
为什么你不应该使用作业
避免使用作业。它们交互起来不直观,并且存在一些严重的问题。
最明显的一个问题是,所有作业都写入同一个输出,这意味着你很快就会得到像这样的混乱输出

这就是当我运行一个每秒输出文本的作业时发生的情况。它在后台运行,但它把内容打印得到处都是,覆盖了我的命令。即使运行 jobs
命令试图找到并停止它也很困难。
输入问题甚至更复杂。如果一个作业在后台运行,但需要输入,它将被静默暂停。这可能会引起混淆。
作业可以在脚本中使用,但必须谨慎行事,并且如果它们留下了无法轻易清理的后台作业,很容易让脚本的使用者感到困惑[7]。
处理作业的错误和退出码可能会有问题,导致混淆、糟糕的错误处理或过于复杂的代码。
如何摆脱作业
如果有两件事值得记住,那就是这个
如果你已经开始在前台运行一个命令,不想停止它,而是想把它移到后台,请按
Ctrl+Z
。然后去 Google 搜索“作业控制”。
并且
如果你认为有作业在后台运行,并且它正在扰乱你的屏幕,输入
fg
将它带到前台,然后用Ctrl+C
终止它。根据需要重复操作!
在任何一种情况下,如果你需要做一些更精细的操作,你可以回到这个参考资料。但第一个命令应该能让你在想办法如何继续作业时重新获得对 shell 的控制,而第二个命令应该能终止一个扰乱你屏幕的后台作业。
作业的替代方案
如果你正在使用任何现代终端,如 iTerm、Terminal 或 GNOME Terminal,只需打开一个新标签页或分屏!这样容易得多。
这样做的好处是每个标签页都有自己的标准输入和输出,所以没有覆盖的风险。当然,你也可以随心所欲地隐藏/显示/重新排列标签页。
对于只想同时进行多项任务的操作员来说,传统的作业替代方案是终端多路复用器,例如 screen
或 tmux
多路复用器的工作方式与现代图形化终端非常相似——它们管理多个 shell 实例。与 iTerm 等现代终端相比,它的好处是拥有非常直观的图形用户界面和许多功能。
多路复用器的好处是,你可以在 SSH 会话中运行它们来管理远程机器上的复杂操作,并且它们采用客户端-服务器模型,这意味着许多人可以处理许多多路复用的进程(并且它们可以跨会话持续存在)。
我个人的偏好是两者都用——我使用现代终端,并且在其中运行 tmux
。我们将在后面的章节中探讨这两个选项。
快速参考
你可能会发现作业很有用,也可能发现它们并非如此。无论哪种方式,这里是一些常用命令的快速参考
命令 | 用法 |
command & |
将命令作为后台作业运行 |
<Ctrl+Z> |
将当前进程移入后台作业,并暂停 |
jobs |
列出所有作业 |
fg %1 |
将 1 号后台作业移至前台 |
bg %1 |
继续运行 1 号后台作业 |
kill %1 |
终止 1 号作业 |
wait %1 |
阻塞直到 1 号作业退出 |
如果你想了解更多关于作业的详细细节,最好的起点是 Bash 手册 - 作业控制部分,或者你偏好的 shell 手册中的“作业控制”部分。
希望你觉得这篇文章有用,并且,一如既往,请在下方留下评论、问题或建议!
脚注
-
如果你不是一个重度 shell 用户,这可能看起来不太可能。但如果你在 shell 中做大量工作,例如系统管理、开发运维,或者在终端中编码,这种情况会一直发生!↩︎
-
像
SIGINT
、SIGKILL
、SIGTERM
等信号将在后续章节中介绍。↩︎ -
技术上讲,是
SIGTSTP
- 即 'TTY stop'(TTY 停止)。如果你一直对 'TTY' 这个缩写感到好奇,请查看前一章,插曲:理解 Shell。↩︎ -
另一种方法是使用
Ctrl+Y
,它会发送一个延迟中断,这将使进程继续运行,直到它尝试从stdin
读取。此时,作业被暂停,控制权交还给 shell。操作员随后可以使用bg
、kill
或fg
将其移至后台、停止进程或根据需要保持在前台。请参阅:https://gnu.ac.cn/savannah-checkouts/gnu/bash/manual/bash.html#Job-Control ↩︎ -
另一个超级有用的代码片段:
lsof -i -P -n | grep 8000
用于查找任何打开了给定端口的进程。又一个可以加入别名章节的内容!↩︎ -
有时候这是必要的。如果一个作业运行多个进程——例如,通过运行一个管道——进程标识符会随着命令从管道的一个阶段移动到下一个阶段而改变。而作业标识符将保持不变。请记住,作业是一个 shell 命令,因此可能运行多个进程。↩︎
-
要看看这有多糟糕,可以创建一个启动作业的脚本,然后运行它。接着运行
jobs
命令看看有什么在运行。输出可能会让你大吃一惊!↩︎