4.1. 由内而外的进程

4.1.1. 多用户和多任务

现在我们已经更加熟悉我们的环境,并且能够与我们的系统进行一些沟通,现在是时候更详细地研究我们可以启动的进程了。并非每个命令都会启动单个进程。有些命令会启动一系列进程,例如 mozilla;而另一些命令,例如 ls,则作为单个命令执行。

此外,Linux 基于 UNIX,在 UNIX 中,常见的策略是让多个用户在同一系统上同时运行多个命令。显然,必须采取措施让 CPU 管理所有这些进程,并且必须提供功能以便用户可以在进程之间切换。在某些情况下,即使启动进程的用户注销,进程也必须继续运行。用户需要一种重新激活中断进程的方法。

我们将在接下来的章节中解释 Linux 进程的结构。

4.1.2. 进程类型

4.1.2.1. 交互式进程

交互式进程通过终端会话初始化和控制。换句话说,必须有人连接到系统才能启动这些进程;它们不会作为系统功能的一部分自动启动。这些进程可以在前台运行,占用启动程序的终端,并且只要此进程在前台运行,您就无法启动其他应用程序。或者,它们可以在后台运行,以便您启动程序的终端可以在程序运行时接受新命令。到目前为止,我们主要关注在前台运行的程序——运行它们所花费的时间太短以至于无法注意到——但是使用 less 命令查看文件是占用终端会话的一个很好的例子。在这种情况下,激活的程序正在等待您执行某些操作。该程序仍然连接到启动它的终端,并且该终端仅对输入此程序可以理解的命令有用。其他命令只会导致错误或系统无响应。

但是,当进程在后台运行时,用户不会被阻止在启动程序的终端中执行其他操作。

shell 提供了一个名为作业控制的功能,可以轻松处理多个进程。此机制在前台和后台之间切换进程。使用此系统,程序也可以立即在后台启动。

在后台运行进程仅对不需要用户输入(通过 shell)的程序有用。当预计作业执行需要很长时间时,通常会将作业置于后台。为了在输入命令后释放发出终端,添加一个尾随的 &。在示例中,在使用图形模式时,我们从现有终端窗口打开一个额外的终端窗口

billy:~> xterm &
[1] 26558

billy:~> jobs
[1]+  Running                 xterm &

完整的作业控制功能在 bash Info 页面中详细解释,因此此处仅列出常用的作业控制应用程序

表 4-1. 控制进程

(部分)命令含义
regular_command在前台运行此命令。
command &在后台运行此命令(释放终端)
jobs显示在后台运行的命令。
Ctrl+Z挂起(停止,但不退出)在前台运行的进程(挂起)。
Ctrl+C中断(终止并退出)在前台运行的进程。
%n每个在后台运行的进程都会被分配一个数字。通过使用 % 表达式,可以使用其编号来引用作业,例如 fg %2
bg在后台重新激活挂起的程序。
fg将作业放回前台。
kill结束进程(另请参阅 bash 的 Info 页面中的 Shell 内置命令)

更多实际示例可以在练习中找到。

大多数 UNIX 系统很可能能够运行 screen,当您实际上想要另一个 shell 来执行命令时,这非常有用。调用 screen 后,将创建一个新会话,其中包含指定的 shell 和/或命令,然后您可以将其移开。在这个新会话中,您可以执行您想做的任何事情。所有程序和操作都将独立于发出 shell 运行。然后,您可以分离此会话,而您在其中启动的程序将继续运行,即使您注销原始 shell,并且您可以随时重新启动您的 screen

该程序起源于虚拟控制台尚未发明的时代,并且所有事情都需要使用一个文本终端完成。对于爱好者来说,即使我们已经拥有虚拟控制台将近十年了,它在 Linux 中仍然具有意义。

4.1.2.2. 自动进程

自动或批处理进程未连接到终端。相反,这些是可以排队到假脱机区域的任务,它们在假脱机区域中等待按 FIFO(先进先出)的原则执行。可以使用以下两个标准之一执行此类任务

  • 在特定的日期和时间:使用 at 命令完成,我们将在本章的第二部分中讨论该命令。

  • 在系统总负载足够低以接受额外作业时:使用 batch 命令完成。默认情况下,任务会放入队列中,它们在队列中等待执行,直到系统负载低于 0.8。在大型环境中,当必须处理大量数据或必须在已加载的系统上执行需要大量系统资源的任务时,系统管理员可能更喜欢批处理。批处理也用于优化系统性能。

4.1.2.3. 守护进程

守护进程是持续运行的服务器进程。大多数时候,它们在系统启动时初始化,然后在后台等待直到需要它们的服务。一个典型的例子是网络守护进程 xinetd,它几乎在每个启动过程中都会启动。系统启动后,网络守护进程只是坐着等待,直到客户端程序(例如 FTP 客户端)需要连接。

4.1.3. 进程属性

进程具有一系列特征,可以使用 ps 命令查看

4.1.4. 显示进程信息

ps 命令是用于可视化进程的工具之一。此命令有多个选项可以组合使用以显示不同的进程属性。

在未指定任何选项的情况下,ps 仅提供有关当前 shell 和最终进程的信息

theo:~> ps
  PID TTY          TIME CMD
 4245 pts/7    00:00:00 bash
 5314 pts/7    00:00:00 ps

由于这没有提供足够的信息 - 通常,您的系统上至少运行着数百个进程 - 我们通常会从所有进程列表中选择特定的进程,在管道中使用 grep 命令,请参阅 第 5.1.2.1 节,如本行所示,它将选择并显示特定用户拥有的所有进程

ps-ef| grepusername

此示例显示了进程名称为 bash 的所有进程,bash 是 Linux 系统上最常见的登录 shell

theo:> ps auxw | grep bash
brenda   31970  0.0  0.3  6080 1556 tty2   S  Feb23   0:00 -bash
root     32043  0.0  0.3  6112 1600 tty4   S  Feb23   0:00 -bash
theo     32581  0.0  0.3  6384 1864 pts/1  S  Feb23   0:00 bash
theo     32616  0.0  0.3  6396 1896 pts/2  S  Feb23   0:00 bash
theo     32629  0.0  0.3  6380 1856 pts/3  S  Feb23   0:00 bash
theo      2214  0.0  0.3  6412 1944 pts/5  S  16:18   0:02 bash
theo      4245  0.0  0.3  6392 1888 pts/7  S  17:26   0:00 bash
theo      5427  0.0  0.1  3720  548 pts/7  S  19:22   0:00 grep bash

在这些情况下,在具有大量空闲时间的系统上,通常也会显示查找包含字符串 bash 的行的 grep 命令。如果您不希望发生这种情况,请使用 pgrep 命令。

Bash shell 是一个特例:此进程列表还显示了哪些是登录 shell(您必须在其中提供用户名和密码,例如当您以文本模式登录或进行远程登录时,与非登录 shell 相对,例如通过单击终端窗口图标启动)。此类登录 shell 以破折号 (-) 开头。

Note|?
 

我们将在下一章中解释 | 运算符,请参阅第 5 章

更多信息可以通过常用方式找到:ps --helpman ps。GNU ps 支持不同样式的选项格式;以上示例不包含错误。

请注意,ps 仅给出活动进程的瞬时状态,它是一次性记录。top 程序通过每五秒更新一次 ps(带有一堆选项)给出的结果来显示更精确的视图,定期生成导致最重负载的进程的新列表,同时整合有关正在使用的交换空间和 CPU 状态的更多信息,从proc文件系统

 12:40pm up 9 days, 6:00, 4 users, load average: 0.21, 0.11, 0.03
89 processes: 86 sleeping, 3 running, 0 zombie, 0 stopped
CPU states:  2.5% user,  1.7% system,  0.0% nice, 95.6% idle
Mem:   255120K av, 239412K used, 15708K free, 756K shrd, 22620K buff
Swap: 1050176K av, 76428K used, 973748K free, 82756K cached

  PID USER  PRI NI SIZE  RSS SHARE STAT %CPU %MEM TIME COMMAND
 5005 root  14  0 91572  15M 11580 R    1.9  6.0  7:53 X
19599 jeff  14  0  1024 1024   796 R    1.1  0.4  0:01 top
19100 jeff   9  0  5288 4948  3888 R    0.5  1.9  0:24 gnome-terminal
19328 jeff   9  0 37884  36M 14724 S    0.5 14.8  1:30 mozilla-bin
    1 root   8  0   516  472   464 S    0.0  0.1  0:06 init
    2 root   9  0     0    0     0 SW   0.0  0.0  0:02 keventd
    3 root   9  0     0    0     0 SW   0.0  0.0  0:00 kapm-idled
    4 root  19 19     0    0     0 SWN  0.0  0.0  0:00 ksoftirqd_CPU0
    5 root   9  0     0    0     0 SW   0.0  0.0  0:33 kswapd
    6 root   9  0     0    0     0 SW   0.0  0.0  0:00 kreclaimd
    7 root   9  0     0    0     0 SW   0.0  0.0  0:00 bdflush
    8 root   9  0     0    0     0 SW   0.0  0.0  0:05 kupdated
    9 root  -1-20     0    0     0 SW<  0.0  0.0  0:00 mdrecoveryd
   13 root   9  0     0    0     0 SW   0.0  0.0  0:01 kjournald
   89 root   9  0     0    0     0 SW   0.0  0.0  0:00 khubd
  219 root   9  0     0    0     0 SW   0.0  0.0  0:00 kjournald
  220 root   9  0     0    0     0 SW   0.0  0.0  0:00 kjournald

top 的第一行包含与 uptime 命令显示的信息相同的信息

jeff:~> uptime
  3:30pm, up 12 days, 23:29, 6 users, load average: 0.01, 0.02, 0.00

这些程序的数据存储在其他位置,包括/var/run/utmp(有关当前连接用户的信息)和虚拟文件系统中/proc,例如/proc/loadavg(平均负载信息)。有各种图形应用程序可以查看此数据,例如 Gnome System Monitorlavaps。在 FreshMeatSourceForge 上,您会找到数十个应用程序,它们将此信息与来自一台(Web)服务器上的多台服务器的其他服务器数据和日志集中在一起,从而可以从一个工作站监控整个 IT 基础设施。

进程之间的关系可以使用 pstree 命令可视化

sophie:~> pstree
init-+-amd
     |-apmd
     |-2*[artsd]
     |-atd
     |-crond
     |-deskguide_apple
     |-eth0
     |-gdm---gdm-+-X
     |           `-gnome-session-+-Gnome
     |                           |-ssh-agent
     |                           `-true
     |-geyes_applet
     |-gkb_applet
     |-gnome-name-serv
     |-gnome-smproxy
     |-gnome-terminal-+-bash---vim
     |                |-bash
     |                |-bash---pstree
     |                |-bash---ssh
     |                |-bash---mozilla-bin---mozilla-bin---3*[mozilla-bin]
     |                `-gnome-pty-helper
     |-gpm
     |-gweather
     |-kapm-idled
     |-3*[kdeinit]
     |-keventd
     |-khubd
     |-5*[kjournald]
     |-klogd
     |-lockd---rpciod
     |-lpd
     |-mdrecoveryd
     |-6*[mingetty]
     |-8*[nfsd]
     |-nscd---nscd---5*[nscd]
     |-ntpd
     |-3*[oafd]
     |-panel
     |-portmap
     |-rhnsd
     |-rpc.mountd
     |-rpc.rquotad
     |-rpc.statd
     |-sawfish
     |-screenshooter_a
     |-sendmail
     |-sshd---sshd---bash---su---bash
     |-syslogd
     |-tasklist_applet
     |-vmnet-bridge
     |-xfs
     `-xinetd-ipv6

The-uand-a选项提供附加信息。有关更多选项及其作用,请参阅 Info 页面。

在下一节中,我们将看到一个进程如何创建另一个进程。

4.1.5. 进程的生命周期

4.1.5.1. 进程创建

新进程的创建是因为现有进程创建了自身的精确副本。此子进程与父进程具有相同的环境,只是进程 ID 号不同。此过程称为forking(派生)。

在派生过程之后,子进程的地址空间被新进程数据覆盖。这是通过对系统的 exec 调用完成的。

因此,fork-and-exec 机制将旧命令切换为新命令,而新程序在其中执行的环境保持不变,包括输入和输出设备的配置、环境变量和优先级。此机制用于创建所有 UNIX 进程,因此也适用于 Linux 操作系统。即使是第一个进程 init(进程 ID 为 1)也是在所谓的引导过程中在启动过程中派生的。

此方案说明了 fork-and-exec 机制。进程 ID 在派生过程后更改

图 4-1. Fork-and-exec 机制

在某些情况下,init 成为进程的父进程,而该进程不是由 init 启动的,正如我们在 pstree 示例中已经看到的那样。例如,许多程序守护进程化它们的子进程,以便它们可以在父进程停止或被停止时继续运行。窗口管理器是一个典型的例子;它启动一个 xterm 进程,该进程生成一个接受命令的 shell。然后,窗口管理器拒绝承担任何进一步的责任,并将子进程传递给 init。使用此机制,可以在不中断正在运行的应用程序的情况下更改窗口管理器。

偶尔,即使在良好的家庭中,事情也会出错。在特殊情况下,进程可能会在父进程不等待此进程完成的情况下完成。这种未埋葬的进程称为僵尸进程。

4.1.5.2. 结束进程

当进程正常结束时(它没有被杀死或以其他方式意外中断),程序会将其退出状态返回给父进程。此退出状态是程序返回的数字,用于提供程序执行的结果。在执行作业时返回信息的系统起源于编写 UNIX 的 C 编程语言。

然后,父进程或脚本可以解释返回代码。返回代码的值是特定于程序的。此信息通常可以在指定程序的 man 页面中找到,例如 grep 命令返回-1如果没有找到匹配项,则会打印类似 “No files found” 的消息。另一个例子是 Bash 内置命令 true,它除了返回退出状态 0(表示成功)之外,什么也不做。

4.1.5.3. 信号

进程因收到信号而结束。您可以向进程发送多个信号。使用 kill 命令向进程发送信号。命令 kill -l 显示信号列表。大多数信号供系统内部使用,或供程序员编写代码时使用。作为用户,您将需要以下信号

表 4-2. 常用信号

信号名称信号编号含义
SIGTERM15以有序的方式终止进程。
SIGINT2中断进程。进程可以忽略此信号。
SIGKILL9中断进程。进程无法忽略此信号。
SIGHUP1对于守护进程:重新读取配置文件。

您可以在 man 7 signal 中阅读有关向进程发送信号时采取的默认操作的更多信息。

4.1.6. SUID 和 SGID

正如上一章中所承诺的,我们现在将更详细地讨论特殊模式 SUID 和 SGID。这些模式的存在是为了让普通用户能够执行他们通常无法执行的任务,因为基于 UNIX 的系统上使用了严格的文件权限方案。在理想情况下,特殊模式应尽可能少地使用,因为它们包含安全风险。Linux 开发人员通常会尽可能避免使用它们。例如,Linux ps 版本使用存储在/proc文件系统中的信息,每个人都可以访问该信息,从而避免将敏感系统数据和资源暴露给公众。在此之前,在较旧的 UNIX 系统上,ps 程序需要访问诸如/dev/memand/dev/kmem之类的文件,由于这些文件的权限和所有权,这存在缺点

rita:~> ls -l /dev/*mem
crw-r-----    1 root     kmem       1,   2 Aug 30 22:30 /dev/kmem
crw-r-----    1 root     kmem       1,   1 Aug 30 22:30 /dev/mem

对于旧版本的 ps,除非对其应用特殊模式,否则普通用户无法启动该程序。

虽然我们通常尝试避免应用任何特殊模式,但在某些情况下,有必要使用 SUID。一个例子是更改密码的机制。当然,用户希望自己执行此操作,而不是让系统管理员设置他们的密码。我们知道,用户名和密码列在/etc/passwd文件中,该文件具有以下访问权限和所有者

bea:~> ls -l /etc/passwd
-rw-r--r--    1 root     root     1267 Jan 16 14:43 /etc/passwd

但是,用户需要能够在此文件中更改自己的信息。这是通过为 passwd 程序提供特殊权限来实现的

mia:~> which passwd
passwd is /usr/bin/passwd

mia:~> ls -l /usr/bin/passwd
-r-s--x--x    1 root     root    13476 Aug  7 06:03 /usr/bin/passwd*

调用时,passwd 命令将使用 root 的访问权限运行,从而使普通用户能够编辑系统管理员拥有的密码文件。

文件上的 SGID 模式不如 SUID 频繁出现,因为 SGID 通常涉及创建额外的组。但是,在某些情况下,我们必须经历这种麻烦才能构建一个优雅的解决方案(不要对此过于担心 - 必要的组通常在安装时创建)。writewall 程序就是这种情况,它们用于向其他用户的终端 (tty) 发送消息。write 命令向单个用户写入消息,而 wall 命令向所有连接的用户写入消息。

通常不允许将文本发送到另一个用户的终端或图形显示器。为了绕过这个问题,创建了一个组,该组拥有所有终端设备。当授予 writewall 命令 SGID 权限时,这些命令将使用适用于此组的访问权限运行,在本例中为 tty。由于此组对目标终端具有写入权限,因此即使是没有权限以任何方式使用该终端的用户也可以向其发送消息。

在下面的示例中,用户 joe 首先使用 who 命令查明他的通信员连接到哪个终端。然后,他使用 write 命令向她发送消息。还说明了 write 程序和接收用户占用的终端的访问权限:很明显,除了组所有者之外,其他用户对该设备没有任何权限,组所有者可以写入该设备。

joe:~> which write
write is /usr/bin/write

joe:~> ls -l /usr/bin/write
-rwxr-sr-x    1 root     tty      8744 Dec  5 00:55 /usr/bin/write*

joe:~> who
jenny     tty1     Jan 23 11:41
jenny     pts/1    Jan 23 12:21 (:0)
jenny     pts/2    Jan 23 12:22 (:0)
jenny     pts/3    Jan 23 12:22 (:0)
joe       pts/0    Jan 20 10:13 (lo.callhost.org)

joe:~> ls -l /dev/tty1
crw--w----    1 jenny   tty  4,     1 Jan 23 11:41 /dev/tty1

joe:~> write jenny tty1
hey Jenny, shall we have lunch together?
^C

用户 jenny 在她的屏幕上得到这个

Message from joe@lo.callhost.org on ptys/1 at 12:36 ...
hey Jenny, shall we have lunch together?
EOF

收到消息后,可以使用 Ctrl+L 组合键清除终端。为了不接收任何消息(系统管理员的消息除外),请使用 mesg 命令。要查看哪些连接的用户接受来自其他用户的消息,请使用 who -w。所有功能都在每个命令的 Info 页面中完整解释。

Note组名称可能有所不同
 

组方案特定于发行版。其他发行版可能使用其他名称或其他解决方案。