对话 UNIX,第 8 部分: UNIX 进程
在最近的街头游乐会上,有一个单人乐队让我很是着迷。的确,这让我很开心,还给我留下了深刻印象。这个单人乐队的唯一成员利用嘴、大腿、膝盖和脚分别控制口琴、五弦琴、钹和脚鼓,生动地演奏了齐柏林飞船乐队的《天堂的阶梯》,他演奏的贝多芬《第五交响曲》也颇为动人。和他相比,我能一边拍脑袋一边摸肚子就觉得很不错了。(或者是一边拍肚子一边摸脑袋。)
对您来说,幸运的是,Unix® 操作系统更像是那个单人乐队,而不是像我这个笨手笨脚的专栏作家。UNIX 特别擅长同时处理多个任务,并安排它们访问系统中的有限资源(内存、设备和 CPU)。打个比方,UNIX 可以一边散步,一边嚼口香糖。
这个月我们研究的内容要比平常更深入一些,我们会看看 UNIX 是如何同时做这么多事的。这次我们还会探索 shell 的内部,了解工作控制命令,如 Ctrl+C(终止)和 Ctrl+Z(挂起)是怎样实现的。
一个真正的多任务系统
在 UNIX(以及大多数现代操作系统,包括 Microsoft® Windows®、Mac OS X、FreeBSD 和 Linux®)中,每个计算任务都是由一个进程表示的。UNIX 似乎能同时运行很多任务,这是因为每个进程都会轮流(从概念上来讲)分到一小片 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 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 TIME14138 mstreic sshd: mstreicher S 09:57 00:00:0014139 mstreic -bashS 09:57 00:00:0014937 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 进程都是某个现有的、正在运行的进程的后代,那么第一个进程是从哪里来的?答案是:UNIX 内核在系统启动序列中产生了第一个进程。
第一个进程被恰如其分地称为 init,所有其他系统进程的亲缘关系最终都可以追溯到 init。实际上,init 的进程编号是 1。如果您要查看 init 的状态,可键入 ps -l 1:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD4 S 0 10 0 68 0 - 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 TIME16351 16350 mstreic -bashS 11:23 00:00:0016364 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 TIME16351 16350 mstreic -bashS 11:23 00:00:0016843 16351 mstreic sleep 10 S 11:42 00:00:0016844 16351 mstreic sleep 10 S 11:42 00:00:0016845 16351 mstreic sleep 10 S 11:42 00:00:0016846 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 %8sleep 10
从命令行中同时异步运行多个命令,是处理您自己的任务集的好方法。一个长时间运行的工作(例如,系统管理的数值计算或大型程序的编译)最适合放在后台。为了捕获每个后台命令的输出,请考虑使用重定向操作符 >、>&、>> 和 >>&,将输入重定向到某个文件。当后台命令结束后,shell 会在下一个提示符之前显示一条警告消息:
$ whoamimstreicher[8]- Donesleep 10[9]+ Donesleep 10$
向遥远的进程池前进
某些进程会一直存活(如 init),而某些进程会以新的形式重生(如您的 shell)。最终大多进程都会因自然原因(即程序运行结束)而消亡。
此外,您还可以将某个进程放在一个挂起的动作序列中,等待被再次激活。正如先前的示例所示,您可以用 kill 提前终止某个进程。
当某个命令在前台运行时,如果您希望将它挂起,请按 Ctrl + Z:
$ sleep 10(Press Control-Z)[1]+ Stopped sleep 10$ ps PID PPID USER COMMAND S STIME TIME18195 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 %1 和 kill 1 有什么区别?
进程甚至可以给自己发送信号。想像一下,您正在编写一个游戏,想留给用户五秒钟时间作出反应。您的代码可以设置一个五秒钟的定时器,接下来继续进行重绘屏幕等操作。当定时器的时间耗尽后,将有一个 SIGALRM 信号被送回您的进程。呯!时间到!
(这里提供了问题的答案:kill %1 会终止标签为 1 的后台工作。kill 1 会终止 init,当必须关闭计算机时,将向操作系统发送这个信号。)
在特殊情况下,操作系统还可以将一些其他信号传送给进程。内存违例会引发 SIGSEGV 信号,立即终止进程,并留下一个内核转储。有一个特殊的信号 SIGKILL 是无法被阻塞或捕捉的,它会立即终止某个进程。
和 Unix 中许多其他资源一样,您只能向您拥有的进程发送信号。这可以防止您终止重要的系统服务和其他用户的进程。超级用户 root 可以向任何进程发送信号。
更多魔法揭密
UNIX 有许多可活动的部分。它有系统服务、设备、内存管理器等等。好在这些复杂的花样大都被隐藏起来,不会被看到,或可以通过用户界面(如 shell 或窗口工具)很方便地使用。更妙的是,如果您想深入探究,随时都可以使用 top, ps 和 kill 等专用工具。
现在您已经知道了进程的工作原理,可以组成自己的单人乐队了。只有一个要求:成为一只自由自在的飞鸟!
相关文章: