对话 UNIX,第 8 部分: UNIX 进程

了解 UNIX 的多任务原理

在 UNIX® 系统中,每个系统和最终用户任务都包含在某个进程中。系统总是不断地创建新的进程,当任务结束或意外发生时,进程会终止。在本文中,您将了解如何控制进程和使用一些命令来查看您的系统。

Martin Streicher (martin.streicher@linux-mag.com), 主编, Linux Magazine

Martin Streicher 目前担任 Linux Magazine 的主编。他毕业于普渡大学并获得计算机科学硕士学位,从 1982 年起他一直从事 UNIX 类系统的编程工作,他使用的编程语言包括 Pascal、C、Perl、Java 以及近期的 Ruby 语言。



2007 年 5 月 16 日

在最近的街头游乐会上,有一个单人乐队让我很是着迷。的确,这让我很开心,还给我留下了深刻印象。这个单人乐队的唯一成员利用嘴、大腿、膝盖和脚分别控制口琴、五弦琴、钹和脚鼓,生动地演奏了齐柏林飞船乐队的《天堂的阶梯》,他演奏的贝多芬《第五交响曲》也颇为动人。和他相比,我能一边拍脑袋一边摸肚子就觉得很不错了。(或者是一边拍肚子一边摸脑袋。)

对您来说,幸运的是,UNIX® 操作系统更像是那个单人乐队,而不是像我这个笨手笨脚的专栏作家。UNIX 特别擅长同时处理多个任务,并安排它们访问系统中的有限资源(内存、设备和 CPU)。打个比方,UNIX 可以一边散步,一边嚼口香糖。

这个月我们研究的内容要比平常更深入一些,我们会看看 UNIX 是如何同时做这么多事的。这次我们还会探索 shell 的内部,了解工作控制命令,如 Ctrl+C(终止)和 Ctrl+Z(挂起)是怎样实现的。

一个真正的多任务系统

在 UNIX(以及大多数现代操作系统,包括 Microsoft® Windows®、Mac OS X、FreeBSD 和 Linux®)中,每个计算任务都是由一个进程表示的。UNIX 似乎能同时运行很多任务,这是因为每个进程都会轮流(从概念上来讲)分到一小片 CPU 时间。

一个进程就像一个容器,它与某个正在运行的应用程序、环境变量、应用程序的输入和输出,以及进程的状态(包括其优先级和累计资源使用情况)捆绑在一起。图 1 显示了一个进程。

图 1. UNIX 进程的概念化模型
UNIX 进程的表示形式

为了便于理解,您可以把一个进程想像成一个独立的国家,有边界、资源,还有国民生产总值。

每个进程还有一个所有者。一般来说,您启动的任务(如您的 shell 和命令)的所有者就是您。系统服务的所有者可能是特殊用户或超级用户 root。例如,为了增强安全性,Apache HTTP Server 的所有者一般是一个名为 www 的专用用户,该用户能提供 Web 服务器所需的的文件访问权限,但不包含其他权限。

进程的所有权可能会改变,但必须严格保持其独占性。一个进程在任何时候都只能有一个所有者。

最后,每个进程都具有权限。一般来说,进程的权限与其所有者的权限是相称的。(例如,如果您无法在命令行 Shell 中访问某个特定文件,则您从 Shell 中启动的程序也会继承同样的限制。)这一继承规则有一个例外情况,即应用程序启用了特殊的 setuidsetgid 位,如 ls 显示的那样,在此情况下,某个进程可能会获得比其所有者更高的权限。

setuid 位可以使用 chmod u+s 进行设置。setuid 的权限如下所示:

$ ls -l /usr/bin/top
-rwsr-xr-x     1 root  wheel     83088 Mar 20  2005 top

setgid 位可以使用 chmod g+s 设置:

$ ls -l /usr/bin/top
-r-xr-sr-x   1 root  tty  19388 Mar 20  2005 /usr/bin/wall

一个 setuid 进程(如启动 top)是用拥有该文件的用户权限运行的。因此,当您运行 top 时,您的权限会被提升,与 root 的权限等同。类似地,一个 setgid 进程是用与文件的组所有者相关联的权限运行的。

例如,在 Mac OS X 中,wall 工具(“write all”的缩写,因其会将某个消息写入所有物理或虚拟终端设备而得名)的 setgid 被设为tty(如上所示)。当您登录并分配到一个用来键入的终端设备(该终端成为 Shell 的标准输入)时,您将被指定为该设备的所有者,而 tty 成为组所有者。因为 wall 是以组 tty 的权限运行的,所以它可以打开和写入所有终端。

获取列表

就像所有其他系统资源一样,您的 UNIX 有一个有限但十分庞大的进程池(实际上,系统中的进程几乎用之不尽)。每个新任务(如启动 vi 或运行 xclock)都会立即从池中分配到一个进程。在 UNIX 系统中,您可以使用 ps 命令,查看一个或多个进程。

例如,如果您想查看您拥有的所有进程,键入 ps -w --user username

$ ps -w --user mstreicher

您可以使用 ps -a -w -x 查看完整的进程列表。(ps 命令的格式和特定的标志随各个 UNIX 版本而有所差异。请参阅系统的联机文档,以查找具体的说明。) -a 是选择 tty 设备上运行的所有进程;-x 则可进一步选择与 tty 无关的所有进程,通常包括所有的永久系统服务,如 Apache HTTP server、cron 工作调度程序等等;-w 则以加宽的格式显示内容,在查看命令行或与每个进程相关的应用程序完整路径名时很有用。

ps 具有丰富的功能,某些版本的 ps 甚至允许您自定义输出。例如,下面就是一个有用的自定义进程列表:

$ ps --user mstreicher -o pid,uname,command,state,stime,time 
  PID USER     COMMAND          S STIME     TIME
14138 mstreic  sshd: mstreicher S 09:57 00:00:00
14139 mstreic  -bash            S 09:57 00:00:00
14937 mstreic  ps --user mstrei R 10:23 00:00:00

-o 根据各列名称的顺序对输出进行格式化。pidunamecommand 分别指进程 ID、用户名和命令。state 代表进程的状态,如正在睡眠 (S) 或运行 (R)。(稍后将对进程状态进行更详细的说明。)stime 显示命令的开始时间,time 则显示该进程占用了多少 CPU 时间。

进程从哪里来?

在 UNIX 中,某些进程会从系统启动到关机的时间里一直运行,但大多数进程都会随任务的开始和完成而迅速地出现和消失。有时,某个进程可能会“早夭“,甚至会“暴死”(比如在系统崩溃时)。新的进程是从哪里来的呢?

每个新的 UNIX 进程都是某个现有进程的产物。另外,每个新进程(不妨将其称为“子”进程)是对“父”进程的克隆体(至少有一瞬间是如此),直到“子”进程继续独立执行为止。(如果每个进程都是某个现有进程的后代,那么不免会有一个疑问:“第一个进程是从哪里来的?”请参阅下面的内容以寻找答案。)

图 1-4 详细说明了进程的产生过程:

  1. 在图 2 和图 3 中,进程 A (Process A),正在运行一个由蓝色方框表示的程序。它运行编号为 10,11,12…的指令。进程 A 有属于自己的数据、程序的副本、打开的文件集,以及自己的环境变量集,当进程 A 刚出现时,会对它们进行初次捕捉。
    图 2. 进程 A 运行代码
    在某个进程内运行代码
  2. 在 UNIX 中,fork() 系统调用(之所以有这个名称,是因为它是一个调用或请求,要求操作系统进行协助)被用来产生新的进程。当程序 A (Program A) 执行指令 13 (Instruction 13) fork() 时,系统会立即创建进程 A 的一个精确克隆版本,并将其命名为进程 Z (Process Z)。Z 具有和 A 相同的环境变量、相同的内存内容、相同的程序状态,打开的文件也一样。图 3 显示的是进程 A 生成进程 Z后,进程 A 和 Z 的状态。
    图 3. 进程 A 生成自身的克隆体
    生成的进程的表现形式
  3. 起初,进程 Z 是从进程 A 停止的地方开始执行的。也就是说,此后进程 Z 从指令 14 (Instruction 14) 处开始执行。进程 A 会在同一指令位置继续执行。
  4. 一般来说,指令 14 处的编程逻辑将测试当前的进程是子进程还是父进程,也就是说,进程 Z 和进程 A 中的指令 14 分别判定这两个进程是否为其他进程的后代或祖先。为了以示区别,fork() 系统调用在子进程中返回 0,但返回给父进程的却是进程 Z 的进程 ID。
  5. 在上次测试之后,进程 A 和进程 Z 会出现差异,每个进程会采用单独的代码路径,就像路上出现岔道,每一个都会走上不同的分枝。生成一个新进程的流程更多地被称为分叉,这就像两位旅行者走到了路上的岔道。因此,系统调用被命名为 fork()

在分叉之后,进程 A 可能会继续运行同一个应用程序。而进程 Z 则可能立即发生变化,转到另一个应用程序。后一种操作会改变程序通过进程运行的内容,它被称为执行,但您可以把它看成是一次再生过程:虽然进程 ID 不变,但进程内部的指令会被新程序的指令完全取代。图 4 显示的是稍后进程 Z 的状态。

图 4. 进程 Z 现在独立于它的祖先,即进程 A
生成的进程的表现形式

分叉

您可以在自己的命令行,很方便地体验分叉操作。首先,打开一个新的 xterm。(您现在可能会认识到,xterm 就是它本身的进程,在 xterm 中,shell 是由 xterm 产生的一个独立进程)。接下来,输入:

ps  -o pid,ppid,uname,command,state,stime,time

您应该会看到类似这样的内容:

  PID  PPID USER     COMMAND          S STIME     TIME
16351 16350 mstreic  -bash            S 11:23 00:00:00
16364 16351 mstreic  ps -o pid,ppid,u R 11:24 00:00:00

从该列表的 PPID 字段中,我们知道 ps 命令是 bash shell 的子进程。(-bash 中的连字符说明 shell 实例是一个登录 shell。)为了运行 ps,bash 会分叉,创建一个新进程;新进程通过使用执行,使其本身得以重生,转化为 ps 的一个新的实例。

这里是另一个可供尝试的实验。键入:

sleep 10 & sleep 10 & sleep 10 & ps  -o pid,ppid,uname,command,state,stime,time

您应该会看到类似这样的内容:

$ sleep 10 & sleep 10 & sleep 10 & ps  -o pid,ppid,uname,command,state,stime,time
  PID  PPID USER     COMMAND          S STIME     TIME
16351 16350 mstreic  -bash            S 11:23 00:00:00
16843 16351 mstreic  sleep 10         S 11:42 00:00:00
16844 16351 mstreic  sleep 10         S 11:42 00:00:00
16845 16351 mstreic  sleep 10         S 11:42 00:00:00
16846 16351 mstreic  ps -o pid,ppid,u R 11:42 00:00:00

命令行生成四个新进程。在每个 sleep 命令后键入 &,在后台运行每一个命令,或与 Shell 并行。 ps 是生成的另一个进程,但它是在前台运行的,可以防止 shell 在该进程终止之前运行其他命令。而且,如 PPID 的值所示,所有四个进程都是 Shell 的后代。三个 sleep 命令都被标为 S,因为没有哪个进程会在它们睡眠时使用资源。

为了方便起见,shell 会持续跟踪它生成的所有后台进程。键入 jobs,可以看到一个列表:

$ sleep 10 & sleep 10 & sleep 10 & 
[1] 16843
[2] 16844
[3] 16845

$ jobs
[1]   Running                 sleep 10 &
[2]   Running                 sleep 10 &
[3]   Running                 sleep 10 &

此处,为了方便起见,三个工作分别用标签标为 1,2 和 3。数字 16843、16844 和 16845 分别是每个进程的进程 ID。因此,后台任务 1 即为进程 ID 16843。

您可以利用这些标签,从命令行操作您的后台工作。例如,如要终止某个命令,键入 kill %N,其中 N 是该命令的标签。如要将某个命令由后台移到前台,请键入 fg %N

$ sleep 10 & sleep 10 & sleep 10 &
[7] 17741
[8] 17742
[9] 17743

$ kill %7
$ jobs
[7]   Terminated              sleep 10
[8]-  Running                 sleep 10 &
[9]+  Running                 sleep 10 &

$ fg %8
sleep 10

从命令行中同时异步运行多个命令,是处理您自己的任务集的好方法。一个长时间运行的工作(例如,系统管理的数值计算或大型程序的编译)最适合放在后台。为了捕获每个后台命令的输出,请考虑使用重定向操作符 >>&>>>>&,将输入重定向到某个文件。当后台命令结束后,shell 会在下一个提示符之前显示一条警告消息:

$ whoami
mstreicher
[8]-  Done                    sleep 10
[9]+  Done                    sleep 10
$

向遥远的进程池前进

某些进程会一直存活(如 init),而某些进程会以新的形式重生(如您的 shell)。最终大多进程都会因自然原因(即程序运行结束)而消亡。

此外,您还可以将某个进程放在一个挂起的动作序列中,等待被再次激活。正如先前的示例所示,您可以用 kill 提前终止某个进程。

当某个命令在前台运行时,如果您希望将它挂起,请按 Ctrl + Z

$ sleep 10
(Press Control-Z)
[1]+  Stopped                 sleep 10

$ ps
  PID  PPID USER     COMMAND          S STIME     TIME
18195 16351 mstreic  sleep 10         T 12:44 00:00:00

Shell 已将命令挂起,为了方便起见,还为它分配了一个标签。您可以像先前那样使用这个标签,以终止工作或让工作返回前台。您还可以使用 bg 命令在后台恢复这个进程:

bg %1
[1]+ sleep 10 &

当某个命令在前台运行时,如果您想终止它,请按 Ctrl + C

$ sleep 10
(Press Control-C
$ jobs
$

您的 Shell 能使进程的挂起和终止变得更容易,但在 Shell 单纯的外表下,却隐藏着复杂的一面。在内部,Shell 使用 UNIX 信号来影响进程的状态。信号是一个事件,它被用来向某个进程发出警报。操作系统生成许多信号,但您可以将信号从一个进程发送到另一个进程,甚至能让某个进程给自己发送信号。

UNIX 包括多种信号,它们大多都有特殊目的。例如,如果您将信号 SIGSTOP 发送到某个进程,该进程将挂起。(要获取信号的完整列表,请键入 man 7 signal 或键入 kill -L)。您可以用 kill 命令发送信号。

$ sleep 20 &
[1] 19988

$ kill -SIGSTOP 19988

$ jobs
[1]+  Stopped                 sleep 20

起初,sleep 命令在后台启动,其进程 ID 为 19988。在发送 SIGSTOP 之后,该进程会改变状态,变为挂起或停止。发送另一个信号 SIGCONT,重新激活进程,该进程将从上次停止的地方继续执行。

也就是说,每次您按 Ctrl + Z 时,您的 shell 将向前台发送 SIGSTOP 信号。bg 命令发送 SIGCONT。而 Ctrl + C 则会发送 SIGTERM,要求立即终止进程。

一些信号可以被某个进程阻塞,应用程序可以通过设计,显式地“捕捉 (catch)”信号,并以一种特殊的方式对每个事件作出反应。例如,系统服务 xinetd 会按需要启动其他网络服务,它在收到 SIGHUP 时会重新读取它的配置文件。在 Linux 中,向 init 发送信号,可能会改变系统的运行级别,甚至会导致系统关闭。.(这里有一个问题:kill %1kill 1 有什么区别?

进程甚至可以给自己发送信号。想像一下,您正在编写一个游戏,想留给用户五秒钟时间作出反应。您的代码可以设置一个五秒钟的定时器,接下来继续进行重绘屏幕等操作。当定时器的时间耗尽后,将有一个 SIGALRM 信号被送回您的进程。呯!时间到!

(这里提供了问题的答案:kill %1 会终止标签为 1 的后台工作。kill 1 会终止 init,当必须关闭计算机时,将向操作系统发送这个信号。)

在特殊情况下,操作系统还可以将一些其他信号传送给进程。内存违例会引发 SIGSEGV 信号,立即终止进程,并留下一个内核转储。有一个特殊的信号 SIGKILL 是无法被阻塞或捕捉的,它会立即终止某个进程。

和 UNIX 中许多其他资源一样,您只能向您拥有的进程发送信号。这可以防止您终止重要的系统服务和其他用户的进程。超级用户 root 可以向任何进程发送信号。

更多魔法揭密

UNIX 有许多可活动的部分。它有系统服务、设备、内存管理器等等。好在这些复杂的花样大都被隐藏起来,不会被看到,或可以通过用户界面(如 shell 或窗口工具)很方便地使用。更妙的是,如果您想深入探究,随时都可以使用 top, pskill 等专用工具。

现在您已经知道了进程的工作原理,可以组成自己的单人乐队了。只有一个要求:成为一只自由自在的飞鸟!

参考资料

学习

获得产品和技术

  • IBM 试用软件:从 developerWorks 可直接下载这些试用软件,您可以利用它们开发您的下一个项目。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=220881
ArticleTitle=对话 UNIX,第 8 部分: UNIX 进程
publish-date=05162007