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

在最近的街头游乐会上 , 有一个单人乐队让我很是着迷 。的确 , 这让我很开心 , 还给我留下了深刻印象 。这个单人乐队的唯一成员利用嘴、大腿、膝盖和脚分别控制口琴、五弦琴、钹和脚鼓 , 生动地演奏了齐柏林飞船乐队的《天堂的阶梯》 , 他演奏的贝多芬《第五交响曲》也颇为动人 。和他相比 , 我能一边拍脑袋一边摸肚子就觉得很不错了 。(或者是一边拍肚子一边摸脑袋 。)
对您来说 , 幸运的是 , Unix?操作系统更像是那个单人乐队 , 而不是像我这个笨手笨脚的专栏作家 。UNIX 特别擅长同时处理多个任务 , 并安排它们访问系统中的有限资源(内存、设备和 CPU) 。打个比方 , UNIX 可以一边散步 , 一边嚼口香糖 。
这个月我们研究的内容要比平常更深入一些 , 我们会看看 UNIX 是如何同时做这么多事的 。这次我们还会探索 shell 的内部 , 了解工作控制命令 , 如 Ctrl C(终止)和 Ctrl Z(挂起)是怎样实现的 。
一个真正的多任务系统
在 UNIX(以及大多数现代操作系统 , 包括 Microsoft?Windowsac OS X、FreeBSD 和 Linux┲校扛黾扑闳挝穸际怯梢桓鼋瘫硎镜摹NIX 似乎能同时运行很多任务 , 这是因为每个进程都会轮流(从概念上来讲)分到一小片 CPU 时间 。
一个进程就像一个容器 , 它与某个正在运行的应用程序、环境变量、应用程序的输入和输出 , 以及进程的状态(包括其优先级和累计资源使用情况)捆绑在一起 。图 1 显示了一个进程 。
图 1. UNIX 进程的概念化模型
为了便于理解 , 您可以把一个进程想像成一个独立的国家 , 有边界、资源 , 还有国民生产总值 。
每个进程还有一个所有者 。一般来说 , 您启动的任务(如您的 shell 和命令)的所有者就是您 。系统服务的所有者可能是特殊用户或超级用户 root 。例如 , 为了增强安全性 , Apache HTTP Server 的所有者一般是一个名为 www 的专用用户 , 该用户能提供 Web 服务器所需的的文件访问权限 , 但不包含其他权限 。
进程的所有权可能会改变 , 但必须严格保持其独占性 。一个进程在任何时候都只能有一个所有者 。
最后 , 每个进程都具有权限 。一般来说 , 进程的权限与其所有者的权限是相称的 。(例如 , 如果您无法在命令行 Shell 中访问某个特定文件 , 则您从 Shell 中启动的程序也会继承同样的限制 。)这一继承规则有一个例外情况 , 即应用程序启用了特殊的 setuid 或 setgid 位 , 如 ls 显示的那样 , 在此情况下 , 某个进程可能会获得比其所有者更高的权限 。
setuid 位可以使用 chmod u s 进行设置 。setuid 的权限如下所示:
$ ls -l /usr/bin/top
-rwsr-xr-x 1 root wheel 83088 Mar 20 2005 topsetgid 位可以使用 chmod g s 设置:
$ ls -l /usr/bin/top
-r-xr-sr-x1 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 -bashS 09:57 00:00:00
14937 mstreic ps --user mstrei R 10:23 00:00:00-o 根据各列名称的顺序对输出进行格式化 。pid、uname 和 command 分别指进程 ID、用户名和命令 。state 代表进程的状态 , 如正在睡眠 (S) 或运行 (R) 。(稍后将对进程状态进行更详细的说明 。)stime 显示命令的开始时间 , time 则显示该进程占用了多少 CPU 时间 。
进程从哪里来?
在 Unix 中 , 某些进程会从系统启动到关机的时间里一直运行 , 但大多数进程都会随任务的开始和完成而迅速地出现和消失 。有时 , 某个进程可能会“早夭“ , 甚至会“暴死(比如在系统崩溃时) 。新的进程是从哪里来的呢?
每个新的 UNIX 进程都是某个现有进程的产物 。另外 , 每个新进程(不妨将其称为“子进程)是对“父进程的克隆体(至少有一瞬间是如此) , 直到“子进程继续独立执行为止 。(如果每个进程都是某个现有进程的后代 , 那么不免会有一个疑问:“第一个进程是从哪里来的?请参阅下面的侧栏以寻找答案 。)
鸡和蛋 某些争论是经久不息的:生存还是毁灭?可口可乐还是百事可乐?PC 还是 Mac?当然 , 还有一个古老的悖论 , “鸡生蛋 , 还是蛋生鸡?
【对话 UNIX,第 8 部分: UNIX 进程】如果每个新的 UNIX 进程都是某个现有的、正在运行的进程的后代 , 那么第一个进程是从哪里来的?答案是:UNIX 内核在系统启动序列中产生了第一个进程 。
第一个进程被恰如其分地称为 init , 所有其他系统进程的亲缘关系最终都可以追溯到 init 。实际上 , init 的进程编号是 1 。如果您要查看 init 的状态 , 可键入 ps -l 1:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S010 0 680 -373 select ?0:02 init [2]正如您所看到的 , init 的所有者 (UID) 是 0 (root) 。和系统中所有其他进程不同的是 , init 没有父进程 , 它的父进程 ID (PPID) 为 0 。图 1-4 详细说明了进程的产生过程:
在图 2 和图 3 中 , 进程 A (Process A) , 正在运行一个由蓝色方框表示的程序 。它运行编号为 10 , 11 , 12…的指令 。进程 A 有属于自己的数据、程序的副本、打开的文件集 , 以及自己的环境变量集 , 当进程 A 刚出现时 , 会对它们进行初次捕捉 。
图 2. 进程 A 运行代码
在 Unix 中 , fork() 系统调用(之所以有这个名称 , 是因为它是一个调用或请求 , 要求操作系统进行协助)被用来产生新的进程 。当程序 A (Program A) 执行指令 13 (Instruction 13) fork() 时 , 系统会立即创建进程 A 的一个精确克隆版本 , 并将其命名为进程 Z (Process Z) 。Z 具有和 A 相同的环境变量、相同的内存内容、相同的程序状态 , 打开的文件也一样 。图 3 显示的是进程 A 生成进程 Z后 , 进程 A 和 Z 的状态 。
图 3. 进程 A 生成自身的克隆体
起初 , 进程 Z 是从进程 A 停止的地方开始执行的 。也就是说 , 此后进程 Z 从指令 14 (Instruction 14) 处开始执行 。进程 A 会在同一指令位置继续执行 。
一般来说 , 指令 14 处的编程逻辑将测试当前的进程是子进程还是父进程 , 也就是说 , 进程 Z 和进程 A 中的指令 14 分别判定这两个进程是否为其他进程的后代或祖先 。为了以示区别 , fork() 系统调用在子进程中返回 0 , 但返回给父进程的却是进程 Z 的进程 ID 。
在上次测试之后 , 进程 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 -bashS 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 -bashS 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]- Donesleep 10
[9]Donesleep 10
$向遥远的进程池前进
某些进程会一直存活(如 init) , 而某些进程会以新的形式重生(如您的 shell) 。最终大多进程都会因自然原因(即程序运行结束)而消亡 。
此外 , 您还可以将某个进程放在一个挂起的动作序列中 , 等待被再次激活 。正如先前的示例所示 , 您可以用 kill 提前终止某个进程 。
当某个命令在前台运行时 , 如果您希望将它挂起 , 请按 CtrlZ:
$ 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:00Shell 已将命令挂起 , 为了方便起见 , 还为它分配了一个标签 。您可以像先前那样使用这个标签 , 以终止工作或让工作返回前台 。您还可以使用 bg 命令在后台恢复这个进程:
bg %1
[1]sleep 10 &当某个命令在前台运行时 , 如果您想终止它 , 请按 CtrlC:
$ 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 , 重新激活进程 , 该进程将从上次停止的地方继续执行 。
也就是说 , 每次您按 CtrlZ 时 , 您的 shell 将向前台发送 SIGSTOP 信号 。bg 命令发送 SIGCONT 。而 CtrlC 则会发送 SIGTERM , 要求立即终止进程 。
一些信号可以被某个进程阻塞 , 应用程序可以通过设计 , 显式地“捕捉 (catch)信号 , 并以一种特殊的方式对每个事件作出反应 。例如 , 系统服务 xinetd 会按需要启动其他网络服务 , 它在收到 SIGHUP 时会重新读取它的配置文件 。在 Linux 中 , 向 init 发送信号 , 可能会改变系统的运行级别 , 甚至会导致系统关闭 。.(这里有一个问题:kill %1 和 kill 1 有什么区别?
进程甚至可以给自己发送信号 。想像一下 , 您正在编写一个游戏 , 想留给用户五秒钟时间作出反应 。您的代码可以设置一个五秒钟的定时器 , 接下来继续进行重绘屏幕等操作 。当定时器的时间耗尽后 , 将有一个 SIGALRM 信号被送回您的进程 。呯!时间到!
(这里提供了问题的答案:kill %1 会终止标签为 1 的后台工作 。kill 1 会终止 init , 当必须关闭计算机时 , 将向操作系统发送这个信号 。) 在特殊情况下 , 操作系统还可以将一些其他信号传送给进程 。内存违例会引发 SIGSEGV 信号 , 立即终止进程 , 并留下一个内核转储 。有一个特殊的信号 SIGKILL 是无法被阻塞或捕捉的 , 它会立即终止某个进程 。
和 Unix 中许多其他资源一样 , 您只能向您拥有的进程发送信号 。这可以防止您终止重要的系统服务和其他用户的进程 。超级用户 root 可以向任何进程发送信号 。
更多魔法揭密
UNIX 有许多可活动的部分 。它有系统服务、设备、内存管理器等等 。好在这些复杂的花样大都被隐藏起来 , 不会被看到 , 或可以通过用户界面(如 shell 或窗口工具)很方便地使用 。更妙的是 , 如果您想深入探究 , 随时都可以使用 top, ps 和 kill 等专用工具 。
现在您已经知道了进程的工作原理 , 可以组成自己的单人乐队了 。只有一个要求:成为一只自由自在的飞鸟!