65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2019年6月10日

CPOL

11分钟阅读

viewsIcon

4040

关于作业控制您不需要知道的一切。

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

作业控制(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 即可。我们将在后面的章节中介绍别名。

现在,如果我们运行这个命令(如果你想自己尝试,可以在这里获取这三个示例文件),那么我们就可以在浏览器中打开网页,并且样式和代码都能成功加载

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

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

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

到目前为止一切顺利。

问题

假设我们现在想继续使用我们的 shell,比如用 Vim 或 Emacs 这样的终端编辑器来编辑网站,或者我们想把网站打包成 zip 文件,或者只是运行任何 shell 命令[1]

我们遇到了一个问题。python 进程仍在运行——它正在为网站提供服务。我们的 shell 基本上毫无用处,直到我们停止服务器。看看我尝试编辑文件时会发生什么

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

在上面的例子中,我尝试运行 vi,但没有任何反应。标准输入既没有被服务器读取,也没有被 shell 解释。

我必须按 Ctrl+C 来终止服务器(这会发送一个 SIGINT[2]信号——我们稍后会更多地了解信号),清除屏幕以去掉所有的错误信息,然后重新开始。

这显然不是最佳方案。让我们看看一些解决方案。

解决方案 1:在后台启动服务器

在大多数 shell 中,你可以运行一个命令并指示 shell 在后台运行它。要做到这一点,你需要在行尾加上一个 & 符号。下面是这个例子在这种情况下会是什么样子

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

通过在命令末尾加上 & 符号,我们指示 shell 将该命令作为后台作业运行。这意味着我们的 shell 仍然可用。shell 还通知我们,这个命令正在作为一个具有特定作业编号的后台作业运行

% python -m SimpleHTTPServer 3000 &
[1] 19372

用稍微晦涩的语言来说,shell 通知我们它在后台启动了一个作业,作业编号为 1,并且这个作业当前正在处理 ID 为 19372 的进程。

使用 & 符号的解决方案是在日常工作中一种相当常见的模式。

解决方案 2:将服务器移至后台

假设你忘记在后台启动命令了。在这种情况下,你很可能会用 Ctrl+C 终止服务器,然后用 & 选项再次启动它。但是,如果这是一个大型文件下载或者一个你不想中止的任务呢?

在下面的例子中,我们会将作业移到后台

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

该进程当前在前台运行,所以我的 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.

现在作业位于前台,你可以再次随心所欲地与该进程交互。

清理作业

你可能会意识到无法继续你正在做的事情,因为一个旧的作业仍在运行。这里有一个例子

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

我试图运行我的 Web 服务器,但还有一个作为后台作业在运行。服务器启动失败,因为端口已被占用。

为了清理它,我运行 jobs 命令来列出作业

% jobs
[1]  + suspended  python -m SimpleHTTPServer 3000

那是我的旧 Web 服务器。请注意,即使它被暂停了,它仍然会阻塞它所服务的端口[5]。进程被暂停了,但它仍然持有着它正在使用的所有资源。

现在我知道了作业标识符(在本例中是 %1),我就可以终止这个作业了

% kill %1
[1]  + 22843 terminated  python -m SimpleHTTPServer 3000

这就是为什么作业标识符以百分号开头! 我使用的 kill 命令不是一个特殊的作业控制命令(比如 bgfg)。它是普通的 kill 命令,用来终止一个进程。但是支持作业控制的 shell 通常可以使用作业标识符来代替进程标识符。所以,我不需要找出需要终止的进程标识符是什么,我可以直接使用作业标识符[6]

为什么你不应该使用作业

避免使用作业。它们交互起来不直观,并且存在一些严重的问题。

最明显的一个问题是,所有作业都写入同一个输出,这意味着你很快就会得到像这样的混乱输出

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

这就是当我运行一个每秒输出文本的作业时发生的情况。它在后台运行,但它把内容打印得到处都是,覆盖了我的命令。即使运行 jobs 命令试图找到并停止它也很困难。

输入问题甚至更复杂。如果一个作业在后台运行,但需要输入,它将被静默暂停。这可能会引起混淆。

作业可以在脚本中使用,但必须谨慎行事,并且如果它们留下了无法轻易清理的后台作业,很容易让脚本的使用者感到困惑[7]

处理作业的错误和退出码可能会有问题,导致混淆、糟糕的错误处理或过于复杂的代码。

如何摆脱作业

如果有两件事值得记住,那就是这个

如果你已经开始在前台运行一个命令,不想停止它,而是想把它移到后台,请按 Ctrl+Z。然后去 Google 搜索“作业控制”。

并且

如果你认为有作业在后台运行,并且它正在扰乱你的屏幕,输入 fg 将它带到前台,然后用 Ctrl+C 终止它。根据需要重复操作!

在任何一种情况下,如果你需要做一些更精细的操作,你可以回到这个参考资料。但第一个命令应该能让你在想办法如何继续作业时重新获得对 shell 的控制,而第二个命令应该能终止一个扰乱你屏幕的后台作业。

作业的替代方案

如果你正在使用任何现代终端,如 iTerm、Terminal 或 GNOME Terminal,只需打开一个新标签页或分屏!这样容易得多。

这样做的好处是每个标签页都有自己的标准输入和输出,所以没有覆盖的风险。当然,你也可以随心所欲地隐藏/显示/重新排列标签页。

对于只想同时进行多项任务的操作员来说,传统的作业替代方案是终端多路复用器,例如 screentmux

Effective Shell Part 6: Everything You Don't Need To Know About Job Control

多路复用器的工作方式与现代图形化终端非常相似——它们管理多个 shell 实例。与 iTerm 等现代终端相比,它的好处是拥有非常直观的图形用户界面和许多功能。

多路复用器的好处是,你可以在 SSH 会话中运行它们来管理远程机器上的复杂操作,并且它们采用客户端-服务器模型,这意味着许多人可以处理许多多路复用的进程(并且它们可以跨会话持续存在)。

我个人的偏好是两者都用——我使用现代终端,并且在其中运行 tmux。我们将在后面的章节中探讨这两个选项。

快速参考

你可能会发现作业很有用,也可能发现它们并非如此。无论哪种方式,这里是一些常用命令的快速参考

命令 用法
command & 将命令作为后台作业运行
<Ctrl+Z> 将当前进程移入后台作业,并暂停
jobs 列出所有作业
fg %1 将 1 号后台作业移至前台
bg %1 继续运行 1 号后台作业
kill %1 终止 1 号作业
wait %1 阻塞直到 1 号作业退出

如果你想了解更多关于作业的详细细节,最好的起点是 Bash 手册 - 作业控制部分,或者你偏好的 shell 手册中的“作业控制”部分。

希望你觉得这篇文章有用,并且,一如既往,请在下方留下评论、问题或建议!


脚注


  1. 如果你不是一个重度 shell 用户,这可能看起来不太可能。但如果你在 shell 中做大量工作,例如系统管理、开发运维,或者在终端中编码,这种情况会一直发生!↩︎

  2. SIGINTSIGKILLSIGTERM 等信号将在后续章节中介绍。↩︎

  3. 技术上讲,是 SIGTSTP - 即 'TTY stop'(TTY 停止)。如果你一直对 'TTY' 这个缩写感到好奇,请查看前一章,插曲:理解 Shell↩︎

  4. 另一种方法是使用 Ctrl+Y,它会发送一个延迟中断,这将使进程继续运行,直到它尝试从 stdin 读取。此时,作业被暂停,控制权交还给 shell。操作员随后可以使用 bgkillfg 将其移至后台、停止进程或根据需要保持在前台。请参阅:https://gnu.ac.cn/savannah-checkouts/gnu/bash/manual/bash.html#Job-Control ↩︎

  5. 另一个超级有用的代码片段:lsof -i -P -n | grep 8000 用于查找任何打开了给定端口的进程。又一个可以加入别名章节的内容!↩︎

  6. 有时候这是必要的。如果一个作业运行多个进程——例如,通过运行一个管道——进程标识符会随着命令从管道的一个阶段移动到下一个阶段而改变。而作业标识符将保持不变。请记住,作业是一个 shell 命令,因此可能运行多个进程。↩︎

  7. 要看看这有多糟糕,可以创建一个启动作业的脚本,然后运行它。接着运行 jobs 命令看看有什么在运行。输出可能会让你大吃一惊!↩︎

© . All rights reserved.