捐赠?

Harley Hahn
个人主页

给 Harley
发送留言


来自 Harley Hahn 的
个人寄语

Unix 之书
主页

搜索

章节列表

目录

插图列表

各章...
   1   2   3
   4   5   6
   7   8   9
  10  11  12
  13  14  15
  16  17  18
  19  20  21
  22  23  24
  25  26

术语表

各附录...
  A  B  C
  D  E  F
  G  H

命令
摘要...

• 按字母顺序
• 按类别

Unix-Linux
年表

互联网
资源

错误与
勘误

推荐语


教师
与学生
资料...

主页
与概述

练习
与答案

Unix 模范
课程与
课程大纲

教师用
PowerPoint 文件

第 26 章...

进程与作业控制 (Processes and Job Control)

在 Unix 内部,每一个对象要么是一个文件,要么是一个 进程。简单地说,文件是输入的源头或输出的目标, 而进程是正在执行的程序。文件提供对数据的访问; 进程则让事情真正发生。

对文件和进程都形成扎实的理解,这一点极其重要。 我们在第 23、24 和 25 章中详细讨论了文件。 在本章中,我们将讨论进程,以及相关的话题—— 作业控制。为此,我们要思考几个关键问题: 进程从哪里来?系统如何管理它们? 你又如何控制自己的进程?

阅读本章时,全书前面讨论过的许多内容将会串联起来, 这会让你获得极大的满足。一旦你理解了进程以及它们 是如何被管理的,你就会体会到 Unix 的丰富与精妙, 体会到它的各个部分如何彼此配合,共同构成一个 复杂而优雅的系统。

跳至页首

内核如何管理进程 (How the Kernel Manages Processes)

在第 6 章中,我们讨论过进程的概念——进程就是 正在执行的程序。更准确地说,进程(PROCESS)是一个 已装入内存、随时可以运行的程序,连同该程序的数据 以及跟踪该程序所需的信息。所有的进程都由内核 (kernel,操作系统的核心部分)来管理。你可以想象, 其中的细节十分复杂,所以我先给你一个概要。

当一个进程被创建时,内核会为它分配一个唯一的标识号, 称为进程 ID 或 PID(按三个单独的字母读作 "P-I-D")。 为了跟踪系统中的所有进程,内核维护一张进程表 (PROCESS TABLE),以 PID 作为索引,每个进程对应一个 条目。除了 PID 之外,表中的每个条目都包含描述和管理 该进程所必需的信息。

这种安排听起来眼熟吗?你应该觉得熟悉,因为它与 我们在第 25 章讨论的 i 节点号(inumber)和 i 节点(inode)体系十分相似。你还记得,每个文件都有 一个唯一的标识号,称为它的 i 节点号,用它作为索引 去查找 i 节点表。每个 i 节点都包含描述和管理某个 特定文件所需的信息。因此,进程表与 i 节点表是类似的。 同样地,进程 ID 对应于 i 节点号,而进程表中的一个 条目对应于一个 i 节点。

一个小型 Unix 系统同时运行 100 多个进程是很容易的事。 其中一些当然是用户运行的程序。然而大多数进程是为了 在后台执行任务而自动启动的。在一台大型系统上,可能 有成百上千个进程,都需要共享系统的资源:处理器、 内存、I/O 设备、网络连接,等等。为了管理如此复杂的 负载,内核提供了精巧的调度服务,有时被称为 调度器(SCHEDULER)。

调度器时刻维护着一张等待执行的进程列表。它用一个 复杂的算法一次挑选出一个进程,给它一个运行的机会, 每次运行一小段时间,这段时间称为时间片 (TIME SLICE)。(在多处理器系统上,调度器一次会 挑选多个进程。)

当我们讨论时间片这类概念时,常常把处理时间称为 CPU 时间(CPU TIME)。这个术语可以追溯到很久以前 ——在那时还没有现代的单芯片处理器——当时的计算 大部分由中央处理单元(CPU)完成。

一个典型的时间片大约是 10 毫秒(10 千分之一秒)的 CPU 时间。时间片一结束,该进程就回到调度列表中, 另一个进程随即启动。这样,每个进程最终都能获得足够的 CPU 时间来完成自己的工作。虽然按人类的标准来看 时间片很短,但现代处理器极其、极其快速,10 毫秒 实际上足以执行成千上万条指令。(请稍微想一想这件事。)

每当一个进程用完它的时间片,内核就需要把这个进程 暂停下来。但是,暂停的方式必须保证:稍后该进程重新 启动时,能够从它被打断的确切位置继续执行。为了做到 这一点,内核会为每个被中断的进程保存数据。例如, 内核会保存程序内下一条待执行指令的位置、运行环境的 一份副本,等等。

跳至页首

分叉不止,至死方休 (Forking Till You Die)

那么,进程是如何被创建的呢?除了一个显著的特例 (本章后面会讨论),每个进程都是由另一个进程创建的。 系统的运作方式如下。

正如我们在第 2 章中讨论的,内核是操作系统的 核心。因此,内核为进程提供基本服务,具体包括:

• 内存管理(虚拟内存管理,包括分页)

• 进程管理(进程的创建、终止与调度)

• 进程间通信(本地、网络)

• 输入/输出(通过设备驱动程序,即真正与物理设备通信的程序)

• 文件管理

• 安全与访问控制

• 网络访问(例如 TCP/IP)

当进程需要内核执行某项服务时,它通过系统调用 (SYSTEM CALL)发出请求。例如,进程要用一个系统调用 来启动一次 I/O 操作。当你编写程序时,使用系统调用的 具体方式取决于你所用的编程语言。例如在 C 程序中, 你会使用标准库中的某个函数。Unix 系统通常有 200-300 个系统调用,而成为一名程序员的过程之一就是 学会使用它们,至少要学会使用其中最重要的那些。

最重要的系统调用就是用于进程控制和 I/O 的那些 (参见图 26-1)。具体地说,用于创建和使用进程的 系统调用是 forkexecwaitexit

Figure 26-1: 常用的系统调用

许多重要任务只能由内核完成。当进程需要执行这类 任务时,它必须使用系统调用向内核发出请求,由内核 来做这件事。Unix/Linux 系统一般都有 200-300 个 不同的系统调用。最常用的系统调用是用于进程控制的 (forkwaitexecexitkill)以及用于文件 I/O 的(openreadwriteclose)。

系统调用 用途
fork创建当前进程的一个副本
wait等待另一个进程执行结束
exec在当前进程内执行一个新程序
exit终止当前进程
kill向另一个进程发送一个信号
open为了读或写而打开一个文件
read从文件读取数据
write向文件写入数据
close关闭一个文件

fork 系统调用创建当前进程的一个副本。一旦 这发生之后,我们就把原来的进程称为父进程(PARENT PROCESS)或更简单地称为父亲(PARENT)。新产生的进程 是父进程的一个完全相同的副本,称为子进程(CHILD PROCESS)或孩子(CHILD)。wait 系统调用迫使 一个进程暂停下来,直到另一个进程执行完毕。exec 系统调用改变进程正在运行的程序。最后,exit 系统调用终止一个进程。为了方便讨论这些概念,我们常常 把 FORK、EXEC、WAIT 和 EXIT 当作动词来使用。例如, 你可能会读到:"当一个进程分叉时,结果得到两个完全 相同的进程。"

令人惊叹的是,仅凭这四个基本的系统调用(再加上一些 我们可以忽略的细微变体),Unix 进程就能协调完成 你、Shell 和所选择运行的程序之间那套复杂的交互。 为了说明它是如何工作的,我们来看看在 Shell 提示符下 输入一条命令时会发生什么。

如你所知(第 11 章),Shell 是一个充当用户界面 和脚本解释器的程序。正是 Shell 使你能够输入命令, 并间接地访问内核的服务。虽然 Shell 很重要,但它一旦 运行起来,也不过是系统中的又一个进程而已。和所有 进程一样,Shell 有自己的 PID(进程 ID),在进程表中 也有自己的条目。实际上,在任何时候,你都可以通过显示 一个名字古怪的 Shell 变量 $(美元符号)的值, 来查看当前 Shell 的 PID:

echo $$

(关于 Shell 变量的讨论,请参见第 12 章。)

正如我们在第 13 章中讨论的,命令有两种类型: 内部命令和外部命令。内部命令或内置命令由 Shell 直接解释,因此无需创建新进程。而外部命令则要求 Shell 运行一个独立的程序。因此,每当你想运行一条 外部命令时,Shell 都必须创建一个新进程。其过程 是这样的。

Shell 做的第一件事,就是用 fork 系统调用 创建一个全新的进程。原来的进程成为父进程,新进程 成为子进程。分叉一完成,就会发生两件事。第一, 子进程使用 exec 系统调用把自己从一个运行 Shell 的进程变成一个运行外部程序的进程。第二, 父进程使用 wait 系统调用让自身暂停,直到 子进程执行完毕。

最终,外部程序运行结束,此时子进程使用 exit 系统调用让自己停止。每当一个进程永久停止时, 无论出于什么原因,我们都说这个进程"死去" (DIES)或"终止"(TERMINATES)。事实上,正如你 稍后在本章将要看到的,当我们有意停止一个进程时, 我们说我们"杀死"了它。

每当一个进程死去,它原先占用的所有资源——内存、 文件等等——都会被释放,以便其他进程使用。此时, 这个已失效的进程被称为僵尸(ZOMBIE)。虽然僵尸已经 死去、不再是一个真正的进程,但它在进程表中仍保留着 自己的条目。这是因为该条目中包含着关于这个刚刚离世的 子进程的信息,而父进程可能对这些信息感兴趣。

子进程一变成僵尸,那位一直耐心等待它死去的父进程 就会被内核唤醒。父进程现在有机会查看僵尸在进程表中 的条目,看看事情的结局如何。随后内核从该表中删除 这个条目,实际上就抹去了这个孩子短暂却有用的一生 的最后痕迹。

为了说明这一过程,我们来看看当你输入一条命令去运行 vi 文本编辑器时会发生什么。Shell 做的第一件 事就是分叉(fork),创建一个与自己完全相同的孩子进程, 然后它开始等待这个孩子死去。(*) 就在同一瞬间, 子进程使用 exec 从一个运行 Shell 的进程变成 一个运行 vi 的进程。你察觉到的现象是: 你输入 vi 命令后的一刹那,Shell 提示符就被 vi 程序取代了。

* 脚注

Unix 编程不适合胆小鬼。

当你在 vi 中做完工作,你就退出该程序。这会 杀死那个一直在运行 vi 的子进程,使它变成 僵尸。孩子的死促使内核唤醒父进程。这又促使僵尸从 进程表中被移除。与此同时,原来的进程回到它中断的 地方。你察觉到的现象是:你停止 vi 程序后 的一刹那,你看到了一个新的 Shell 提示符。

跳至页首

孤儿进程与被遗弃的进程
(Orphans and Abandoned Processes)

你可能会问:如果一个父进程分叉之后意外死去,把孩子 孤零零地撇下怎么办?孩子当然会继续执行,但现在它被 视为一个孤儿(ORPHAN)。孤儿仍然可以完成自己的工作, 可是当它死去时,却没有父进程会被唤醒。结果是, 这个死去的孩子——此刻以僵尸的形式——被困在冥冥之中, 无处可去。

在早些年,一个成为孤儿的僵尸会永远留在进程表中, 或者直到系统重新启动(看哪个先来)。 在现代 Unix 系统上,孤儿进程会被 1 号进程 ——init 进程(本章后面讨论)——自动收养。 这样,每当一个孤儿死去,init 就会以 in loco parentis(代替父母的位置)的身份, 毫不犹豫地冲下来,启动那些最终消灭这个僵尸的步骤。

还有一种类似的情况:一个父进程创建了孩子,却没有 等待孩子死去的礼貌。等到后来孩子死去变成僵尸时, 这位失职的父亲就把可怜的僵尸——像《荒郊的 Mariana》 中的那位女子一样(*)——撇在那里,任由它自言自语: "我的日子多么凄苦,他却不来。我厌倦了这生活, 厌倦了,我宁愿死去!"

* 脚注

参见阿尔弗雷德·丁尼森(Alfred Tennyson)的诗 "Mariana in the Moated Grange"。

幸运的是,这是一种不常发生的情形。事实上,这类事情 通常只在程序有缺陷时才会发生——该缺陷使程序创建了 一个孩子却不等待孩子死去。有意思的是,如果你自己的 某个程序以这种方式无意中制造出一个"永生"的僵尸, 你并没有直接的办法把它清除掉。毕竟,你怎么能杀死 一个已经死去的东西呢?

要去掉一个变成僵尸的被遗弃孩子,你可以使用 kill 程序(本章后面有介绍)来终止它的 父进程。父进程一死,僵尸就成了孤儿,会自动 被 init 进程收养。init 迟早会履行它 作为负责任的继父的使命,把最后一根木桩钉进僵尸的 心脏。

Unix 编程够不够酷?

跳至页首

区分父进程与子进程
(Distinguishing Between Parent and Child)

本章前面我解释过,Shell 通过分叉创建一个子进程来 执行外部命令。子进程随后 exec 以运行该命令,而父进程 则等待子进程终止。

我们已经讨论过,分叉的结果是两个完全相同的进程: 原来的那个(父进程)和副本(子进程)。但总得有一个 进程去等待,另一个去运行程序。如果父进程和子进程 完全相同,那么父进程怎么知道自己才是父亲?子进程 又怎么知道自己是孩子?换句话说,它们各自怎么知道 自己该做什么?

答案是:当 fork 系统调用完成它的工作时, 它会向父进程和子进程各传回一个数值,称为返回值 (RETURN VALUE)。孩子的返回值被设为 0(零)。父亲的 返回值被设为这个新建子进程的进程 ID。因此,分叉操作 结束之后,一个进程只需检查返回值,就能判断自己是 父亲还是孩子。如果返回值大于零,该进程就知道自己是 父进程。如果返回值是 0,该进程就知道自己是子进程。

那么,当你运行一条外部命令时会发生什么?Shell 分叉 之后,有两个完全相同的 Shell。一个是父进程,另一个 是子进程,但一开始它们并不知道谁是哪个。为了搞清楚, 每个进程都会检查自己从 fork 得到的返回值。 返回值为正的那个 Shell 知道自己是父进程,于是它使用 wait 系统调用让自身暂停。返回值为零的那个 Shell 知道自己是子进程,于是它使用 exec 系统调用去运行外部程序。(所有的戏法都一样, 一旦你明白其中门道,就不再觉得是魔术了。)

跳至页首

最初的第一个进程: init
(The Very First Process: init)

下一节中,我们将开始了解那些你日常用来控制进程的 程序和技巧。在此之前,我想先岔开一句,谈一个非常 有趣的观察。如果进程都是由分叉创建的,那么每个子 进程都必须有一位父亲。可是那位父亲又必须有它自己的 父亲,如此类推。的确,如果你把世系往上追溯得足够远, 就会得出结论:必定存在过某一个最初的进程。

这个结论是正确的。每个 Unix 系统都有一个进程, 它——至少间接地——是系统中所有其他进程的父亲。 具体细节在各种 Unix 之间有所不同,但我只希望你能 理解这个总体思想。我来描述一下它在 Linux 上是如何 工作的。

在第 2 章中,我们谈过启动过程(boot procedure), 也就是启动操作系统那一整套复杂的步骤。在启动过程 接近结束时,内核"手工"创建一个特殊的进程,也就是说, 不通过分叉。这个进程被赋予 PID(进程 ID)0。由于 我马上要解释的原因,0 号进程被称为空闲进程 (IDLE PROCESS)。

在执行了一些重要功能——例如初始化内核所需的数据 结构——之后,空闲进程进行分叉,创建出 1 号进程。 然后空闲进程 exec 去运行一个非常简单的程序,它 本质上是一个什么都不做的无限循环。(这就是"空闲 进程"名字的由来。)其思想是:每当没有进程等待 执行时,调度器就去运行空闲进程。等到 0 号进程蜕变 为空闲进程时,它已经完成了自己的使命,实际上已经 "消失"了。真的,如果你用 ps 命令(后面讨论) 查看 0 号进程的状态,内核会否认这个进程的存在。

那么 1 号进程呢?它负责执行设置内核、完成启动过程 所剩下的那些步骤。因此它被称为 INIT 进程,而这个 程序本身的名字就是 init。具体地说,init 进程 打开系统控制台(参见第 3 章),并挂载根文件系统 (参见第 23 章)。然后它运行 /etc/inittab 文件中包含的 Shell 脚本。在此过程中,init 会 多次分叉,以创建运行系统所必需的基本进程(例如设置 运行级别;参见第 6 章),并使用户能够登录。就这样, init 成为了系统中所有其他进程的祖先。(*)

* 脚注

0 号进程(空闲进程)分叉创建出 1 号进程,即 init 进程。因此严格说来,所有进程的最终祖先其实是 0 号进程。然而,0 号进程一旦完成自己的工作,实际上 就消失了。所以我们可以说,1 号进程是系统中所有进程 唯一活着的祖先。

事实上,如果某个进程哪天对家谱产生了兴趣,它也无力 把自己的血统追溯到 1 号进程之外,因为进程是不被允许 读取内核源代码的。

与空闲进程(#0)不同,init 进程(#1)永不停止运行。 实际上,它是进程表中的第一个进程,并且一直留在 那里,直到系统关机。即使系统启动完成之后,init 仍然会被时不常地请来执行一些重要动作。例如,正如 我们已经讨论过的,当父进程比它的孩子先死时,孩子 就成了孤儿。init 程序会自动收养所有孤儿, 以确保它们的身后事得到妥善处理。

本章后面讨论 ps(进程状态)命令时,我会向你 展示如何同时显示一个进程和它历代父亲的进程 ID。 你会发现,如果你把系统中任何进程的世系追溯得足够远, 它总会把你带回 1 号进程。

跳至页首

前台进程与后台进程
(Foreground and Background Processes)

当你运行一个程序时,它的输入和输出通常是连接到你的 终端的。对于基于文本的程序,输入来自你的键盘, 输出送到你的显示器。这样做理所当然,因为你要使用的 大多数程序都需要与你交互才能完成工作。

然而,有些程序可以自己默默运行,而不会占用你的 终端。举例来说,假设你想用一个程序从某个文件读取 海量数据,对这些数据排序,然后把输出写到另一个文件 里。这样的程序完全没有理由非要你插手才能工作。

如前所述,每当你输入一条命令去运行一个程序,Shell 都会等到程序结束,然后才请你输入下一条命令。可是, 如果你用的是我上面描述的那个排序程序,就没必要等它 跑完。你可以输入启动程序的命令,然后立刻继续执行 下一条命令,把程序留给自己慢慢运行。

要做到这一点,你只需在命令末尾输入一个 & (与号)字符。这是在告诉 Shell:你要运行的程序应该 自行独立执行。例如,假设运行排序程序的命令如下:

sort < bigfile > results

如果你输入这条一字不变的命令,Shell 会启动该程序并 一直等到程序执行完毕。只有程序做完之后,Shell 才会 显示提示符,告诉你它在等待新命令。但如果你在命令 末尾加上一个 &,情况就不同了:

sort < bigfile > results &

在这种情况下,Shell 不会等待程序结束。程序一启动, Shell 就重新取得控制权并显示新的提示符。这意味着你 可以不必等待第一个程序结束就输入下一条命令。

当 Shell 在提示你输入新命令之前先等待程序结束,我们 就说该进程在前台(FOREGROUND)运行。当 Shell 启动 一个程序,随后任它自行运行,我们就说该进程在后台 (BACKGROUND)运行。在第一个例子中,我们在前台运行 sort 程序。在第二个例子中,我们在命令末尾 输入 & 字符,把 sort 放到后台运行。

如我在第 15 章所解释的,大多数 Unix 程序都被 设计为从标准输入(stdin)读取输入,把输出写到标准 输出(stdout)。错误信息则写到标准错误(stderr)。 当你从 Shell 提示符运行一个程序时,stdin 连接到你的 键盘,stdout 和 stderr 连接到你的显示器。如果你想 改变这一点,可以在运行程序的同时重定向 stdin、 stdout 和 stderr。

在前台运行进程时,读键盘、写显示器都没问题。但是, 当你在后台运行一个程序时,进程自行执行,以便你可以 输入另一条命令。那么,如果一个后台进程试图读或写 标准 I/O,会发生什么?答案是:输入被断开,而输出 连接保持不变。

这有两点重要含义。第一,如果在后台运行的进程试图从 stdin 读取,那里将什么也没有,进程便会无限期地暂停 下去,等待输入。这个进程想要读取数据,它就会一直等、 一直等,直到你给它可读的东西。在这种情形下,你唯一 能做的就是把这个进程移到前台(后面讨论)。这样你 才能与它交互,给它想要的东西。

第二,如果在后台运行的进程向 stdout 或 stderr 写入, 输出会出现在你的显示器上。然而,由于你多半正在忙 别的,这些输出会和你正在做的事情混杂在一起, 既让人困惑又令人分心。

跳至页首

制造延迟: sleep (Creating a Delay: sleep)

为了演示后台输出是如何与前台输出搅在一起的,我给你 准备了一个小实验。在这个实验中,我们要在后台运行 两条命令组成的序列。第一条命令制造一个延迟;第二条 命令随后向终端写一些输出。在此期间(也就是延迟之中), 我们要在前台启动另一个程序。你随后就会看到,当一个 后台进程在你于前台工作时把输出写到屏幕上的时候, 会发生什么。

开始之前,我想先介绍一下我们用来制造延迟的工具。 我们要用一个名为 sleep 的程序。其语法是:

sleep interval[s|m|h|d]

其中 interval 是延迟的时长。

使用 sleep 很简单。只需按秒指定你想要的 延迟长度。例如,要暂停 5 秒,就用:

sleep 5

如果你在终端输入这条命令,看起来似乎什么也没发生。 不过 5 秒之后,程序结束,你会看到一个新的 Shell 提示符。

在 Linux 或任何使用 GNU 工具集(参见第 2 章)的 系统上,你可以在时长后面加一个单字母修饰符: s 表示秒(默认值),m 表示分钟, h 表示小时,d 表示天。例如:

sleep 5
sleep 5s
sleep 5m
sleep 5h
sleep 5d

前两条命令暂停 5 秒。后面三条命令分别暂停 5 分钟、 5 小时和 5 天。

最常见的用法是在 Shell 脚本里用 sleep 制造 一个特定的延迟。例如,假设程序 A 把数据写入某个 文件,而程序 B 需要这个文件。你可能需要编写一个 Shell 脚本,确保运行程序 B 之前数据文件已经存在。 在脚本中,你用 sleep 在循环里制造一个比如 5 分钟的延迟。每 5 分钟,你的脚本检查一次文件是否 存在。若不存在,就再等 5 分钟重试。最终,当文件被 发现时,脚本继续往下运行程序 B。

在命令行上,当你想在运行命令之前先等待指定的时间, sleep 就很好用——这正是我们要做的。为了做 这个实验,我要你快速输入下面两行命令,一行紧接着 另一行:

(sleep 20; cat /etc/passwd) &
vi /etc/termcap

第一条命令在后台运行。它暂停 20 秒,然后把口令文件 (第 11 章)的内容复制到你的终端上。第二条命令 在前台运行。它使用 vi 文本编辑器(第 22 章)查看 Termcap 文件(第 7 章)。

输入第二条命令、vi 启动之后,稍等片刻,你 就会看到口令文件的内容洒满你整个屏幕。现在你就能 体会,当你正在做别的事情时,一个后台进程把输出写到 你的终端上有多么恼人。

这个实验告诉我们的道理是?如果程序要读或写终端, 就不要把它放到后台运行。

(在 vi 中:要重绘屏幕,请按 ^L。 正是为了应付这种场合,这是一个值得记住的便利命令。 要退出,请输入 :q,然后按 <Return>。)

一个程序只有在不需要交互式运行的时候,才适合当作 后台进程运行;也就是说,它不需要从你的键盘读取, 也不需要向你的屏幕写入。想想我们前面的例子:

sort < bigfile > results &

在这种情况下,我们可以在后台运行该程序,因为它从 一个文件(bigfile)取得输入,并把输出写到 另一个文件(results)中。

有意思的是,Shell 允许你在后台运行任何程序; 你只需在命令末尾加上一个 & 字符。所以要 三思而后行。不要,比如说,把 viless 或其他这类程序放到后台运行。

— 提示 —

如果你不小心把一个交互式程序放到了后台运行,你可以 用 kill 命令终止它,本章后面有介绍。

— 技术提示 —

编译源程序是一项非常适合放在后台进行的活动。例如, 假设你正在用 gcc 编译器编译一个名为 myprog.c 的 C 程序。只要记得把标准错误重定向 到一个文件,一切就会顺利运转。下面第一条命令适用于 Bourne Shell 家族(Bash、Korn Shell)。第二条命令 适用于 C-Shell 家族(Tcsh、C-Shell):

gcc myprog.c 2> errors &
gcc myprog.c >& errors &

另一种常见情形是你要构建一个使用 makefile 的程序。 例如,假设你下载了一个名为 game 的程序。 解包文件之后,你可以用 make 在后台构建该程序:

make game > makeoutput 2> makeerrors &

在这两种情况下,程序结束时 Shell 都会为你显示一条 消息。

跳至页首

作业控制 (Job Control)

1970 年代早期,最早的 Unix Shell 在进程控制方面 提供的功能少得可怜。当用户运行一个程序时,产生的 进程使用终端作为标准输入、标准输出和标准错误。 在那个进程结束之前,用户无法再输入任何命令。如果有 必要在它自行结束之前终止该进程,用户可以按 ^C 发送 intr 信号,或按 ^\ 发送 quit 信号(参见第 7 章)。两者唯一的区别 是,quit 会生成一个用于调试的核心转储 (core dump)。

另外,用户可以在命令行末尾输入一个 & (与号)字符,把程序作为异步进程 (ASYNCHRONOUS PROCESS)来运行。异步进程有两个界定性 特征。第一,默认情况下,标准输入会连接到空文件 /dev/null。第二,由于该进程在没有任何用户输入 的情况下自行运行,它不会对 intrquit 信号作出响应。

今天,我们有了图形用户界面、终端窗口和虚拟控制台, 同时运行多个程序轻而易举。然而在 1970 年代,能够 创建异步进程却极其重要,因为它让用户可以启动那些 自行运行、不占用终端的程序。例如,如果你有一个很长 的源程序需要编译,你可以用异步进程来干这件事。进程 一启动,你的终端就空出来了,你不必停下手里的工作。 当然了,如果异步进程出了乱子,你无法用 ^C^/ 终止它。相反,你得使用 kill 命令 (本章后面有讲)。

如我们在第 11 章讨论的,最初的 Bourne Shell 由 贝尔实验室的 Steven Bourne 于 1976 年创建。这个 Shell 是 AT&T Unix 的一部分,它支持异步进程——在 1978 年之前,除了异步进程别无其他。1978 年,加州 大学伯克利分校的研究生 Bill Joy 创建了一个全新的 Shell,他称之为 C-Shell(参见第 11 章)。作为 C-Shell 的一部分,Joy 加入了对一种新特性的支持, 称为作业控制(JOB CONTROL)。(Joy 还加入了若干其他 重要的新特性,例如别名和命令历史。)

作业控制使得运行多个进程成为可能:一个在前台,其余 在后台。在 C-Shell 中,用户可以暂停任何进程,并按需 重启它。他还可以把进程在前台和后台之间移动、挂起 (暂停)它们、显示它们的状态。Joy 把 C-Shell 收入 BSD(Berkeley Unix),而作业控制被证明是这个 Shell 最受欢迎的特性之一。即便如此,AT&T Unix 又过了四年 才拥有作业控制,直到 David Korn 在 1982 年把它纳入 第一版 Korn Shell。如今,每个重要的 Unix Shell 都支持作业控制。

作业控制的基本特征是:你输入的每条命令都被视为一个 作业(JOB),由一个唯一的作业号(JOB NUMBER)标识, 作业号也称为作业 ID(读作 "job-I-D")。为了控制和 操纵你的作业,你要把作业 ID 与各种命令、变量、 终端设置、Shell 变量和 Shell 选项配合使用。作为 参考,这些工具在图 26-2 中作了汇总。

Figure 26-2: 作业控制:工具

作业控制是 Shell 支持的一项特性,它使你能够运行 多个作业,一个在前台,其余在后台。你可以选择性地 挂起(暂停)作业、重启它们、把它们在前台与后台之间 移动,并显示它们的状态。为此,你要使用各种命令、 变量、终端设置、Shell 变量和 Shell 选项。

作业控制命令
jobs显示作业列表
ps显示进程列表
fg把作业移到前台
bg把作业移到后台
suspend挂起当前 Shell
^Z挂起当前前台作业
kill向作业发送信号;默认终止作业
变量
echo $$显示当前 Shell 的 PID
echo $!显示你最后一个放入后台的进程的 PID
终端设置
stty tostop挂起那些试图写终端的后台作业
stty -tostop关闭 tostop
Shell 选项:Bash、Korn Shell
set -o monitor启用作业控制
set +o nomonitor关闭 monitor
set -o notify后台作业结束时立即通知
set +o nonotify关闭 notify
Shell 变量:Tcsh、C-Shell
set listjobs每当作业被挂起时列出所有作业(仅 Tcsh)
set listjobs longlistjobs 的长列表形式(仅 Tcsh)
set notify后台作业结束时立即通知
set nonotify关闭 notify

在 Bourne Shell 家族(Bash、Korn Shell)中,当 monitor 选项被设置时,作业控制即被启用。 对交互式 Shell 来说这是默认值,但可以通过取消该 选项来关闭它(参见第 12 章)。在 C-Shell 家族 (Tcsh、C-Shell)中,交互式 Shell 的作业控制始终 开启。

人们自然会想:作业与进程有什么不同?就实际用途而言, 这两个概念是相似的,你常会看到人们把"作业"和 "进程"当作同义词混用。不过严格说来二者有区别。 进程是正在执行或准备执行的程序。作业则指解释整条 命令行所需要的所有进程。进程由内核控制,而作业由 Shell 控制;就像内核用进程表来跟踪进程一样, Shell 用一张作业表(JOB TABLE)来跟踪作业。

举个例子,假设你输入下面这条简单的命令来显示时间 和日期:

date

这条命令产生一个单独的进程,带有它自己的进程 ID, 以及一个单独的作业,带有它自己的作业 ID。当这个作业 运行时,进程表中会新增一个条目,作业表中也会新增一个 条目。现在再考虑下面几条更复杂的命令行。第一条使用 由四个不同程序组成的管道;第二条按顺序执行四个不同 的程序:

who | cut -c 1-8 | sort | uniq -c
date; who; uptime; cal 12 2008

这几条命令行各自产生四个不同的进程,每个程序一个, 每个进程都有自己的进程 ID。但是,整条管道——无论 它可能需要多少个进程——被视为单个作业,只有一个 作业 ID。作业运行期间,进程表中会有四个条目,但作业 表中只有一个条目。

在任何时候,你都可以用 ps(进程状态)命令 显示你所有进程的列表。同样,你可以用 jobs 命令显示你的作业列表。细节我们在本章后面讨论。

跳至页首

在后台运行作业 (Running a Job in the Background)

要在后台运行一个作业,你在命令末尾输入一个 & 字符。例如,下面这条命令在后台运行 ls 程序, 并把输出重定向到一个名为 temp 的文件:

ls > temp &

每当你把一个作业放到后台运行,Shell 都会显示作业号 和进程 ID。作业号由 Shell 自己指派,从 1 开始。例如, 如果你创建 4 个作业,它们会被指派为作业号 1、2、3 和 4。进程 ID 则由内核指派,在大多数情况下是一个 多位数字。

举个例子,假设你输入了上面那条命令。Shell 显示如下 内容:

[1] 4003

这表示刚刚启动了 1 号作业,它的进程 ID 是 4003。 如果你的作业是由多个程序组成的管道,你看到的进程 ID 是管道中最后一个程序的。例如,假设你输入:

who | cut -c 1-8 | sort | uniq -c &

Shell 显示如下内容:

[2] 4354

这告诉你,你启动了 2 号作业,而最后一个程序 (uniq)的进程 ID 是 4354。

由于后台作业自行运行,你没有简便的办法跟踪它们的 进度。因此,每当一个后台作业结束,Shell 都会向你 发送一条简短的状态消息。例如,当我们第一个例子中的 作业结束时,Shell 会显示类似这样的一条消息:

[1] Done    ls > temp

这条消息通知你 1 号作业刚刚结束。

如果你正在等待某个特定的后台作业结束,这样的通知就 很重要。然而,如果你正忙于别的事情——例如编辑文件 或阅读 man 页——Shell 却不分场合地弹出状态消息, 那就会让人恼火。因此,当一个后台作业结束时,Shell 并不会立即通知你。它会等到该显示下一个 Shell 提示符 的时候才说。这样就不会让状态消息干扰另一个程序的 输出。

如果你不想等待这样的消息,有一个设置可以更改,它 迫使 Shell 在后台作业结束的那一刻立即通知你,无论你 当时正在做什么。在 Bourne Shell 家族(Bash、Korn Shell)中,你设置 notify 选项:

set -o notify

要取消该选项,用:

set +o notify

在 C-Shell 家族(Tcsh、C-Shell)中,你设置 notify 变量:

set notify

要取消该变量,用:

unset notify

关于如何使用 Shell 选项和 Shell 变量的讨论,请参见 第 12 章。如果你想让这个设置永久生效,只需把 相应的命令放入你的环境文件(第 14 章)。

跳至页首

挂起一个作业: fg
(Suspending a Job: fg)

在任何时刻,每个作业都处于三种状态(STATES)之一: 在前台运行;在后台运行;或者已暂停,等待一个信号来 恢复执行。要暂停一个前台作业,你按 ^Z (Ctrl-Z)。如第 7 章所述,这会发送 susp 信号,使进程暂停。当你以这种方式暂停一个进程时,我们 说你把它挂起(SUSPEND)或停止(STOP)了。

这些术语可能有点误导,我们花点时间来辨析一下。 "停止"一词指的是暂时的暂停。的确,你马上会看到, 一个被停止的作业是可以重启的。因此,当你按 ^Z 时,它只是把作业暂停下来。如果你想永久 终止一个进程,你必须按 ^C 或使用 kill 命令(这两者本章后面都会讨论)。

当你停止一个程序时,Shell 把它搁置一旁并显示新的 Shell 提示符。现在你可以想输入多少命令就输入多少。 当你想继续与那个被挂起的程序一起工作时,你用 fg 命令把它移回前台。这样配合使用 ^Zfg,你就能挂起一个程序、输入 几条命令,然后随时回到原来的程序。下面是一个典型的 例子,说明你可以怎样利用这个机制。

你正在用 vi 文本编辑器编写一个 Shell 脚本。 在脚本里你想用 cal 命令显示一个日历,但对 它的语法没有把握。你挂起 vi,显示 cal 的 man 页,查到你要的东西,然后回到 vi 中你离开的确切位置。做法如下。首先,输入 下面这条命令运行 vi:

vi script

现在你正在编辑一个名为 script 的文件。假装 你已经输入了脚本的前几行,而你需要了解 cal 程序。要挂起 vi,请按 ^Z:

^Z

Shell 暂停 vi 并显示一条说明性消息:

[3]+ Stopped    vi script

在这个例子中,消息告诉你 vi(3 号作业)已经 被挂起。现在你处在 Shell 提示符下。输入显示 cal 的 man 页的命令:

man cal

四处看一看,然后按 q 退出。你会看到 Shell 提示符。现在你可以通过把 vi 移回前台来重启它:

fg

现在你回到了 vi,正好处在你离开的地方。 (当你想退出 vi 时,输入 :q 并按 <Return>。)

— 提示 —

如果你正在工作,突然之间你的程序停了,你看到一条像 "Stopped" 或 "Suspended" 这样的消息,那意味着你误按了 ^Z。

发生这种情况时,你只需输入 fg,你的程序就会 恢复活力。

当你挂起一个作业时,进程会被无限期地暂停。如果你想 注销,这就会带来一个问题,因为你会留下一些挂起的 作业在那里。规则是:当你注销时,所有挂起的作业都会被 自动终止。多数情况下这会是个错误。所以如果你试图 注销而手上有挂起的作业,Shell 会显示一条警告消息。 下面是一些例子:

There are suspended jobs.
You have stopped jobs.

如果你试图注销并看到这样的消息,请用 fg 把 挂起的作业移到前台,然后正确地退出程序。如果你有 不止一个挂起的作业,你必须对每一个重复这个步骤。 这样就能避免你意外丢失数据。

有时,你可能非常确定自己就是要注销,哪怕手上有一 个或多个挂起的作业。若是这样,你只需再注销一次。 由于 Shell 已经警告过你一次,它会假定你知道自己在 做什么,于是允许你注销而不再警告。不过要记住,以 这种方式注销会终止你所有挂起的作业;下次你登录时, 它们不会在那里等着你。

— 给 Tcsh 用户的提示 —

当你在 Tcsh 中挂起一个作业时,Shell 只显示一条简短的 "Suspended" 消息,没有别的信息。不过,如果你设置了 listjobs 变量,那么每当有任何作业被挂起时, Tcsh 都会列出你所有的作业。要用的命令是:

set listjobs

如果你给 listjobs 赋值为 long,Tcsh 就会显示一份"长"列表,其中还会给出每个作业的进程 ID:

set listjobs=long

我的建议是把这个命令放进你的环境文件(参见 第 14 章),让这个设置永久生效。

跳至页首

挂起一个 Shell: suspend
(Suspending a Shell: suspend)

^Z 会挂起当时在前台运行的那个作业。然而, 有一个进程它不会挂起:你当前的 Shell。如果你想暂停 自己的 Shell,就得使用 suspend 命令。其语法是:

suspend [-f]

你为什么要挂起一个 Shell?举个例子。如我们在 第 4 章讨论的,当你自己的计算机上运行 Unix 或 Linux 时,你必须自己做系统管理。假设你用自己的用户 ID 登录,而你需要做一件要求超级用户权限的事,于是你 用 su(第 6 章)启动一个新的 Shell,在这 个 Shell 中你是 root。干了片刻之后,你意识到 需要用自己的用户 ID 快速做件事。停掉超级用户 Shell 稍后再重启太麻烦了,因为你会搞不清自己的工作目录、 变量的改动等等。相反,你可以输入:

suspend

这会暂停当前的 Shell——就是那个你是超级用户的 Shell——并把你带回前一个 Shell,在那里你是用常规 用户 ID 登录的。当你准备好重新做回超级用户、完成 管理工作时,你可以用 fg 命令把你的超级用户 Shell 移回前台:

fg

再举一个例子。假设你的默认 Shell 是 Bash,但你想 试用一下 Tcsh。你输入下面这条命令启动一个新的 Shell:

tcsh

在任何时候,你都可以用 suspend 暂停 Tcsh 并返回 Bash。稍后,你可以用 fg 继续在 Tcsh 中工作。

关于挂起 Shell 的唯一限制是:默认情况下,你不可以 挂起你的登录 Shell。这可以防止你因为停止自己的主 Shell 而把自己悬在半空中。不过在某些情形下,你可能 确实想暂停一个登录 Shell。例如,当你用 su - 而不是 su 启动超级用户 Shell 时,它创建的是一个登录 Shell(参见第 6 章)。如果你想挂起这个新 Shell,就必须使用 -f(强制)选项:

suspend -f

这是在告诉 suspend:暂停当前 Shell,不管它 是不是一个登录 Shell。

跳至页首

作业控制与多窗口 (Job Control vs. Multiple Windows)

在第 6 章中,我们讨论过在你自己的计算机上使用 Unix 或 Linux 时,同时运行多个程序的各种方法。第一, 你可以使用多个虚拟控制台,每一个都支持一个完全独立 的工作会话。第二,在图形用户界面中,你可以打开任意 多个终端窗口,每个窗口都有自己的命令行界面(CLI)和 自己的 Shell。最后,有些终端窗口程序允许你在同一个 窗口内使用多个标签页,每个标签页都有自己的 Shell。

既然有这么多灵活手段,你为什么还需要能够挂起进程、 在后台运行程序呢?为什么不干脆每个程序开一个窗口, 不要作业控制呢?对这个问题有几个重要的回答。

第一,如果你每开始一项新任务都要切换到另一个虚拟 控制台、窗口或标签页,你的工作会慢得多。很多情况下, 把手头的事暂停一下、输入几条命令、再回到原来的任务, 要省事得多。

第二,当你使用多个窗口时,屏幕上会有多得多的视觉 元素,这可能会拖慢你。而且窗口是需要管理的:移动、 调整大小、最小化成图标、最大化,等等。使用作业控制 能减少精神和视觉上的杂乱,让你的生活简单得多。

第三,常常有这样的情况:你在一段短时间内使用的命令 都与某个特定任务或问题有关。在这种情况下,能够从 历史列表中调出以前的命令是很方便的(参见 第 13 章)。当你使用各自独立的窗口时,一个窗口 中的历史列表无法在另一个窗口中访问。

最后,有些时候你会用终端仿真程序去访问远程主机 (参见第 3 章),如果你是系统管理员尤其如此。 在那种情形下,你只有唯一一个连到远程主机的命令行 界面。你没有带多个窗口的图形用户界面,也没有若干 虚拟控制台。如果你不善于使用作业控制,就只能一次 运行一个程序,那会让人抓狂。

作为一条一般规则:当你需要在完全无关的任务之间切换 时——尤其是那些需要占满整个屏幕的任务——使用多个 窗口或各自独立的虚拟控制台是有道理的。但在大多数 其他情形下,你会发现作业控制更好用、也更快捷。

跳至页首

显示你的作业列表: jobs
(Displaying a List of Your Jobs: jobs)

在任何时候,你都可以用 jobs 命令显示你所有 作业的列表。其语法是:

jobs [-l]

多数情况下,你只需单独输入命令名即可:

jobs

下面是一些示例输出,你可以看到三个被挂起的作业 (#1、#3、#4)和一个在后台运行的作业(#2):

[1]   Stopped    vim document
[2]   Running    make game >makeoutput 2>makeerrors &
[3]-  Stopped    less /etc/passwd
[4]+  Stopped    man cal

如果你想同时看到进程 ID、作业号和命令名,请使用 -l(长列表)选项:

jobs -l

例如:

[1]   2288 Stopped    vim document
[2]   2290 Running    make game >makeoutput 2>makeerrors &
[3]-  2291 Stopped    less /etc/passwd
[4]+  2319 Stopped    man cal

注意,在两份列表中,都有一个作业被标上了 + (加号)字符。这就是所谓的"当前作业"。另一个作业被 标上 -(减号)字符,它是"上一个作业"。

这些标记供各种操纵作业的命令使用。如果你不指定作业 号,这类命令默认会作用于当前作业。(我们在讨论 fgbg 命令时会看到这一点。)多数 情况下,当前作业就是最近被挂起的那一个。上一个作业 则是紧随其后的那一个。在我们的例子中,当前作业是 #4,上一个作业是 #3。

如果没有被挂起的作业,那么当前作业就是最近被移入 后台的那一个。例如,假设你输入 jobs 命令, 看到下面这些内容:

[2]   Running    make game >makeoutput 2>makeerrors &
[6]-  Running    calculate data1 data2 &
[7]+  Running    gcc program.c &

在这个例子中,没有被挂起的作业。不过有三个作业正在 后台运行。当前作业是 #7。上一个作业是 #6。

跳至页首

把作业移到前台: fg
(Moving a Job to the Foreground: fg)

要把一个作业移到前台,你使用 fg 命令。它的 语法有三种变体:

fg
fg %[job]
%[job]

其中 job 用于标识某个特定的作业。

虽然语法看起来让人糊涂,其实非常简单,你马上就会 看到。这个命令最简单的形式是单独输入 fg:

fg

这是在告诉 Shell 重启当前作业,也就是当你使用 jobs 命令时被 + 字符标记的那一个。 例如,假设你使用 jobs 命令,输出是:

[1]   2288 Stopped    vim document
[2]   2290 Running    make game >makeoutput 2>makeerrors &
[3]-  2291 Stopped    less /etc/passwd
[4]+  2319 Stopped    man cal

当前作业是 #4,它处于挂起状态。如果你单独输入 fg 命令,它会通过把 4 号作业移到前台来重启它。

再假设在另一种情形下,你又一次输入 jobs 命令, 输出是:

[2]   Running    make game >makeoutput 2>makeerrors &
[6]-  Running    calculate data1 data2 &
[7]+  Running    gcc program.c &

在这种情况下,当前作业是 #7,它正在后台运行。如果你 单独输入 fg,它会把 7 号作业从后台移到前台。 这样你就可以与该程序交互。

要移动一个不是当前作业的作业,你必须明确地指出它。 有若干种做法,汇总在图 26-3 中。

Figure 26-3: 作业控制:指定一个作业

要使用作业控制命令,你必须指定一个或多个作业。你可以 用几种不同的方式引用作业:作为当前作业、作为上一个 作业、使用特定的作业号,或者使用命令名的全部或一部分。

作业标识 含义
%%当前作业
%+当前作业
%-上一个作业
%nn 号作业
%name具有指定命令名的作业
%?name命令中任何位置含有 name 的作业

大多数时候,指定一个作业最简便的方法是使用一个 %(百分号)字符,后面跟上作业号。例如,要把 1 号作业移到前台,你可以用:

fg %1

你也可以通过引用命令的名字来指定作业。例如,如果你想 重启那个运行 make game 命令的作业,可以 用:

fg %make

实际上,你只需指出足以把它与其他所有作业区分开的 那部分命令名即可。如果没有其他以字母 "m" 开头的命令, 你可以用:

fg %m

另一种做法是使用 %?,后面跟上命令的一部分。 例如,要把 make game 命令移到前台,还有 一种写法是:

fg %?game

如我所说,如果你使用 fg 命令而不指定具体的 作业,fg 会把当前作业移到前台。(这就是你用 jobs 命令时被 + 字符标记的那个作业。) 另外,你可以用 %%+ 来引用当前作业。 因此,下面三条命令是等价的:

fg
fg %
fg %+

同样,你可以用 %- 来引用上一个作业:

fg %-

这就是当你使用 jobs 命令时被 -(减号) 标记的那个作业。

— 提示 —

要在两个作业之间快速来回切换,请用:

fg %-

一旦你习惯了这个命令,你会经常使用它。

为了便利,有些 Shell(Bash、Tcsh、C-Shell)在你仅仅 输入一个以 % 字符开头的作业标识时,就会假定你 想用 fg 命令。例如,假设 2 号作业是命令 vim document,并且没有其他作业使用相近的名字。 下面所有命令都会产生相同的效果:

%2
fg %2
fg %vim
fg %?docu

在每一种情况下,Shell 都会把 2 号作业移到前台。

在某些 Shell 中,还有一种最后的简写可用:一条只由 单独一个 % 字符组成的命令,会告诉 Shell 把 当前作业移到前台。因此下面四条命令是等价的:

%
fg
fg %
fg %+

你注意到什么有趣的地方了吗?如果你单独输入一个作业 标识,Shell 就会假定你想用 fg 命令。因此, fg 是唯一一个连命令名本身都可以省略的命令。 记住这个有趣的小知识;总有一天它会帮你赢得朋友、 影响他人。

— 提示 —

尽管我们的例子显示了同时挂起好几个作业的情形,但 通常你只会暂停一个作业,去做点别的事,然后再回来 继续原来的工作。

在那种情况下,作业控制非常简单。要挂起一个作业, 你按 ^Z。要重启这个作业,你输入 fg, 或者(如果你的 Shell 支持)输入 %

跳至页首

把作业移到后台: bg
(Moving a Job to the Background: bg)

要把一个作业移到后台,你使用 bg 命令。其语法是:

bg [%job...]

其中 job 用于标识某个特定的作业。

指定作业时,你遵循与 fg 命令相同的规则。特别 地,你可以使用图 26-3 中的各种变体。例如,要把 2 号作业移到后台,你可以用:

bg %2

如果愿意,你还可以一次把多个作业移到后台,例如:

bg %2 %5 %6

要把当前作业移到后台,就单独使用命令名,不带作业标识:

bg

你可以想象得到,你使用 fg 命令的次数会比 bg 多得多。但有一种重要情形,bg 会 大显身手。假设你输入了一条看起来要运行很久的命令。 如果这个程序不是交互式的,你可以把它挂起并移到后台。 例如,假设你想用 make 构建一个名为 game 的程序,于是你输入命令:

make game > makeoutput 2> makeerrors

等了一会儿之后,你意识到这可能要花很长时间。由于 make 不需要你提供任何输入,再占用你的终端就 没有意义了。只需按 ^Z 挂起该作业;然后输入 bg 把作业移到后台。你的终端现在空出来了。

— 提示 —

当你本打算在后台运行一个程序,却在输入命令时忘了打 & 字符,这时 bg 命令就很有用。结果 该作业在前台开始运行了。

只要按 ^Z 挂起该作业,然后使用 bg 命令把作业移到后台即可。

跳至页首

学会使用 ps 程序 (Learning to Use the ps Program)

要显示有关进程的信息,你使用 ps(process status,进程状态)程序。ps 程序是一个有用的 工具,它能帮你找到某个特定的 PID(进程 ID)、查看你 的进程在干什么,并让你对系统上发生的一切有一个总览。 遗憾的是,ps 有太多令人困惑、晦涩难懂的选项, 光是读一读它的 man 页,就可能给你的眶额皮层造成 永久性损伤。

造成这种局面有几方面原因。第一,正如我们在 第 2 章讨论的,1980 年代 Unix 有两个主要分支: 官方的 UNIX(来自 AT&T)和非官方的 BSD(来自加州大学 伯克利分校)。UNIX 和 BSD 各有各的 ps 版本, 每个版本又各有各的选项。随着时间推移,两种 ps 都变得广为人知、广泛使用。

结果就是,许多现代版本的 ps 同时支持 两类选项,我们把它们称为 UNIX 选项和 BSD 选项。 例如 Linux 就是这样。因此在 Linux 版本的 ps 上,UNIX 或 BSD 选项你爱用哪个就用哪个。不过,你 时不时也会遇到只支持 UNIX 选项、或只支持 BSD 选项的 ps 版本。由于你永远不知道什么时候就得在这样的 系统上干活,所以两类选项你都必须熟悉。

第二,ps 是一个强大的工具,系统管理员和高级 程序员用它做各种分析。因此,存在大量技术性的选项, 日常使用其实并不需要。但它们毕竟摆在那里,而当你阅读 man 页时,那些描述可能会让人摸不着头脑。

第三,如果你的系统使用 GNU 工具集——例如 Linux (参见第 2 章)——你会发现 ps 不仅支持 UNIX 选项和 BSD 选项,还支持一组额外的 GNU 专有选项。 不过大多数时候,你可以忽略这些选项。

最后,雪上加霜的是,你有时会把 UNIX 选项称为 POSIX 选项或标准选项。这是因为它们被用作 ps 的 POSIX 版本的基础。(POSIX 是一个大型项目,始于 1990 年代,目的是标准化 Unix;参见第 11 章。)

到这里你应该清楚了,要从这一团混乱中理出头绪,我们 需要一个计划,计划如下。

尽管 ps 有许许多多选项,但日常工作中真正必要 的极少。我的计划是教会你同时使用 UNIX 选项和 BSD 选项所必需的最少知识。所有那些冷僻的选项,包括 GNU 专有的那些,我们都一概不管。万一你哪天真的需要别的 选项,当然可以直接去查你系统上 ps 的 man 页 (man ps),看看有哪些可用。

跳至页首

ps 程序:基本技能 (The ps Program: Basic Skills)

要显示有关进程的信息,你使用 ps(进程状态) 程序。正如我们刚才讨论的,ps 有大量选项,可以 分为三组:UNIX、BSD 和 GNU 专有。我会教你如何使用 最重要的 UNIX 和 BSD 选项,这已经是你平时所需要的全部。

说到 ps 的选项,有一个有趣的老规矩。UNIX 选项 按常规在前面加一个短横线,而 BSD 选项不加短横线。 阅读 man 页时请记住这一点:如果某个选项带短横线, 它就是 UNIX 选项;如果不带,它就是 BSD 选项。在我们的 讨论中,我也会沿用这个规矩。

如果你使用的 ps 版本同时支持 UNIX 和 BSD 选项, 你可以任选你喜欢的一组。事实上,有经验的用户有时用 这一组选项,有时用那一组,视哪个最适合当前的问题而定。 不过,我要给你一个警告:不要在同一条命令里混用两类 选项,那可能引起微妙的问题。

首先,这里是用 UNIX 选项调用 ps 的基本语法:

ps [-aefFly] [-p pid] [-u userid]

以下是使用 BSD 选项的语法:

ps [ajluvx] [p pid] [U userid]

在两种情况下,pid 都是一个进程 ID, userid 都是一个用户 ID。

我不打算逐个解释每个选项,而是把你需要了解的全部内容 汇总在几张表里。图 26-4 给出了以 UNIX 选项使用 ps 所需的信息。图 26-5 则显示了 BSD 选项 所需的内容。请花点时间把这两张图都看一遍。一开始也许 有点让人眼花,但习惯之后,一切都会豁然开朗。

Figure 26-4: ps 程序:UNIX 选项

ps(进程状态)程序显示有关你的系统上正在运行的 进程的信息。你可以使用两组选项:UNIX 选项和 BSD 选项。 这里汇总了最重要的 UNIX 选项。

显示哪些进程?
ps与你的用户 ID 和你的终端相关联的进程
ps -a与任何用户 ID 和某个终端相关联的进程
ps -e所有进程(包括守护进程)
ps -p pid进程 ID 为 pid 的进程
ps -u userid与指定 userid 相关联的进程
显示哪些数据列?
psPID TTY TIME CMD
ps -fUID PID PPID C TTY TIME CMD
ps -FUID PID PPID C SZ RSS STIME TTY TIME CMD
ps -lF S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
ps -lyS UID PID PPID C PRI NI RSS SZ WCHAN TTY TIME CMD
特别有用的组合
ps显示你自己的进程
ps -ef显示所有用户的进程,完整输出
ps -a显示所有非守护进程
ps -t -只显示所有守护进程

Figure 26-5: ps 程序:BSD 选项

ps(进程状态)程序显示有关你的系统上正在运行的 进程的信息。你可以使用两组选项:UNIX 选项和 BSD 选项。 这里汇总了最重要的 BSD 选项。

显示哪些进程?
ps与你的用户 ID 和你的终端相关联的进程
ps a与任何用户 ID 和某个终端相关联的进程
ps ax所有进程(包括守护进程)
ps p pid进程 ID 为 pid 的进程
ps U useriduserid 相关联的进程
显示哪些数据列?
psPID TT STAT TIME COMMAND
ps jUSER PID PPID PGID SESS JOBC STAT TT TIME COMMAND
ps lUID PID PPID CPU PRI NI VSZ RSS WCHAN STAT TT TIME >COMMAND
ps uUSER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
ps vPID STAT TIME SL RE PAGEIN VSZ RSS LIM TSIZ %CPU %MEM COMMAND
特别有用的组合
ps显示你自己的进程
ps ax显示所有进程
ps aux显示所有进程,完整输出

假设你只想看到在你的终端上、以你的用户 ID 运行的 所有进程的基本信息。在这种情况下,你只需单独输入 命令名:

ps

下面是使用 UNIX 版本的 ps 时的一些典型输出:

  PID  TTY       TIME  CMD
 2262  tty1  00:00:00  bash
11728  tty1  00:00:00  ps

下面是使用 BSD 版本 ps 时同样的输出:

  PID  TT  STAT     TIME  COMMAND
50384  p1  Ss    0:00.02  -sh (sh)
72883  p1  R+    0:00.00  ps

一般而言,ps 显示的是一张表,其中每一行包含 一个进程的信息。在上面那个 UNIX 例子中,我们看到关于 两个进程的信息:#2262 和 #11728。在 BSD 例子中,我们 看到关于进程 #50384 和 #72883 的信息。

表的每一列包含一种特定类型的信息。根据你使用的选项 不同,你会看到各种各样的列。作为参考,图 26-6 列出了最常见的列标题。我们就用这张图里的信息来解读 上面例子中的内容。

Figure 26-6: ps 程序:列标题

ps(进程状态)程序显示有关进程的信息。这些信息 组织成若干列,每列都有一个标题。由于标题是缩写,它们 可能有点晦涩难懂。

作为参考,这里列出使用图 26-4 和 26-5 中介绍的 基本选项时你很可能会遇到的列标题。大多数时候,你可以 忽略那些更冷僻的列。不过我还是把全部都解释了一遍, 以防你好奇。你可以看到,UNIX 选项使用的标题与 BSD 选项使用的标题不同。至于状态码的含义,请参见 图 26-7。

UNIX 标题 含义
ADDR进程表内的虚拟地址
C处理器利用率(已废弃)
CMD正在执行的命令名
F与该进程相关联的标志
NInice 值,用于设置优先级
PID进程 ID
PPID父进程的进程 ID
PRI优先级(数字越大,优先级越低)
RSS常驻集大小(内存管理)
S状态码(D,R,S,T,Z)
STIME累计系统时间
SZ以物理页计的大小(内存管理)
TIME累计 CPU 时间
TTY控制终端的全名
UID用户 ID
WCHAN等待通道
  
BSD 标题 含义
%CPUCPU(处理器)使用百分比
%MEM实际内存使用百分比
CMD正在执行的命令名
COMMAND正在执行的完整命令
CPU短期 CPU 使用量(调度)
JOBC作业控制计数
LIM内存使用上限
NInice 值,用于设置优先级
PAGEIN缺页总数(内存管理)
PGID进程组号
PID进程 ID
PPID父进程的进程 ID
PRI调度优先级
RE内存驻留时间(秒)
RSS常驻集大小(内存管理)
SESS会话指针
SL睡眠时间(秒)
STARTED启动时刻
STAT状态码(O,R,S,T,Z)
TIME累计 CPU 时间
TSIZ文本段大小(千字节)
TT控制终端的缩写名
TTY控制终端的全名
UID用户 ID
USER用户名
VSZ虚拟大小(千字节)
WCHAN等待通道

先看 UNIX 的例子。我们看到四列,标题分别是 PIDTTYTIMECMD。 在图 26-6 中查这些名字,我们得到:

PID: 进程 ID
TTY: 控制终端的名字
TIME: 累计 CPU 时间
CMD: 正在执行的命令名

于是我们可以看出,进程 #2262 由终端 tty1 控制, 几乎没有占用 CPU 时间,正在运行 Bash。进程 #11728 的 信息基本相同。唯一的区别是,这个进程正在运行 ps 命令。你在这个例子中看到的是你所能看到的 最少内容,因为至少总是有两个进程:你的 Shell 和 ps 程序本身。不过 ps 这个进程活得不长。 事实上,它的输出一显示完,它就死掉了。

现在我们用同样的方式分析 BSD 的例子。看输出,我们 发现有五列,标题分别是 PIDTTSTATTIMECOMMAND。对照 图 26-6,我们看到:

PID: 进程 ID
TT: 控制终端的名字
STAT: 状态码(O,R,S,T,Z)
TIME: 累计 CPU 时间
COMMAND: 正在执行的完整命令

总体而言,BSD 版本 ps 的输出简单明了,只有 STAT 列例外,我们一会儿就来谈它。

在离开本节之前,我想给你看一个小小却有意义的变化: 当你使用 BSD 选项时,ps 显示的是终端的缩写名。 请花点时间仔细看看上面例子中的 TT 列。注意你 只看到两个字符,在这个例子中是 p1。这个终端的 全名其实是 ttyp1。(终端的名字在第 23 章 讨论过。)

— 提示 —

lsps 作个类比会很有意思。这两个 程序都是检查特定的数据结构,以便为你找到并显示信息。

ls 程序(第 25 章)检查以 i 节点号为索引 的 i 节点表,以显示有关文件的信息。ps 程序检查 以进程 ID 为索引的进程表,以显示有关进程的信息。

跳至页首

ps 程序:选择选项
(The ps Program: Choosing Options)

使用 ps 的最佳方法是先问自己两个问题:我关心 哪些进程?关于每个进程我想看到什么信息?一旦你想清楚 了自己的需要,你只要查图 26-4(UNIX)或 图 26-5(BSD)选出合适的选项即可。

例如,假设你想看到系统上运行的每个进程的进程 ID, 以及所有父进程的进程 ID。我们先做 UNIX 版本。首先, 我们问自己:哪个选项能显示系统上的所有进程?从 图 26-4 中我们看到,这是 -e(everything, 全部)选项。

接下来,我们必须找到能显示每个进程及其父进程的进程 ID 的选项。对照图 26-6,我们看到我们想要的列标题是 PIDPPID。回到图 26-4,我们寻找 能显示这两个标题的选项。四种选择都能胜任,那我们就用 -f(完整输出),因为它显示的输出量最少。

把这一切合起来,我们就弄清了如何显示系统中每个进程 以及所有父进程的进程 ID:

ps -ef

很可能,这条命令会产生很多行,所以最好把输出用管道 交给 less(第 21 章),一屏一屏地显示:

ps -ef | less

现在对 BSD 版本的 ps 做同样的分析。首先,我们 看图 26-5,看哪个选项能显示系统中的所有进程。 答案是 ax。接下来,我们寻找能显示父进程 ID 的 选项。我们有两个选择:jl。我们选 j,因为它产生的输出更少。于是,我们想要的命令 的 BSD 版本是:

ps ajx | less

作为练习,我们来看看要把一个进程的家谱尽量往上追溯 该怎么做。首先,我们用 UNIX 版本的 ps 显示 我们当前的进程:

ps

输出是:

  PID TTY         TIME CMD
12175 tty2    00:00:00 bash
12218 tty2    00:00:00 ps

我们的目标是追溯 Shell(进程 #12175)的父亲。首先我们 问:哪个选项能显示关于某一个特定进程的信息?看看 图 26-4,我们发现可以用 -p,后面跟上进程 ID。 接着我们问:哪个选项能显示父进程的进程 ID?答案是 -f。于是,为了开始我们的追溯,使用命令:

ps -f -p 12175

输出是:

UID       PID  PPID  C  STIME  TTY      TIME  CMD
harley  12175  1879  0  14:14  tty1 00:00:00  -bash

由此我们可以看出,进程 #12175 的父亲是进程 #1879。 让我们用这个新的进程 ID 重复同一条命令:

ps -f -p 1879

输出是:

UID      PID  PPID  C  STIME  TTY     TIME  CMD
root    1879     1  0  09:36  ?   00:00:00  login -- harley

注意,进程 #1879 的父亲是进程 #1。这就是我们本章前面 讨论过的 init 进程。

在继续之前,有两个有趣的要点我想提请你注意。第一, 注意 TTY 列中的 ? 字符。它表示该进程 没有控制终端。我们把这样的进程称为"守护进程" (daemons),本章后面会谈到它们。第二,我们看到进程 #1879 正在 root 用户 ID 的主持下运行 login 程序。这是因为 login 就是那个使 用户得以登录系统的程序。你可能还记得,在第 4 章 中,我们用的正是同一个程序来注销,让终端为新用户 做好准备。

为了完成我们对终极祖先的追溯,我们来显示进程 #1 的 信息:

ps -f -p 1

输出是:

UID      PID  PPID  C  STIME  TTY     TIME  CMD
root       1     0  0  09:34  ?   00:00:01  init [5]

我们这段寻根之旅到达了终点。正如本章前面所讨论的, 进程 #1(init 进程)的父亲是进程 #0(空闲进程)。 顺便请注意,进程 #1 运行了 init 命令,把系统 引导到运行级别 5(带图形用户界面的多用户模式)。 我们在第 6 章讨论运行级别。

跳至页首

ps 程序:状态 (The ps Program: States)

现在让我们通过讨论状态,来结束对 ps 命令的 讲解。如本章前面所讨论的,进程通常处于三种状态之一: 在前台运行;在后台运行;或者被挂起,等待一个信号以 恢复执行。此外还有其他一些较少见的变体,例如僵尸状态, 即进程已经死去而它的父进程却没有在等待它。

要查看一个进程的状态,你用 ps 显示 S 列(UNIX 选项)或 STAT 列(BSD 选项)。 先从 UNIX 版本说起。对照图 26-4,我们看到显示 S 列的 UNIX 选项是 -l-ly。 我们用 -ly,因为它显示的输出更少。因此,要显示 包含状态在内的你所有进程的列表,你会用:

ps -ly

下面是一台 Linux 系统上的一些典型输出:

S UID  PID PPID C PRI NI RSS  SZ WCHAN  TTY      TIME CMD
S 500 8175 1879 0  75  0 464 112 wait   tty1 00:00:00 bash
T 500 8885 8175 0  75  0 996 366 finish tty1 00:00:00 vim
R 500 9067 8175 2  78  0 996 077 -      tty1 00:00:02 find
R 500 9069 8175 0  78  0 800 034 -      tty1 00:00:00 ps

状态由 S 列中的单字母码描述。这些码的含义在 图 26-7 中解释。在这个例子里,我们可以看到列表 中的第一个进程,即进程 #8175(Shell),其状态码是 S。这意味着它在等待某件事完成。(具体来说,它 在等待子进程 #90682,也就是 ps 程序本身。)

Figure 26-7: ps 程序:进程状态码

在某些选项下,ps 命令会显示一列数据,指出每个 进程的状态。使用 UNIX 选项时,这一列标注为 S,内含一个单字符码。使用 BSD 选项时,这一列 标注为 STAT,内含类似的码,后面有时还跟着 1-3 个不那么重要的其他字符。以下是这些码的含义,它们在各 系统之间略有差异。

Linux、FreeBSD
D不可中断睡眠:等待某个事件完成(通常是 I/O;D="disk")
I空闲:睡眠已超过 20 秒(仅 FreeBSD)
R正在运行或可运行(可运行 = 在运行队列中等待)
S可中断睡眠:等待某个事件完成
T已挂起:因作业控制信号或因其正被跟踪
Z僵尸:已终止,父进程未在等待
Solaris
O正在运行:当前正在执行(O="onproc")
R可运行:在运行队列中等待
S睡眠:等待某个事件完成(通常是 I/O)
T已挂起:因作业控制信号或因其正被跟踪
Z僵尸:已终止,父进程未在等待

第二个进程 #8885 的状态码是 T,意味着它被挂起了。 在这个例子中,vim 编辑器原本在前台运行,后来 因为按下 ^Z 而被挂起。(这一操作在本章前面已有 说明。)

第三个进程 #9067 的状态码是 R。这意味着它正在 运行。实际上,它是一条在后台运行的 find 命令 (第 25 章)。

最后,最后一个进程是 ps 程序本身。它的状态码 也是 R,因为它同样在运行——在本例中是在前台。 正是这个进程显示出了你正在阅读的输出。事实上,等到你 看到输出时,该进程早已终止,Shell 进程(#8175)已经 重新掌握了控制权。

在结束这个例子之前,我想指出一件有趣的事。通过查看 PID 和 PPID,你可以看出 Shell 是所有其他进程的父亲。 (这对你来说应该顺理成章。)

现在我们来讨论如何用 BSD 选项查看状态。首先,请看 图 26-6。我们想显示的标题是 STAT。再看 图 26-5。注意所有形式的 ps 都会显示 STAT 列,包括不带任何选项、单独使用的 ps。如果你用的是纯 BSD 系统,你只需:

ps

如果你用的是混合系统(Linux 就是这样),你就必须使用 某个 BSD 选项来强制产生 BSD 输出。我的建议是选 j,因为它产生的输出量最少:

ps j

下面是一些典型输出,是在一台 FreeBSD 系统上使用不带 选项的 ps 命令得到的。(若使用 j 选项, 输出会类似,但列更多。)

  PID  TT  STAT     TIME   COMMAND
52496  p0  Ss    0:00.02   -sh (sh)
52563  p0  T     0:00.02   vi test
54123  p0  Z     0:00.00   (sh)
52717  p0  D     0:00.12   find / -name harley -print
52725  p0  R+    0:00.00   ps

第一个进程 #52496 是 Shell。(*) 注意 STAT 列不止一个 字符。第一个字符是状态码。第二个字符给出的是我们可以 放心忽略的深奥技术信息。(如果你有兴趣,请查看 man 页。) 在本例中,状态码是 S。在图 26-7 中查这个 码,我们看到该进程正在等待某件事完成。具体来说,它在 等待一个子进程 #52725,即 ps 程序。

* 脚注

你在第 11 章应该还记得,老式 Bourne shell 的名字 是 sh。你可能在想:这是不是一个 Bourne shell? 答案是不是;Bourne shell 已经多年不用了。恰好 FreeBSD 的 Shell 也叫 sh

第二个进程 #52563 的状态码是 T,意味着它被挂起 了。在这个例子中,vi 是因为按下 ^Z 而被 挂起的。

第三个进程 #54123 是一个老 Shell,状态码为 Z, 意味着它是一个僵尸。这是一个不寻常的发现。不知怎的, 这个进程在它的父进程没有等待它的时候死掉了。(参见本章 前面关于僵尸的讨论。)

第四个进程 #52717 是一个在后台运行的 find 程序。 它的状态码是 D,表示它正在等待一个 I/O 事件完成 (在本例中是从磁盘读取)。这合乎情理,因为 find 要做大量 I/O。不过你必须记住,每当你使用 ps,你看到的都是一张瞬时快照。恰好我们在 find 等待 I/O 的时候抓住了它。我们同样很可能 发现它正在运行,那样的话状态码就会是 R

最后,最后一个进程 #52725 是 ps 程序本身。它的 状态码是 R,因为它正在前台运行。

在离开这个例子之前,请允许我提请你注意一个有趣的要点。 如果你看最右边那一列 COMMAND,你会发现它显示 正在执行的完整命令。这一列只有使用 BSD 选项时才有。 使用 UNIX 选项时,你所能看到的永远只是 CMD 列, 它只显示名字,而不显示完整命令。(*)

* 脚注

在 Solaris 上,CMD 列确实会显示完整命令。

— 提示 —

如果你使用的系统像 Linux 那样同时支持 UNIX 和 BSD 选项,你可以挑选最符合你需要的选项。例如,假设你想 显示一张进程列表,里面带有完整命令(COMMAND) 而不是命令名(CMD)。如果你能用 BSD 选项, 可以用:

ps j

只用 UNIX 选项没有简便的办法做到这一点。(BSD 万岁!)

— 给疑神疑鬼者的提示 —

在多用户系统上,你可以用 ps 偷看别人在干什么 来消遣。特别是,当你使用 BSD 选项时,你可以查看 COMMAND 列,看到其他用户输入的完整命令。 (如果你的系统不支持 BSD 选项,你可以用 w 程序 达到同样的目的;参见第 8 章。)

起初这似乎是无伤大雅的乐趣,直到你意识到,系统上 所有其他人同样能看到在做什么。

所以要小心。如果你是个男士,你想一想,假如系统管理员 或你的女朋友(*)来偷看你,发现过去整整一个小时你一直 在用命令 vi pornography-list 工作,他们会 怎么想?

* 脚注

如果你的女朋友正是系统管理员,你就得更加小心。

跳至页首

监视系统进程: top, prstat
(Monitoring System Processes: top, prstat)

要查看你自己的进程,你可以用 ps 命令。然而, 如果你想考察整个系统呢?不错,ps 有一些选项 可以显示系统上所有进程的各种各样的信息。但是 ps 有一个重大局限:它给你的是进程的静态快照, 即它们在某一瞬间的样子。由于进程是动态的,当你需要 观察各个进程如何随时间一刻刻变化时,这个局限就变得 很重要。在这种情况下,你可以用 top 程序显示 每隔几秒更新一次的系统整体统计信息,以及那些最重要的 进程实时变化的信息。

这个程序的名字来源于它向你显示"top"(最顶层)的 进程,也就是占用 CPU 时间最多的那些进程。使用 top 的语法有点复杂,并且在各系统之间可能略有 不同。这里是在 Linux 上你会使用的基本语法。在别的 系统上,选项会有差异,所以你得去查你的联机手册。

top [-d delay] [-n count] [-p pid[,pid]...]

其中 delay 是以秒计的刷新间隔;count 是 刷新的总次数;pid 是一个进程 ID。

大多数 Linux 和 BSD 系统都有 top 程序。如果你 的系统没有 top,通常会有一个等效的程序。例如, 在 Solaris 上,你可以改用 prstat。由于选项会 因你的 top 版本而异,花一点时间查一下你系统上 的 man 页是值得的。

要看 top 如何工作,请单独输入该命令:

top

要随时退出该程序,请按 q^C

lessvi 一样,top 以原始模式 工作(参见第 21 章)。这使它得以完全接管命令行和 屏幕,按需增删行、改动字符。举个例子,看看 图 26-8,那里有一份典型输出的缩写示例。

Figure 26-8: top 程序

top 程序用于显示有关系统上"top"进程——也就是 占用 CPU 时间最多的进程——的动态信息。你在这里看到的 是 top 所显示输出类型的一份缩略示例。输出会按 固定间隔更新。在 top 运行期间,你可以键入命令 来控制它的行为。要获得帮助,按 h;要退出,按 q^C

top - 9:10:24 up 14:50, 7 users, load average: 0.32,0.17,0.05
Tasks: 97 total, 1 running, 92 sleeping, 4 stopped, 0 zombie
Cpu(s): 1.7% us, 2.0% sy, 0.0% ni, 96.4% id, 0.0% wa
Mem: 385632k total, 287164k used, 98468k free, 41268k buffer
Swap: 786424k total, 0k used, 786424k free, 156016k cached

 PID USER   PR NI VIRT  RES  SHR S %CPU %MEM  TIME+  COMMAND
4016 harley 16  0 2124  992  780 R  1.3  0.3 0:00.35 top
3274 harley 15  0 7980 1808 1324 S  0.3  0.5 0:01.14 sshd
   1 root   16  0 1996  680  588 S  0.0  0.2 0:01.67 init
   2 root   34 19    0    0    0 S  0.0  0.0 0:00.00 ksoftirqd
   3 root   RT  0    0    0    0 S  0.0  0.0 0:00.00 watchdg
   4 root   10 -5    0    0    0 S  0.0  0.0 0:00.00 events
   5 root   10 -5    0    0    0 S  0.0  0.0 0:00.01 khelper
   6 root   11 -5    0    0    0 S  0.0  0.0 0:00.00 kthread
   8 root   10 -5    0    0    0 S  0.0  0.0 0:00.02 kblockd
  11 root   10 -5    0    0    0 S  0.0  0.0 0:00.00 khubd

输出可以分为两部分。最上面五行显示关于系统整体的信息。 在我们的例子中,第一行显示时间(上午 9:10)、系统已 运行多久(14 小时 50 分钟)以及用户数量(7)。此外还有 大量其他更技术性的信息,给出有关进程、CPU 时间、 实际内存(Mem)以及虚拟内存(swap 空间)的统计数字。

在系统信息之下,你看到描述各个进程的数据,每行一个 进程,按 CPU 使用量排序。在我们的例子中,系统很清闲。 事实上,top 自己就是最"top"的进程。

top 程序之所以强大,是因为它 会按固定间隔 自动刷新统计数字。默认间隔因你的 top 版本而异。 例如,在我的一台 Linux 系统上是 3 秒;在我的 FreeBSD 系统上是 2 秒;在我的 Solaris 系统上(使用 prstat)是 5 秒。要改变刷新速率,请使用 -d(延迟)选项。例如,要告诉 top 每秒 刷新一次,你可以用:

top -d 1

有些版本的 top 允许你输入更短的间隔。如果你的 系统支持,不妨试试用极快的刷新率运行,例如:

top -d 0.1

在一台繁忙的系统上,这会形成一幅迷人的景象。(*)

* 脚注

给男士们的提示:如果你有一位急切想打动的约会对象, 请她到你家坐坐,让她坐在你的计算机前。然后登录一台 繁忙的 Unix 或 Linux 系统,以 1 秒或更短的刷新率运行 top。如果这都打动不了她,那就什么也别想了。

由于 top 以原始模式工作,你可以在程序运行期间 键入各种命令。最重要的命令是 q,它退出程序。 次重要的是 h(help,帮助)或 ?,它显示 所有命令的摘要。第三个命令——并不总是有文档记载——是 <Space> 键。它迫使 top 当场刷新显示。 当你选了较慢的刷新率却又需要立即更新时,按 <Space> 就很有用。我这里不打算把全部命令都过一遍, 因为它们非常技术性。不过,当你有空时,请按 h, 看看你的 top 版本都有哪些功能。

为了额外的控制,还有两个选项你可以使用。默认情况下, top 无限刷新下去。-n 选项让你告诉 top 只刷新若干次。例如,要刷新显示 6 次、 每 10 秒一次,你可以用:

top -d 10 -n 6

在这种情况下,程序只会运行 60 秒。

要显示关于某个特定进程的信息,使用 -p,后面 跟上进程 ID,例如:

top -p 3274

要指定多个进程 ID,用逗号把它们分隔开。例如,下面这条 命令使用 1 秒的刷新率,并显示 #1 到 #5 各进程的信息:

top -d 1 -p 1,2,3,4,5

一般而言,top 更多是被系统管理员和程序员使用, 而不是普通用户。通常,管理员会用 top 做性能 监视。例如,他可能想看看一个新应用在一台服务器上的 表现,或者他可能想评估两个不同的数据库程序,看哪一个 运行得更有效率。程序员则常用 top 来测试一个 程序在各种负载下的性能。

你会发现,pstop 更能满足你的需要。 不过在特定情形下,top 可能价值千金。例如, 如果你用的系统突然变得异常缓慢,你可以用 top 来弄清究竟发生了什么。

跳至页首

显示进程树: pstree, ptree
(Displaying a Process Tree: pstree, ptree)

到目前为止,我们讨论了两件可以用来显示进程信息的重要 工具:用 ps 查看静态信息,用 top 查看 动态信息。第三件工具 pstree,在你想要理解进程 之间的关系时很有用。

本章前面我解释过,每个进程(除最初那一个之外)都是 由另一个进程创建的。当这件事发生时,原来的进程称为 父进程;新创建的进程称为子进程。每当一个新进程被创建, 它都会被赋予一个标识号,即进程 ID 或 PID。

在启动过程接近尾声时,内核创建最初的第一个进程, 即空闲进程,它的 PID 是 #0。在完成若干任务之后, 空闲进程创建第二个进程,即 init 进程,它的 PID 是 #1。随后空闲进程就进入永久的沉睡(名字由此而来)。

init 进程的职责是创建各种其他进程。这些第三代进程中 大多数是守护进程(本章后面我会解释这个名字),它们的 任务是等待某件事发生,然后作出 恰当的反应。具体地说,有些守护进程什么都不做,只是 等待用户登录。当一个用户准备登录时,该守护进程就 再创建一个进程来处理这件事。登录守护进程随后又创建 另一个进程来运行用户的 Shell。最后,每当 Shell 需要 为用户执行一个程序时,Shell 就再创建一个进程来完成 这项工作。

尽管这套安排看起来很复杂,但只要作一个简单的观察, 它就能被极大化简:每个进程(除第一个之外)都只有一位 父亲。因此,我们可以想象把系统中的所有进程排列成一棵 巨大的树状层次结构,init 进程位于树的根部。我们把这样 的数据结构称为进程树(PROCESS TREES),用它来显示父 进程与其孩子们之间的联系。

你可以用 pstree 程序显示系统进程树任一部分的 图示。例如,你可以显示从 init 进程开始的整棵进程树。 或者,你可以显示以某个特定 PID 或用户 ID 为基础的 子树。使用的语法是:

pstree [-aAcGnpu] [ pid | userid ]

其中 pid 是一个进程 ID,userid 是一个 用户 ID。

大多数 Unix 系统都提供 pstree 程序。如果你的 系统没有 pstree,有时会有一个等效的程序。例如, 在 Solaris 上,你可以改用 ptree。(细节参见 联机手册。)在另一些系统上,ps 命令有专门的 选项来显示进程树。你可以试试 ps fps -H,尽管输出不如 pstree 漂亮。

要看 pstree 如何工作,请先不带任何选项输入 该命令。默认情况下,pstree 绘制整个系统的进程 树,从 init 进程开始。这会产生很多行,所以最好把输出 用管道交给 less(第 21 章),一屏一屏地 显示:

pstree | less

下面是一份缩略示例,显示一台 Linux 系统上输出的前八行。 注意树的根——init 进程——位于图示的顶部:

init-+-apmd
     |-and
     |-automount
     |-crond
     |-cups-config-dae
     |-cupsd
     |-2*[dbus-daemon---{dbus-daemon}]
     |-dbus-launch

看着这棵进程树,我要你注意几件事。首先,在每一层上, 树都按进程名以字母顺序排列。这是默认行为,你可以用 -n 选项改变它(见下文)。

接下来,注意倒数第二行中的记号 2*。它表示 存在两棵完全相同的子树。使用这样的记号能让 pstree 生成更紧凑的图示。如果你想让 pstree 展开所有子树,包括那些相同的子树, 请使用 -c(不压缩)选项。

最后,你可以看到 pstree 用普通的 ASCII 字符 绘制树的分支。在某些终端上,pstree 会改用 特殊的画线字符。这使它得以画出连续的线条。如果出于 某种原因你的输出看起来不对劲,你可以用 -A 强制 使用 ASCII 字符,或用 -G 强制使用画线字符。 请花点时间试一试,看看哪一类输出在你的系统上最好看:

pstree -A | less
pstree -G | less

除了显示选项之外,还有一些选项让你控制显示哪些信息。 我最喜欢的两个选项是 -p(显示每个进程的 PID) 和 -n(按 PID 而不是按进程名对树排序):

pstree -np

下面是使用这些选项时输出的前八行。我们看到进程树从 进程 #1(init 进程)开始。这个进程有许多孩子:进程 #2、#3、#4、#5、#6,等等。进程 #6 又有它自己的孩子: #8、#11、#13、#80,等等。

init(1)-+-ksoftirqd(2)
        |-watchdog(3)
        |-events(4)
        |-khelper(5)
        |-kthread(6)-+-kblockd(8)
        |            |-khubd(11)
        |            |-kseriod(13)
        |            |-pdflush(80)

默认情况下,pstree 从根开始绘制整棵进程树, 也就是从进程 #1 开始。不过有些时候,你最感兴趣的只是 树的某一部分。在这种情况下,有两种方法可以限制输出的 范围。如果你指定一个 PID,pstree 会显示由那个 特定进程派生的子树。

举个例子。你用 Bash 作为你的 Shell。在 Shell 里,你有 两个后台进程:makegcc。你还有两个 被挂起的进程:vimman。你想显示一棵 只包含这些进程的进程树。首先,你用 psecho $$ 查出你 Shell 的 PID。恰好是 #2146。 然后你输入下面这条命令:

pstree -p 2146

输出如下:

bash-+-gcc(4252)
     |-pstree(4281)
     |-make(4276)
     |-man(4285)---sh(4295)---less(4301)
     `-vim(4249)

注意 man 创建了一个子进程来运行一个新的 Shell (#4295),这个新 Shell 又创建了另一个子进程(#4301) 来运行 less。这是因为 manless 来显示它的输出。

限制进程树范围的第二种方法,是指定一个用户 ID 而不是 PID。当你这样做时,pstree 只显示在那个用户 ID 主持下运行的进程,例如:

pstree -p harley

我要提到的最后两个选项,用于在进程名之外显示额外的 信息。-a(all,全部)选项显示每个进程的完整 命令行,而不仅是程序名。-u(用户 ID 变化) 选项在子进程运行于与父进程不同的用户 ID 之下时,标出 这一转换。

跳至页首

思考 Unix 如何组织进程与文件: fuser
(Thinking About How Unix Organizes Processes and Files: fuser)

在继续之前,我想花点时间请你思考一下:Unix 组织进程 的方式与组织文件的方式有何相似之处。

进程和文件都可以被想象成存在于根部朝上的层次树之中。 进程树的根是进程 #1(init 进程)。文件树的根是根目录 (参见第 23 章)。在进程树中,每个进程上方都有 唯一一个父进程。在文件树中,每个子目录上方都有唯一 一个父目录。要显示文件树,我们使用 tree 程序 (第 24 章)。要显示进程树,我们使用 pstree 程序。

再多想一下,我们还能发现更多相似之处。每个进程都由 一个称为进程 ID 的唯一数字标识。每个文件都由一个称为 i 节点号的唯一数字标识。在内部,Unix 用一张以进程 ID 为索引的进程表来跟踪进程。在进程表中,每个条目包含 关于单个进程的信息。类似地,Unix 用一张以 i 节点号为 索引的 i 节点表来跟踪文件。在 i 节点表中,每个条目 (即 i 节点)包含关于单个文件的信息。

然而,事出有因,这个类比我们只能推到这里为止。 为什么?因为进程与文件之间有一个根本的区别。进程是 动态的:每一瞬间,描述它们的数据都在变化。文件则 相对静态。

例如,要显示有关文件的信息,我们使用 ls 程序 (第 24 和 25 章),它只需到 i 节点表里去找数据。 要显示有关进程的信息,我们使用 pstop 程序,而收集进程信息要更棘手些。不错, 一些基本数据可以在进程表中找到。但是,大部分动态信息 必须来自内核本身,而获取这类信息并不像查表那么简单。

为了取得完成本职工作所需要的数据, pstop 都必须使用一类叫做 proc 文件的伪文件(见第 23 章)。在 /proc 目录里,每个进程都由它自己的 proc 文件来代表。 当一个程序需要某个进程的信息时,它就从那个进程的 proc 文件里读取。而这反过来又会触发一个请求,让内核提供所需 的数据。整个过程快得让你根本不会想到:查找进程信息其实 比查找文件信息要复杂得多。

你也许会问,有没有什么工具能把进程和文件这两个世界 连接起来?有的。其中最有趣的一个是 fuser,它是一个系统管理工具,列出所有正在 使用某个指定文件的进程。举例来说,假设你输入下面这条 命令,运行 find 程序(第 25 章)来搜索名字 叫 foo 的文件。注意,这个程序是在后台运行的, 而且它把标准输出重定向到一个叫 bar 的文件里(*):

find / -name foo -print > bar 2>/dev/null &

* 脚注

关于 foobar 这两个名字的讨论,请见第 9 章。

程序启动时,Shell 会显示下面这条消息,向你展示作业 号(3)和进程号(3739):

[3] 3739

由于标准输出被重定向到了 bar,你知道在这个 程序运行期间,这个文件是一直被占用的。要验证这一点, 你输入这条命令:

fuser bar

下面是输出结果:

bar: 3739

如你所见,bar 这个文件正被 #3739 号进程使用。就这样,fuser 给出了一个很有意思的例子,说明单个工具如何能够同时 收集关于进程和文件的信息。

如果你试着拿 fuser 做实验,可能会碰到一个值得 讨论的问题。fuser 程序是供系统管理员使用的。 因此,它通常和其他同类工具一起存放在某个管理目录里, 比如 /sbin(见第 23 章)。然而,除非你以超级用户 身份登录,否则这些管理目录多半不在你的搜索路径里。这就 意味着,当你键入 fuser 命令时,Shell 是找不到这个程序的。

遇到这类问题时,只要用 whereis(第 25 章)找出 fuser 在你系统上的位置即可。例如:

whereis fuser

下面是一些典型的输出:

fuser: /sbin/fuser /usr/share/man/man1/fuser.1.gz

在这个例子中,第一个路径是程序所在的位置,第二个路径是 man 手册页所在的位置。要运行 fuser,你只需要告诉 Shell 到哪里去找这个程序:

/sbin/fuser bar

当你想运行的程序所在目录不在你的搜索路径里时,用的就是 这个办法。

跳至页首

终止一个进程 (Killing a Process): kill

kill 程序有两种用途:终止一个进程,以及向一个 进程发送信号。本节我们先谈终止。下一节再讨论更一般的 话题——信号。

一般来说,程序会一直运行,直到它自己结束,或者直到你 叫它退出。你通常可以提前让一个程序停下来:按 ^C 发送 intr 信号(见第 7 章),或者 键入一条退出命令。不过,这些办法并不总是有效。比如, 有时候程序会卡住,不再响应键盘。在这种情况下,按 ^C 或键入退出命令都没用。还有一个类似的问题: 你想终止一个在后台运行的程序。由于后台进程不从键盘 读取输入,你没办法直接联系到它。

遇到这些情况,你可以用 kill 程序来终止一个程序。 用这种方式终止程序时,我们说你把它的进程 KILL(杀死)了。 所用语法是:

kill [-9] pid... | jobid...

其中 pidjobid 用来指明进程。

大多数时候,你想杀掉的是一个单独的进程。你会先用 psjobs 找出想杀掉的进程的进程号或作业 号,然后用 kill 执行真正的终止。看下面这个例子。 你输入了下面这条命令,让 make 程序在后台运行:

make game > makeoutput 2> makeerrors &

过了一段时间,你决定杀掉这个进程。第一步是弄清进程号。 你输入:

ps

输出是:

 PID TTY         TIME CMD
2146 tty2    00:00:00 bash
5505 tty2    00:00:00 make
5534 tty2    00:00:00 ps

你要找的进程号是 5505。要杀掉这个进程,你输入:

kill 5505

Shell 会杀掉这个进程并显示一条消息,例如:

[2]  Terminated   make game >makeoutput 2>makeerrors

这表示正在运行 make game 程序的进程已经被杀掉了。行首的数字表示该进程当时是 #2 号作业。

列出你的进程的另一种办法是使用 jobs -l 命令。假设你当时用的不是 ps,而是下面这条命令:

jobs -l

你会看到这样的内容:

[2]-  5505 Running    make game >makeoutput 2>makeerrors &

同样,你可以用 kill 5505 这条命令来杀掉 make 进程。不过还有另一种办法: 你可以像使用 fgbg 命令那样指定作业号(见图 26-3)。因此在刚才的情形下, 下面任何一条命令都能奏效:

kill 5505
kill %-
kill %2
kill %make
kill %?game

再说说另一种常见情形。一个前台进程变得毫无反应,无论你 键入什么都停不下它,连 ^C 也不行。你有两个选择。第一,可以尝试按 ^Z 把这个进程挂起。如果成功了,你就可以用 psjobs 找到这个进程,再用 kill 终止它。

第二种选择是打开一个新的终端窗口,用 ps -ups U 列出你的用户身份下运行的所有进程。这样你就能认出那个失控的 进程,并用 kill 终止它。事实上,有时候这是杀掉 一个独自漂在深空里的进程的唯一办法。

如果你用的是一台远程 Unix 主机,还有第三种选择。 如果其他办法全都失效,那就干脆与主机断开连接。在某些 系统上,一旦你的连接中断,内核会自动杀掉你的全部进程。 当然,这也会杀掉当时可能在运行的其他任何程序。

每当你杀掉一个有子进程的进程时,都会连带把这些子进程 一起杀掉。因此,你只要找到最初的父进程并把它杀掉,就能 杀掉整组相关的进程。(在 Unix 里,血缘关系是很牢的。)

^C 或退出命令不奏效时,kill 通常能解决问题。不过偶尔连 kill 也会失败。这种情况下,有一个必定有效的 变通用法:在命令中加上选项 -9。这会发送 9 号"必杀"信号(我们在下一节会讲到)。例如:

kill -9 5505
kill -9 %2

发送 9 号信号总是有效。不过它应当是你的最后 一手,因为它杀得太快了。当你使用 kill -9 时,进程没有机会释放它所占用的任何 资源。例如,进程将无法关闭文件(这可能导致数据丢失)、 释放内存,等等。使用 kill -9 还可能产生被遗弃的子进程,它们日后就无法正常地结束了。 (关于孤儿,见本章前面的讨论。)

虽然内核通常会把烂摊子收拾干净,但在诉诸极端手段之前, 先把其他所有办法都试一遍才是明智的。

跳至页首

向进程发送信号 (Sending a Signal to a Process): kill

正如我们刚才所讨论的,你可以用 kill 程序来终止一个用其他方式无法联系到的进程。不过, kill 并不只是一个终止工具。实际上它是一个功能强大的程序,可以向任何 进程发送任何信号。这样使用时,更一般的语法形式是:

kill [-signal] pid...|jobid...

其中 signal 是你想发送的信号类型,pidjobid 用来指明进程,这一点上一节已经讨论过。

在第 23 章里,我们遇到过进程间通信(interprocess communication,IPC)的概念,也就是两个进程之间交换数据。 当时我们讨论的是用命名管道把数据从一个进程送到另一个进程。 kill 程序支持的是另一种 IPC,具体来说,是发送一种 非常简单的消息,叫做信号(SIGNAL)。信号只不过是一个被发给 进程的数字,用来让它知道发生了某种事件。识别信号并采取行动, 就是进程自己的事了。进程做到这一点时,我们说它捕获(TRAPS) 了这个信号。

在第 7 章里,我们在讨论几个特殊组合键时遇到过信号, 比如 ^C^Z。当你按下这些键之一时, 它就向当前的前台进程发送一个信号。例如,按下 ^C 会发送 2 号信号。

Unix 内部使用的信号种类很多,其中大多数只有系统程序员 才关心。作为参考,图 26-9 列出了最常用的信号。请注意,每个信号除了有一个编号, 还有一个标准化的名称和缩写(这两者都必须用大写字母键入)。

Figure 26-9: 信号

信号是一种简单但很重要的进程间控制形式。这张列表 给出了最常用的信号及其名称。当你用 kill 向进程发送信号时,可以用编号、名称或缩写 来指定该信号。如果用名称或缩写,一定要键入大写字母。 有些编号在不同类型的系统上并不相同,所以作为一般规则, 最好使用已经标准化的名称或缩写。这里显示的信号编号是 Linux 所使用的。

编号 名称 缩写 说明
1SIGHUPHUP挂断:当你注销或终端断开时发送给进程的
2SIGINTINT中断:按下 ^C 时发送
9SIGKILLKILL杀死:立即终止;进程无法捕获
15SIGTERMTERM终止:要求终止;进程可以捕获
18SIGCONTCONT继续:恢复被挂起的进程;由 fgbg 发送
19SIGSTOPSTOP停止(挂起):按下 ^Z 时发送

大体上,HUPINTKILLTERM 的信号编号在所有系统上都是一样的。但其他 信号的编号在不同种类的 Unix 之间可能不同。因此,使用永远 不变的名称或缩写,而不是使用编号,是一个好习惯。图 26-9 的表格显示的是 Linux 所使用的信号编号。

如果你想看看自己的系统支持的全部信号列表,就给 kill 命令加上 -l(列出)选项:

kill -l

如果你的系统不支持这个选项,可以去找一个名叫 signal.h 的包含文件(见第 23 章)并显示其内容。用下面 某条命令:

locate signal.h
find / -name 'signal.h' -print 2> /dev/null

kill 程序允许你指定任何想要的信号。例如,假设你 想挂起在后台运行的 %2 号作业。只要给它发送 STOP 信号即可:

kill -STOP %2

如果你不指定信号,kill 默认发送 TERM 信号。因此,下面这些命令(都作用于 3662 号进程)是等价的:

kill 3662
kill -15 3662
kill -TERM 3662
kill -SIGTERM 3662

如我所说,信号有很多种,而 kill 命令的作用就是把 某一个特定的信号发送给某一个特定的进程。从这个意义上说,把它 叫做 signal 也许更合适。不过,kill 默认发送的是 TERM 信号,其效果就是杀死进程,这也正是 这个命令叫 kill 的原因。的确,大多数人只用 kill 来杀进程,而不用它发送其他信号。

出于安全考虑,普通用户身份只能向自己的进程发送信号。而超级用户 则可以向系统上的任何进程发送信号。也就是说,如果你用的是自己的 机器,又碰上一个怎么也死不掉的进程,你随时可以切换成超级用户, 用 kill 结束它的痛苦。不过一定要非常小心,超级用户加 kill 是一个致命性极强的组合,如果你不清楚自己在做什么,它会让你惹上 大麻烦。

跳至页首

设置进程的优先级 (Setting the Priority for a Process): nice

在本章开头我解释过,即使是一台很小的 Unix 系统,也可能同时 运行着一百多个进程。大型系统可能有数千个进程,它们全都 需要共享系统的资源:处理器、内存、I/O 设备、网络连接等等。为了管理如此复杂的负载,内核使用了一个 叫做调度器(scheduler)的精密子系统,它的职责是在 各个进程之间动态地分配资源。

在做出这种一刻接一刻的决策时,调度器会考虑与每个进程相关的 若干不同数值。其中比较重要的一个值是优先级(PRIORITY), 它表示与其他进程相比,应该给某个进程多大的优先权。优先级由 许 多因素决定,而这些因素通常都不是普 通用户能够触及的。这样做只有一点道理,原因有两个。

第一,高效地管理进程是一项非常复杂的操作,调度器做得比 人类又好又快,哪怕对方是一位经验丰富的系统管理员。第二, 如果允许用户操纵优先级,那对他们来说诱惑实在太大了——他 们会抬高自己程序的优先级,而其他用户和系统本身就要为此 付出代价。

不过在某些情形下,你可能想做相反的事:把你某个程序的优先 级低。通常发生这种情况是当你在运行一个非交互式 程序,它需要在很长一段时间里占用相当多的 CPU 时间。这种情况下,你不妨做个礼貌的人,以低优先级在后台 运行这个程序。毕竟,程序多花一点时间才结束你也不会介意, 而以低优先级运行它可以让调度器把优先权给其他程序,从而使 系统响应更快、效率更高。

要以较低的优先级运行程序,你使用一个叫 nice 的工具。(能看出这个名字是怎么来的吗?) 语法是:

nice [-n adjustment] command

其中 adjustment 是一个数值,command 是你想运行的命令。

使用 nice 最简单的方式,就是把它写在你打算放到后台运行的命令前面, 例如:

nice gcc myprogram.c &

就这么简单。用这种方式启动程序时,nice 会让它以较低的优先级运行。不过,nice 不会自动把程序放到后台。这得你自己动手,在命令末尾加一个 & 字符。

什么样的程序该配合 nice 使用?一般来说,任何可以 在后台运行、又消耗大量 CPU 时间的程序。使用 nice 的传统场合是那些要编译大量 源代码的程序、用来 make(组装)软件包的程序,或者执行复杂 数学计算的程序。举例说,如果你和别人共用一台多用户机器, 而你在测试一个计算圆周率第 1,000,000 位数字的程序,那你一定要让它的优先级尽可能低。

在使用 nice 时,有两条注意事项你需要了解。第一, nice 只能用于能独立运行的自包含程序。例如,你可以把 nice 用于外部命令和 Shell 脚本。但你无法降低 Shell 内置命令(内部命令)、管道或复合命令的优先级。

第二点是你只应当对后台运行的程序使用 nice。虽然降低前台程序的优先级在技术上是可行的, 但这么做没有意义。毕竟,当一个程序在前台运行时,你希望它 尽可能快地响应你。

在大多数情况下,按我前面描述的方式使用 nice 就够了。不过偶尔你可能想更精确地控制优先级 降低多少。为此可以使用 -n 选项,后面跟一个称为优先级调整值(NICE NUMBER)或 NICENESS 的数值。在大多数系统上,你可以指定 0 到 19 之间的 nice 值。nice 值越高,程序的优先级越低(也就是说, 作为用户你就越"客气")。

当你按常规方式(不用 nice)运行一个程序时,程序得到的 niceness 是 0。这被认为是正常优先级。当你使用 nice 而不加 -n 选项时,它默认取 10,正好在范围中间。 大多数时候这就够了。但必要时你也可以指定自己的值。

举例说,假设你有一个叫 calculate 的程序,要花好几个小时执行复杂的数学 计算。为了做个体贴的人,你决定以尽可能低的优先级在后台 运行这个程序。你可以用下面这样的命令:

nice -n 19 calculate > outputfile 2> errorfile &

到这里你大概已经在想:既然大的 nice 值会降低程序优先级,那么小的 nice 值会提高优先级吗?答案是会,但前提是你是超级用户。作为 超级用户,你可以指定 -20 到 -1 之间的负数。例如,要让一个非常特殊的程序以尽可能高的 优先级运行,就切换成超级用户并输入:

nice -n -20 specialprogram

你可以想象得出,设置负的 nice 值是你几乎不需要做的事。事实上,大多数时候,让调度器自己 管理系统反而更好。

— 提示 —

当你和别人共用一台多用户机器时,养成用 nice 以低优先级在后台运行 CPU 密集型程序的好习惯。这样可以避免这类程序拖慢其他用户的 系统。

不过,nice 在单用户系统上同样有用。当你强制自己最耗资源的后台程序以低 优先级运行时,你就避免了它们拖累自己眼下的日常操作。

(换句话说,与人为善总有回报。)

跳至页首

改变现有进程的优先级
(Changing the Priority of
an Existing Process): renice

有时候,你会发现自己在前台等一个程序等了很久,忽然想到: 让这个程序以低优先级在后台跑其实更合理。这种情况下,你要做 的只是按 ^Z 挂起进程,用 bg 把它移到后台,然后降低它的优先级。要降低一个现有进程的优先级, 你使用 renice。语法是:

renice niceness -p processid

其中 niceness 是一个 nice 值,processid 是进程号。

正如上一节所讨论的,nice 值越高,优先级越低。当你以普通用户 身份使用 renice 时,只允许把某个进程的 nice 值调大,不能调小。也就是说,你可以降低一个进程的优先级,但不 能提高它。另外,作为一项合理的预防措施,普通用户只能修改自己 进程的 niceness。(能明白为什么吗?)不过,这些限制对超级用户 不适用。

下面是一个 renice 的用法示例。你输入下面这条命令, 运行一个计算圆周率第 1,000,000 位数字的程序:

picalculate > outputfile 2&> errorfile

眼看你的程序半天什么也没做出来,你想到不妨把它放到后台跑。 同时,把优先级降到尽可能低也是个好主意。首先,按 ^Z 挂起前台进程。你会看到类似下面的消息:

[1]+  Stopped    picalculate >outputfile 2>errorfile

这告诉你,#1 号作业(进程)已经被挂起。现在你可以用 bg 把这个进程移到后台:

bg %1

接着你会看到下面这条消息,它告诉你程序已经在后台运行了:

[1]+ picalculate >outputfile 2>errorfile &

然后,用 ps 查出进程号:

ps

输出是:

 PID TTY       TIME CMD
4052 tty1  00:00:00 bash
4089 tty1  00:00:00 picalculate
4105 tty1  00:00:00 ps

最后,用 renice 给这个进程赋予尽可能高的 niceness,从而把它的优先级降到尽可能低:

renice 19 -p 4089

你会看到这样一条确认消息:

4089: old priority 0, new priority 19

— 提示 —

如果你的系统似乎毫无缘由地变得迟钝,你可以用 top 看看有没有哪个非交互式进程占用了大量 CPU 时间。如果有, 你可以考虑用 renice 降低这些进程的优先级。 (警告:不要对你不理解的进程乱动手脚。)

跳至页首

守护进程 (Daemons)

你觉得自己的系统现在正在运行多少个进程?这很容易知道。 只要给 ps 程序加上合适的选项,把每个进程一行列出, 然后用管道把输出交给 wc -l(第 18 章)数一下行数即可。

下面两条命令都能办成这件事。第一条用 ps 加 UNIX 选项,第二条用 BSD 选项:

ps -e | wc -l
ps ax | wc -l

作为一个测试,我在三台不同的 Unix 系统上运行了这些命令。 首先,我检查了一台通过 Internet 访问的 Solaris 系统。当时绝对没有别人在用这台机器,上面也没有运行别的 东西。接着,我检查了坐在我旁边的一台 Linux 系统。它运行着 完整的基于图形用户界面的桌面环境,除了我之外没有别人在用。 最后,我检查了一台 FreeBSD 系统,它充当一台中等规模的 Web 服务器和数据库服务器。

结果如下。不算 ps 程序本身,那台小型 Solaris 系统运行着 46 个进程;小型 Linux 系统运行着 95 个进程;中型 FreeBSD 系统运行着 133 个进程。值得注意的是,这些都还是相对较小的数字。一台规模 可观的 Unix 系统同时运行几百甚至几千个进程,是再平常不过的 事。

显然,这些进程大多不是用户运行的程序。那么它们是什么? 答案是:它们是守护进程(DAEMONS),是在后台运行、与任何 终端完全脱离、用来提供某种服务的程序。(在 Microsoft Windows 上,提供同类功能的程序叫做"服务"。)通常,守护进程 会静静地等在后台,等待某件事发生:一个事件、一个请求、一次 中断、一个特定的时间间隔,等等。触发条件一出现,守护进程就 立刻行动起来,做好完成本职所需的一切。

守护进程承担着运行系统所必需的大量工作。作为参考,我在图 26-10 里列出了一些比较有意思的守护进程。虽然大多数 Unix 系统上都能找到少数几个守护进程(比如 init),但不同系统之间的差别还是相当大的。要找出你系统 上的守护进程,可以用 ps,然后在输出里寻找 TTY 列中是一个 ? 字符的行。这表示该进程不受 终端控制。

Figure 26-10: 守护进程

守护进程是在后台静默运行、与任何终端脱离、用以提供某种 服务的进程。Unix 系统通常有许多守护进程,各自等待着按需 完成自己的工作。这里列出的是你在许多系统上都会遇到的、 有意思的守护进程。请注意,许多名字以字母 "d" 结尾。

守护进程 用途
init所有其他进程的祖先;收养护孤儿进程
apacheApache Web 服务器
atd运行由 at 程序排入队列的作业
crond管理预先安排好的作业的执行(cron 服务)
cupsd打印调度器(CUPS=Common Unix Printing System,通用 Unix 打印系统)
dhcpd为客户机动态配置 TCP/IP 信息(DHCP)
ftpdFTP 服务器(FTP=File Transfer Protocol,文件传输协议)
gated网络的网关路由
httpdWeb 服务器
inetdInternet 服务
kerneld按需加载和卸载内核模块
kudzu在引导期间检测并配置新的或有变动的硬件
lpd打印假脱机(line printer daemon,行式打印机守护进程)
mysqlMySQL 数据库服务器
namedInternet DNS 名字服务(DNS=Domain Name System,域名系统)
nfsd网络文件访问(NFS=Network File System,网络文件系统)
ntpd时间同步(NTP=Network Time Protocol,网络时间协议)
rpcbind远程过程调用(RPC)
routed管理网络路由表
schedswapper 的另一个名字
sendmailSMTP 服务器(电子邮件)
smbd为 Windows 客户机提供文件共享与打印服务(Samba)
sshdSSH(安全 Shell)连接
swapper把数据从内存复制到交换空间,以回收物理内存
syncd使文件系统与系统内存的内容保持同步
syslogd收集各种系统消息(system logger,系统日志记录器)
xinetdInternet 服务(inetd 的替代者)

最好用的命令是 ps 的那种显示所有不受终端控制的进程的变体:

ps -t - | less

如果你的系统上这条命令不工作,可以试试下面这条:

ps -e | grep '?' | less

大多数守护进程是在引导过程最后一部分自动创建的。在某些 情况下,这些进程由 init 进程(#1 号进程)创建。在另一些 情况下,这些进程由那些自行退出的父进程创建,于是守护进程 就成了孤儿。你也许还记得本章前面讨论过的:所有孤儿都会被 init 进程收养。因此不管走哪条路,大多数守护进程最终都会 成为 #1 号进程的子进程。基于这个原因,守护进程的一种定义 就是:任何没有控制终端、且父进程的进程号为 #1 的后台进程。

如果你用的是 Linux 系统,花一点时间看看 /etc/rc.d/init.d 目录。你会发现大量 Shell 脚本,每一个都用来启动、停止或重启某个特定的守护进程。

名称的由来

Daemon


守护进程是在后台静默运行、与任何终端脱离、用以提供某种服务 的进程。虽然这个名字读作 "dee-mon"(迪门),但正确的拼法 是 "daemon"。

你偶尔会读到这样的说法:这个名字代表 "Disk And Executing Monitor"(磁盘与执行监视器),源自老的 DEC 10 和 20 计算机。不过,这个解释是事后编造出来的。"daemon" 这个名字最早由 MIT 的程序员使用,他们从事的是 1963 年开发的 CTSS(Compatible Time-sharing System,兼容分时系统)。他们造出这个词,是用来 指称另一批 ITS(Incompatible Time-sharing System,不兼容分时系统)程序员所说的"dragons"(龙)。

CTSS 和 ITS 都是 Unix 的祖先。ITS 是一个重要但古怪的操作系统,在 MIT 培养了一批狂热的追随者。当 CTSS 和 ITS 的程序员们从 MIT 转到贝尔实验室(Unix 的诞生地)时,守护进程这个概念也跟着他们一起过去了。

那么,为什么叫 "daemon" 呢?有一种说法是这个名字来自 "麦克斯韦妖"(Maxwell's demon),那是苏格兰物理学家詹姆斯· 麦克斯韦(1831-1879)为热力学第二定律的一个思想实验设计出的 想象中的生物。你可以信,也可以不信。(我不信。)

无论词源如何,谁也不知道我们为什么偏偏用了这种英式拼法。 在凯尔特神话里,daemon 通常是善良或中性的,不过是一种精灵 或灵感。而 demon(魔鬼) 则永远是一个邪恶的存在。也许这里面自有某种寓意。

跳至页首

上一章的结尾 (The End of the Last Chapter)

我要谢谢你,愿意花这么多时间陪我一起谈论 Unix 和 Linux。 我写这本书,是为了让明智的人也能轻松地使用 Unix;只要我确实帮到了你,我就感恩于这个机会。

Unix 历来吸引着最有才华的计算机用户和程序员,对他们来说, 在 Unix 上工作是一种出于热爱的劳动。Unix 之所以如此出色,一个原因是它的绝大部分设计成形于那些穿西装的 人坐起身来注意到它之前。这正是 Unix 运作得如此良好、如此优雅的原因:基本的 Unix 理念早在商业和市场人员开始设法从中赚钱之前很久就已确立。正如 我们在第 2 章讨论过的,到了 1990 年代,这一理念被移植到了 Linux 项目和开源社区,结出了 美妙的果实。

你也许还记得我说过:Unix 不容易学,但很容易用。到 了现在,你会明白这句话意味着什么:一件工具为一个聪明的 人精心设计,比它简单到让一个人生最大智力挑战是下载铃声的 人第一天就能上手,要重要得多。

我向你保证,你花在学习和使用 Unix 上的每一分钟,都会得到 丰厚的回报。我无法在你工作时陪在你身边,但你手上有这本书, 而我已经尽了很大力气,力求为你奉上我所能力及的最好的 Unix 伴侣。

虽然我有时可能有些玩世不恭 — 事实上,每当能讲出一个我编辑抓不住的笑话时,我都会讲 — 但我想在此停下片刻,祝愿你一切都好。当你阅读并反复 阅读这本书时,请记住:我是站在这一边的。

— 提示 —

Unix 很有趣。

跳至页首



练习 (Exercises)

复习问题 #1:

什么是进程?操作系统中哪一部分负责管理进程?

给出下列术语的定义:进程号、父进程、子进程、fork 和 exec。

复习问题 #2:

什么是作业?操作系统中哪一部分负责管理作业?

什么是作业控制?

在前台运行一个作业和在后台运行一个作业,有什么区别?

如何在前台运行一个作业?

如何在后台运行一个作业?

如何把一个作业从前台移到后台?

复习问题 #3:

ps(process status,进程状态)程序用来显示关于 进程的信息。这个程序可以使用哪两类选项?

对每一类选项,你会用哪些命令来显示下面这些信息?

• 你当前的进程
• #120357 号进程
• 系统上运行的所有进程
• 与用户身份 weedly 相关的进程

复习问题 #4:

你是一名系统管理员。你负责的某台系统看起来正在变慢,而你 的任务是找出原因。第一步,你想看看系统上运行的各种进程, 以及它们一刻之间的变化。你会用哪个程序?请写出能每 5 秒自动刷新运行这个程序的命令。

复习问题 #5:

杀掉一个进程和停止一个进程,有什么区别?

如何杀掉一个进程?

如何停止一个进程?

复习问题 #6:

你启动了一个名叫 foobar 的程序,它正在失控地乱跑。你会采取哪些步骤来杀掉它?如果 foobar 毫无反应,你怎么办?

应用你的知识 #1:

输入一条命令行,它先暂停 5 秒钟,然后显示消息 "I am smart."。等上 5 秒钟,确认它确实有效。

现在把延迟改成 30 秒,重新输入这条命令行。这一次,在 30 秒结束之前按下 ^C。发生了什么?为什么?

应用你的知识 #2:

你刚刚通过 Internet 用你的用户身份 weedly 登录了一台 Unix 系统。你输入 ps -f 命令,看到:

UID    PID  PPID C STIME TTY   TIME     CMD
weedly 2282 2281 0 15:31 pts/3 00:00:00 -bash
weedly 2547 2282 0 16:13 pts/3 00:00:00 ps -f

一切看起来都很正常。只是出于好奇,你决定看看系统的其余部分, 于是输入命令 ps -af。在输出的若干行中,你看到:

weedly 2522 2436 0 16:09 pts/4 00:00:00 vim secret

居然有别人在用你的用户身份登录!你会怎么做?

应用你的知识 #3:

构造一条管道,统计你系统上守护进程的数量。然后再构造第二条 管道,显示所有守护进程的排序列表。你应当只显示守护进程的名字, 不显示别的东西。

进一步思考 #1:

kill 命令来杀进程,比它本来应有的样子更复杂了。请描述一种更简单的 办法,实现同样的功能。

进一步思考 #2:

为什么 ps 会有两类不同的选项?这是好事还是坏事?

跳至页首