Bourne Shell 脚本/环境
没有哪个程序是孤立存在的。即使是Bourne Shell也不例外。每个程序都在一个环境中执行,这是一个控制程序执行方式以及程序拥有和可以建立的外部连接的资源系统。并且程序本身可以在其中进行更改。
在本模块中,我们将讨论环境,即每个程序和命令生存和执行的栖息地。我们将了解环境由什么组成,它来自哪里以及它将去往哪里……并且我们将讨论Shell用于传递数据的最重要的机制:环境变量。
在讨论Unix Shell时,你经常会遇到“环境”这个术语。这个术语用于描述程序执行的上下文,通常是指一组“环境变量”(我们很快就会讲到这些)。但实际上,有两个不同的术语在某种程度上是程序的环境,并且在“环境”中经常被混淆在一起。其中较简单的一个实际上是环境变量的集合,实际上被称为“环境”。第二个是影响程序执行的一组更广泛的资源,称为命令执行环境。
每个正在运行的程序,无论是用户从Shell中直接启动的,还是由另一个进程间接启动的,都在其命令执行环境(CEE)中的一组全局资源中运行。
程序的CEE包含重要信息,例如程序可以操作的数据的源和目标(也称为标准输入、标准输出和标准错误句柄)。此外,还定义了变量,这些变量列出了启动程序的用户或进程的身份和主目录、机器的主机名以及用于启动程序的终端类型。还有其他变量,但这只是一些主要的变量。环境还为程序提供了工作空间,以及一种与将在同一环境中运行的其他未来程序进行通信的简单方法。
Shell的CEE中包含的完整资源列表为
- 在启动Shell的父进程中持有的打开的文件。这些文件是继承的。此文件列表包括通过重定向访问的文件(例如标准输入、输出和错误文件)。
- 当前工作目录:Shell的“当前”目录。
- 文件创建模式:创建新文件时设置的文件权限的默认集。
- 活动的陷阱。
- 在调用Shell期间设置或从父进程继承的Shell参数和变量。
- 从父进程继承的Shell函数。
- 由set或shopts设置的Shell选项,或作为Shell可执行文件的命令行选项。
- Shell别名(如果你的Shell中可用)。
- Shell的进程ID以及父进程启动的一些进程的进程ID。
每当Shell执行启动子进程的命令时,该命令将在其自己的CEE中执行。此CEE继承其父级的CEE的一部分副本,但不是整个父级CEE。继承的副本包括
- 打开的文件。
- 工作目录。
- 文件创建模式掩码。
- 标记为导出到子进程的任何Shell变量和函数。
- Shell设置的陷阱。
“set”命令允许你设置或禁用CEE的一部分并影响Shell行为的许多选项。要设置选项,请使用“-”后跟一个或多个标志作为命令行参数调用set。要禁用该选项,请使用“+”然后是相同的标志调用set。你可能不会经常使用这些选项;“set”最常见的用法是在没有任何参数的情况下调用它,这将生成环境中所有已定义名称(变量和函数)的列表。以下是一些你可能会用到的选项
- +/-a
- 设置后,自动将所有新创建或重新定义的变量标记为导出。
- +/-f
- 设置后,忽略文件名元字符。
- +/-n
- 设置后,仅读取命令但不执行它们。
- +/-v
- 设置后,导致Shell在从输入读取时打印命令(详细调试标志)。
- +/-x
- 设置后,导致Shell在执行命令时打印命令(调试标志)。
同样,你可能主要是在没有参数的情况下使用set,以检查已定义变量的列表。
CEE的一部分是简单称为环境的东西。环境是名为环境变量的名称/值对的集合。从技术上讲,这些变量还包含Shell函数,但我们将在单独的模块中讨论这些函数。
环境变量是环境中的一块带标签的存储空间,你可以在其中存储任何你想要的东西,只要它适合即可。这些空间被称为变量,因为你可以改变你放入其中的内容。你只需要知道用于存储内容的名称(标签)。Bourne Shell也使用这些“环境变量”。你可以编写检查这些变量的脚本,并且这些脚本可以根据存储在变量中的值做出决策。
环境变量是以下形式的名称/值对
这也是创建变量的方式。有多种使用变量的方法,我们将在关于替换的模块中讨论,但现在我们将限制自己使用简单的方法:如果你在变量名前面加上一个$-字符,Shell将用变量的值替换该变量。例如
正如您从上面的示例中看到的,环境变量有点像公告板:任何人都可以在那里发布任何类型的数值,供所有人读取(只要他们有权访问该公告板)。并且任何发布的值都可以被任何读者以他们喜欢的任何方式解释。这使得环境变量成为在不同位置之间传递数据的一种非常通用的机制。因此,环境变量用于各种用途。例如,用于设置程序在执行过程中可以使用的全局参数。或者从一个 shell 脚本中设置一个值,以便另一个脚本获取。Shell 本身在其配置中也使用许多环境变量。一些典型的示例
- IFS
- 此变量列出了 shell 认为是空白字符的字符。
- PATH
- 此变量被解释为目录列表(在 Unix 系统上由冒号分隔)。每当您键入可执行文件的名称以供 shell 执行但未包含该可执行文件的完整路径时,shell 将按顺序查看所有这些目录以查找可执行文件。
- PS1
- 此变量列出了一些代码。这些代码指示您的 shell 交互式 shell 中的命令行提示符应该是什么样子。
- PWD
- 此变量的值始终是工作目录的路径。
如上所述,环境变量的绝对优点在于它们只包含随机的字符字符串,没有直接的含义。任何变量的含义都由读取该变量的任何程序或进程来解释。因此,一个变量可以保存任何类型的信息,并且可以在任何地方使用。例如,考虑以下示例
$ echo $CMD
$ CMD=ls
$ echo $CMD
ls
$ $CMD
bin booktemp Documents Mail mbox public_html sent
将变量设置为可执行文件的名称,然后通过将变量作为命令来执行该可执行文件,这没有任何问题。
尽管您以相同的方式使用所有环境变量,但有几种不同的变量类型。在本节中,我们将讨论它们之间的区别及其用途。
最简单和最直接的环境变量是命名变量。我们之前见过它:它只是一个带有值的名称,可以通过在名称前加上“$”来检索。您可以通过键入名称、等号,然后键入生成字符字符串的内容来一次创建和定义命名变量。
之前我们看到了以下简单的示例
$ VAR=Hello
这只是分配一个简单值。定义变量后,我们也可以重新定义它
$ VAR=Goodbye
我们也不限于简单的字符串。我们可以很容易地将一个变量的值赋给另一个变量
$ VAR=$PATH
我们甚至可以全力以赴,组合多个命令来生成一个值
$ PS1= "`whoami`@`hostname -s` `pwd` \$ "
在这种情况下,我们获取三个命令“whoami”、“hostname”和“pwd”的输出,然后添加“$”符号,以及一些空格和其他格式,只是为了稍微填充一下。哇。所有这些,都放在一个带标签的空间里。正如您所看到的,环境变量可以保存相当多的内容,包括整个命令的输出。
即使您没有意识到,您的环境中通常也定义了许多命名变量。尝试“set”命令并查看。
shell 中的大多数环境变量都是命名变量,但也有一些“特殊”变量。您不会设置这些变量,但它们的数值由 shell 自动安排和维护。这些变量的存在是为了帮助您发现有关 shell 或环境的信息。
最常见的是位置变量或参数变量。您在 shell 中执行的任何命令(在交互模式或脚本中)都可以有命令行参数。即使命令实际上没有使用它们,它们仍然可以存在。您可以通过在命令之后简单地键入它们来将命令行参数传递给命令,如下所示
这适用于任何命令。甚至您自己的 shell 脚本。但是假设您这样做(创建一个 shell 脚本,然后使用参数执行它);您如何从脚本中访问命令行参数?这就是位置变量的作用。当 shell 执行命令时,它会自动将任何命令行参数按顺序分配给一组位置变量。这些变量的名称是数字:1 到 9,通过 $1 到 $9 访问。实际上是 0 到 9;$0 是执行的命令的名称。例如,考虑这样的脚本
#!/bin/sh
echo $0
echo $1
echo $2
以及对该脚本的以下调用
如您所见,shell 自动将值“Hello”和“World”分配给 $1 和 $2(好吧,从技术上讲是分配给名为 1 和 2 的变量,但在书面文本中称它们为 $1 和 $2 不会那么令人困惑)。如果我们使用两个以上的参数调用此脚本会发生什么?
这根本不是问题——额外的参数将分配给 $3 和 $4。但是我们没有在脚本中使用这些变量,因此这些命令行参数会被忽略。相反的情况(参数太少)呢?
同样,没问题。当脚本访问 $2 时,shell 只会用 $2 的值替换 $2。在这种情况下,该值为空,因此我们打印完全相同的内容。在这种情况下,这不是问题,但如果您的脚本有强制性参数,则应检查它们是否确实存在。
如果我们希望“Hello”和“World”被视为要传递给脚本的一个命令行参数怎么办?即“Hello World”而不是“Hello”和“World”?当我们开始讨论引用时,我们将深入探讨这一点,但现在只需用单引号将这些单词括起来
$ WithArgs.sh 'Hello World' 'Mouse Cheese'
WithArgs.sh
Hello World
Mouse Cheese
那么,如果您有超过九个命令行参数会发生什么?然后您的脚本过于复杂。不,但说真的:然后您遇到了一点小问题。允许传递超过九个参数,但只有九个位置变量(至少在 Bourne Shell 中是这样)。为了处理这种情况,shell 包含了shift命令
Shift 导致位置参数向左移动。也就是说,$1 的值变成 $2 的旧值,$2 的值变成 $3 的旧值,依此类推。使用shift,您可以访问所有命令行参数(即使超过九个)。shift 的可选整数参数是要移动的位置数(因此您可以一次移动任意多个位置)。不过,需要注意以下几点
- 无论您移位多少次,$0 始终保持原始命令。
- 如果您移位 n 个位置,则 n 必须小于参数的数量。如果 n 大于参数的数量,则不会发生移位。
- 如果您移位 n 个位置,则前 n 个参数将丢失。因此,请确保您已将它们存储在其他位置,或者您不再需要它们!
- 您无法向右移位。
在关于控制流的模块中,我们将了解如何在不知道参数的确切数量的情况下遍历所有参数。
除了位置变量之外,Bourne Shell 还包含许多其他特殊变量,其中包含有关 shell 的特殊信息。您可能不会经常使用这些变量,但了解它们的存在总是一件好事。这些变量是
- $#
- 当前命令的命令行参数数量(在使用shift命令后会发生变化!)。
- $-
- 当前生效的 shell 选项(参见“set”命令)。
- $?
- 最后执行命令的退出状态(如果成功则为 0,如果出错则为非零)。
- $$
- 当前进程的进程 ID。
- $!
- 最后一个后台命令的进程 ID。
- $*
- 所有命令行参数。带引号时,扩展为所有命令行参数作为一个单词(即“$*" = "$1 $2 $3 ...")。
- $@
- 所有命令行参数。带引号时,扩展为所有命令行参数分别带引号(即“$@" = "$1" "$2" "$3" ...)。
我们之前已经提到过几次:Unix 是一个多用户、多处理的操作系统。Bourne Shell 完全支持这一事实,它允许您直接在正在运行的 shell 中启动新进程。事实上,您甚至可以在彼此旁边同时运行多个进程(但我们稍后再讨论这一点)。这是一个启动子进程的简单示例
$ sh
我们还讨论了命令执行环境和环境(后者是变量的集合)。这些环境会影响程序的运行方式,因此它们不能无意中相互影响这一点非常重要。毕竟,您不希望仅仅因为有人在另一个进程中启动了 Midnight Commander,您的 shell 中的屏幕就变成蓝色带黄色字母,对吧?
shell 为避免进程无意中相互影响所做的事情之一是环境分离。基本上,这意味着每当启动一个新的(子)进程时,它都会拥有自己的 CEE 和环境。当然,如果您的 shell 的子进程的环境完全为空,那将非常不方便;您的子进程将没有 PATH 变量或您为提示符格式选择的设置。另一方面,通常有充分的理由不在子进程的环境中包含某些变量,并且通常与在进程不需要这些数据时不向其传递过多的环境数据有关。在运行 MS-DOS 的副本和 Windows 下的 DOS 版本时,尤其如此。您只有有限的环境空间,因此您必须小心使用它,或者在启动时请求更多空间。如今在 UNIX 环境中,空间问题不再相同,但是如果所有现有变量最终都进入子进程的环境,您仍然可能会对在该子进程中启动的程序的运行产生不利影响(在子进程的情况下,保持环境精简和干净确实有其道理)。
Stephen Bourne 和其他人提出的两种极端情况之间的折衷方案是:子进程有一个环境,其中包含其父进程环境中变量的副本——但仅限于标记为要导出(即复制到子进程)的变量。换句话说,您可以将任何变量复制到子进程的环境中,但您必须首先让 shell 知道您想要这样做。这是一个区分的示例
$ echo $PATH
/usr/local/bin:/usr/bin:/bin
$ VAR=value
$ echo $VAR
value
$ sh
$ echo $PATH
/usr/local/bin:/usr/bin:/bin
$ echo $VAR
$
在上面的示例中,PATH 变量(默认情况下标记为导出)被复制到在 shell 内启动的 shell 的环境中。但是 VAR 变量未标记为导出,因此第二个 shell 的环境不会获得副本。
要将变量标记为导出,您可以使用export命令,如下所示
如您所见,您可以在一次操作中导出任意数量的变量。您也可以在没有参数的情况下发出export命令,这将打印环境中标记为导出的变量列表。这是一个导出变量的示例
$ VAR=value
$ echo $VAR
value
$ sh
$ echo $VAR
$ exit #Quitting the inner shell
$ export VAR #This is back in the outer shell
$ sh
$ echo $VAR
value
Korn Shell 和 Bash 等更现代的 shell 具有更扩展形式的export。一个常见的扩展是允许在一个命令中定义和导出变量。另一个是允许您删除变量的导出标记。但是,Bourne Shell 只支持如上所述的导出。
在前面的部分中,我们讨论了使用 shell 运行的每个程序和命令的运行时环境。我们讨论了命令执行环境,并详细讨论了其中一个名为“环境”的部分,其中包含环境变量。我们已经看到,您可以定义自己的变量,并且系统通常已经有很多变量可以开始使用。
这里有一个关于系统开始时设置的那些变量的问题:它们从哪里来?它们像甘露一样从天而降吗?相关问题是:如果每次 shell 启动时您都想要自动创建一些变量该怎么办?或者每次登录时都运行一个程序?
那些在其他操作系统上进行过一些挖掘的读者会知道我的意思:通常有一些方法可以在每次登录时(或至少在每次系统启动时)执行一组命令。例如,在 MS-DOS 中,有一个名为 autoexec.bat 的文件,该文件在每次系统启动时都会执行。在旧版本的 MS-Windows 中,有 system.ini。Bourne Shell 有类似的东西:每个用户主目录中名为.profile的文件。$HOME/.profile(HOME 是一个默认变量,其值为您的主目录)文件与任何其他 shell 脚本一样,在您登录到新的 shell 会话后会自动执行。您可以编辑脚本以使其执行您喜欢的任何登录命令。
每个特定的 Unix 系统都有自己默认的 .profile 脚本实现(包括无——允许不具有 .profile 脚本)。但它们都以某种形式的以下内容开头
#!/bin/sh
if [ -f /etc/profile ]; then
. /etc/profile
fi
PS1= "`whoami`@`hostname -s` `pwd` \$ "
export PS1
这个 .profile 可能会让您有点惊讶:设置的所有这些变量在哪里?在典型的 Unix 系统上为您设置的大多数变量也为所有其他用户设置。为了使这成为可能并易于维护,常见的解决方案是让每个 $HOME/.profile 脚本首先执行另一个 shell 脚本:/etc/profile。此脚本是一个系统范围的脚本,其内容由系统管理员(以用户名root登录的用户)维护。此脚本设置各种变量并调用设置更多变量的脚本,并通常执行提供每个用户舒适工作环境所需的一切操作。
从上面的示例中可以看到,您可以将任何您想要或需要的个人配置添加到您目录中的 .profile 脚本中。执行系统配置文件脚本的调用不必是第一个,但您可能不想完全删除它。
随着快速计算机、能够在极短时间内在多个任务之间切换的 CPU、能够同时执行多项任务的 CPU 以及多个 CPU 的网络的出现,让计算机同时执行多个任务已变得司空见惯。快速任务切换提供了计算机确实能够同时运行多个任务的错觉,从而能够有效地同时为多个用户提供服务。并且能够在旧任务等待外设设备时切换到新的 CPU 任务,这使得 CPU 使用效率大大提高。
为了利用多任务处理功能作为用户,您需要一个支持多任务处理的命令环境。例如,能够将一个程序设置为一个任务,然后继续并启动一个新程序而旧程序仍在运行。这种能力允许您作为用户在同一台机器上同时执行多项操作,只要这些程序不相互干扰即可。当然,您不能总是将每个程序视为“启动并忘记”的事情;您可能需要输入密码,或者程序可能已完成并希望告诉您其结果。多任务处理环境必须允许您在正在运行的多个程序之间切换,并允许这些程序在需要您的注意时向您发送某种消息。
为了使事情更易于理解,请考虑像下载文件这样的事情。通常,在下载文件时,您也希望做其他事情——否则,当您想要下载整张 CD 的数据时,您将不得不长时间坐在键盘前空转。因此,您启动文件下载器并为其提供要获取的文件列表。输入完文件后,您可以告诉它“开始!”,它将首先开始下载第一个文件,并继续直到完成最后一个文件,或者直到出现问题。更智能的文件下载器甚至会尝试自行解决常见问题,例如文件不可用。一旦启动,您将获得标准的 shell 提示符,让您知道可以启动另一个程序。
如果您想查看文件下载器的下载进度,只需检查系统中的文件与列表中的文件即可。但另一种通知您的方法是通过环境。环境可以包含您使用的文件,这可以帮助提供有关正在运行的程序(如文件下载器)的进度信息。它是否下载了所有文件?如果检查状态文件,您将看到它已下载了 65% 的文件,并且现在正在处理最后三个文件。
其他一些不需要手动干预的程序示例包括播放音乐的程序。通常情况下,一旦你启动了一个播放音乐轨道的程序,你**不希望**告诉程序“好的,现在播放下一首曲目”。它应该能够自行完成此操作,前提是给定了一个要播放的歌曲列表。事实上,它甚至不需要占用监视器;你可以在按下“播放”按钮后立即开始运行其他软件。
在本节中,我们将探讨 Unix shell 中的多任务支持。我们将了解如何启用支持、如何处理多个任务以及 shell 提供的可用实用程序。
在我们讨论 shell 中多任务处理的机制之前,让我们先了解一些术语。这将有助于我们清楚地讨论主题,你也会知道在其他地方遇到这些术语时它们指的是什么。
首先,当我们在系统上以其自身进程的方式启动一个程序运行时,该进程与该程序的一个正在运行的实例称为一个作业。你还会遇到诸如进程、任务、实例或类似的术语。但 Unix shell 中使用的术语是作业。其次,shell 影响和使用多任务处理(启动作业等)的能力称为作业控制。
- 作业
- 正在执行计算机程序实例的进程。
- 作业控制
- 能够选择性地停止(挂起)作业的执行并在稍后继续(恢复)其执行。
请注意,这些术语在 Unix shell 中以这种方式使用。其他情况和其他上下文可能允许不同的定义。以下是一些你还会遇到的其他术语
- 作业 ID
- 唯一标识作业的 ID(通常为整数)。可用于引用不同工具和命令的作业。
- 进程 ID(或 PID)
- 唯一标识进程的 ID(通常为整数)。可用于引用不同工具和命令的进程。与作业 ID 不同。
- 前台作业(或前台进程)
- 可以访问终端的作业(即可以从键盘读取并写入监视器)。
- 后台作业(或后台进程)
- 无法访问终端的作业(即无法从键盘读取或写入监视器)。
- 停止(或挂起)
- 停止作业的执行并将终端控制权返回给 shell。停止的作业不是终止的作业。
- 终止
- 从内存中卸载程序并销毁运行该程序的作业。
作业是在 shell 中启动的程序。默认情况下,新作业将挂起 shell 并控制输入和输出:你在键盘上输入的每个按键都将发送到作业,鼠标的每个移动也是如此。除了作业之外,没有任何其他内容可以写入监视器。这就是我们所说的前台作业:它位于前台,作为用户对你来说清晰可见,并且遮挡了系统中所有其他作业的视图。
但有时这种工作方式非常笨拙且令人恼火。如果你启动了一个不需要你输入的长时间运行的作业(例如硬盘备份)会怎样?如果这是一个前台进程,你必须等到它完成才能执行任何其他操作。在这种情况下,你更希望将程序作为后台进程启动:一个正在运行但不会监听输入设备也不会写入监视器的进程。Unix 支持它们,shell(使用作业控制)允许你将任何作业作为后台作业启动。
但是中间地带呢?比如那个文件下载器?你必须启动它,登录到远程服务器,选择你的文件并开始下载。只有在完成所有这些操作后,该作业才有意义在后台运行。但是,如果你已经将程序作为前台作业启动,该如何实现呢?或者考虑这种情况:你正在你喜欢的编辑器中忙着编写文档,并且只想暂时退出查看邮件。你是否必须为此关闭编辑器?然后,在你查看完邮件后,重新启动它,重新打开你的文件并找到你离开的位置?这很不方便。不,在这两种情况下,更好的方法是简单地挂起程序:只需停止它进一步运行并将控制权返回给 shell。一旦你回到 shell 中,你就可以启动另一个程序(邮件),然后在你完成该程序后恢复挂起的程序(编辑器)——并准确地返回到你离开程序的位置。相反,你也可以决定让挂起的进程(下载器)继续运行,但现在在后台运行。
当我们谈论 shell 中的作业控制时,我们指的是上面描述的功能:在后台启动程序、挂起正在运行的程序以及恢复挂起的程序(在前台或后台)。
为了完成我们在上一节中讨论的所有操作,你需要两样东西
- 支持作业控制的操作系统。
- 支持作业控制并已启用作业控制的 shell。
Unix 系统支持多任务处理和作业控制。Unix 从一开始就被设计为支持多任务处理。如果你遇到一个声称是 Unix 供应商但其软件不支持作业控制的人,称他为骗子。然后扔掉他的安装 CD。然后扔掉他本人。
当然,你已经猜到了接下来会发生什么,对吧?我要告诉你 Bourne Shell 支持作业控制。并且你可以依靠相同的机制在所有兼容的 shell 中工作。猜猜看:你错了。原始的 Bourne Shell 没有作业控制支持;它是一个单任务 shell。但是,Bourne Shell 有一个扩展版本,称为jsh(猜猜“j”代表什么……),它具有作业控制支持。要在原始 Bourne Shell 中使用作业控制,你必须以交互模式启动此扩展 shell,如下所示
在该 shell 中,你拥有我们将在以下部分讨论的作业控制工具。
从那时起编写的几乎所有其他 shell 都直接将作业控制集成到基本 shell 中,并且 POSIX 1003 标准已经标准化了作业控制实用程序。因此,你几乎可以依赖现在可用的作业控制,并且通常在交互模式下默认启用(一些较旧的 shell,如 Korn shell,具有支持,但要求你专门启用该支持)。但以防万一,请记住你可能需要在你的系统上执行一些额外操作才能使用作业控制。不过,有一个需要注意的地方:在 shell 脚本中,你通常会包含一个调用 Bourne Shell 的解释器提示(即#!/bin/sh)。由于原始的 Bourne Shell 没有作业控制,因此许多现代 shell 在非交互模式下默认关闭作业控制作为兼容性功能。
我们已经详细讨论了如何创建前台作业:在提示符处键入命令或可执行文件名,按 Enter 键,这就是你的作业。已经做过,完成了,买了 T 恤。
我们也已经提到如何启动后台作业:在命令末尾添加一个&符号。
$ ls * > /dev/null &
[1] 4808
$
但现在看起来与我们之前发出命令时有所不同;那里有一个“[1]”和一些数字。“[1]”是作业 ID,数字是进程 ID。我们可以使用这些数字来引用我们刚刚创建的进程和作业,这对于使用处理作业的工具很有用。当任务完成后,你会收到类似以下内容的通知
[1]+ Done ls * > /dev/null &
你用来管理作业的工具之一是“fg”命令。此命令获取一个后台作业并将其置于前台。例如,考虑一个实际需要一些时间才能完成的后台作业
while [ $CNT -lt 200000 ]; do echo $CNT >> outp.txt; CNT=$(expr $CNT + 1); done &
我们还没有深入探讨流程控制,但这会将 200,000 个整数写入文件,需要一些时间。它还在后台运行。假设我们启动了此作业
$ CNT=0
$ while [ $CNT -lt 200000 ]; do echo $CNT >> outp.txt; CNT=$(expr $CNT + 1); done &
[1] 11246
作业被赋予作业 ID 1 和进程 ID 11246。让我们将进程移到前台
$ fg %1
while [ $CNT -lt 200000 ]; do
echo $CNT >> outp.txt; CNT=$(expr $CNT + 1);
done
作业现在在前台运行,你可以从我们没有返回提示符这一事实中看出。现在键入CTRL+Z键盘组合
'CTRL+Z'
[1]+ Stopped while [ $CNT -lt 200000 ]; do
echo $CNT >> outp.txt; CNT=$(expr $CNT + 1);
done
$
你注意到 shell 报告作业已停止了吗?尝试使用“cat”命令检查 outp.txt 文件。尝试几次;内容不会改变。该作业不是后台作业;它根本没有运行!作业已挂起。许多程序识别CTRL+Z组合以进行挂起。即使那些没有识别该组合的程序通常也有一些方法可以自行挂起。
作业挂起后,你可以将其恢复到前台或后台。要恢复到前台,请使用前面讨论的“fg”命令。你使用“bg”恢复到后台
为了恢复我们之前编写的生成数字的长期作业,我们需要执行以下操作
$ bg %1
[1]+ while [ $CNT -lt 200000 ] do
echo $CNT >> outp.txt; CNT=`expr $CNT + 1`;
done &
$
输出表明作业已重新运行。这次是在后台,因为我们也收到了一个提示。
我们能否也停止后台的进程?当然可以,我们可以将其移动到前台并按下“CTRL+Z”。但是我们能否直接做到这一点呢?嗯,没有实用程序或命令可以做到这一点。大多数情况下,你不会想要这样做——将其置于后台的全部意义在于让它运行而不会打扰任何人或需要关注。但如果你真的想这样做,可以按照以下步骤操作
或者
我们将在稍后讨论信号时详细了解其具体作用。
我们之前提到过,POSIX 1003.1 标准已经标准化了许多作业控制工具,这些工具包含在 jsh shell 及其后续版本中用于作业控制。我们已经了解了一些这些工具;在本节中,我们将介绍完整的列表。
标准作业控制工具列表如下:
- bg
- 将作业移至后台。
- fg
- 将作业移至前台。
- jobs
- 列出活动作业。
- kill
- 终止作业或向进程发送信号。
- CTRL+C
- 终止作业(与使用 SIGTERM 信号的“kill”相同)。
- CRTL+Z
- 挂起前台作业。
- wait
- 等待后台作业终止。
所有这些命令都可以接受作业规范作为参数。作业规范以百分号开头,可以是以下任何一项:
- %n
- 作业 ID(n 为数字)。
- %s
- 命令行以字符串 s 开头的作业。
- %?s
- 命令行包含字符串 s 的作业。
- %%
- 当前作业(即您使用作业控制管理的最新作业)。
- %+
- 当前作业(即您使用作业控制管理的最新作业)。
- %-
- 上一个作业。
我们已经了解了“bg”、“fg”和 CTRL+Z,我们将在后面的部分中介绍“kill”。剩下的就是“jobs”和“wait”。让我们从最简单的开始
“Wait”是所谓的同步机制:它导致调用进程暂停,直到所有后台作业终止。或者,如果您包含一个或多个作业规范,则直到您列出的作业终止。如果您启动了多个作业(仅仅是为了利用系统的并行处理能力),并且在所有作业完成之前无法安全地继续,则可以使用“wait”。
“wait”命令在相当高级的脚本中使用。换句话说,你可能不会经常使用它。不过,这里有一个你可能会经常使用的命令:
- -l 除了正常输出外,还列出进程 ID
- -n 将输出限制为自上次状态报告以来状态已更改的作业的信息
- -p 仅列出作业的进程组组长的进程 ID
- -r 将输出限制为正在运行的作业的数据
- -s 将输出限制为已停止的作业的数据
- 作业规范如上所述
jobs 命令报告有关活动作业的信息和状态(不要将活动与运行混淆!)。但必须记住,此命令报告的是作业而不是进程。由于作业对 shell 是本地的,因此“jobs”命令无法跨 shell 查看。“jobs”命令是您可以对其应用作业控制的作业的主要信息来源;首先,如果您不记得作业 ID,则可以使用此命令检索作业 ID。例如,请考虑以下情况:
$ CNT0=0
$ while [ $CNT0 -lt 200000 ]; do echo $CNT0 >> outtemp0.txt; CNT0=`expr $CNT0 + 1`; done&
[1] 26859
$ CNT1=0
$ while [ $CNT1 -lt 200000 ]; do echo $CNT1 >> outtemp1.txt; CNT1=`expr $CNT1 + 1`; done&
[2] 31331
$ jobs
[1]- Running while [ $CNT0 -lt 200000 ]; do echo $CNT0 >> outtemp0.txt; CNT0=`expr $CNT0 + 1`; done & [2]+ Running while [ $CNT1 -lt 200000 ]; do echo $CNT1 >> outtemp1.txt; CNT1=`expr $CNT1 + 1`; done &
说到状态(由“jobs”命令报告),现在是时候讨论一下我们拥有的不同状态了。作业可以处于多种状态,有时甚至可以同时处于多种状态。“jobs”命令在作业 ID 和顺序之后直接报告状态。我们识别以下状态:
- 运行
- 作业正在执行其应执行的操作。除非您真的想亲自关注程序(例如,停止程序或了解文件下载的进度),否则您可能不需要中断它。您通常会发现,前台任何不等待您关注的内容都处于此状态,除非它已被置于睡眠状态。
- 睡眠
- 当程序需要检索尚不可用的输入时,它们无需继续使用 CPU 资源。因此,它们将进入睡眠模式,直到另一批输入到达。您将看到更多睡眠进程,因为它们不太可能在确切的时间点处理数据。
- 停止
- 停止状态表示程序已被操作系统停止。这通常发生在用户挂起前台作业(例如,按下 CTRL-Z)或收到 SIGSTOP 时。此时,作业无法主动消耗 CPU 资源,并且除了仍加载在内存中之外,不会影响系统的其余部分。一旦收到 SIGCONT 信号或以其他方式从 shell 恢复,它将从中断的地方继续。睡眠和停止的区别在于,“睡眠”是一种等待计划事件发生的形式,而“停止”可以由用户发起并且是不确定的。
- 僵尸
- 如果父程序在子程序能够将其返回值提供给父程序之前终止,则会出现僵尸进程。这些进程将由init进程清理,但有时需要重新引导才能将其删除。
在上一节中,我们讨论了 Unix shell 中可用于作业控制的标准工具。但是,您也可能会遇到许多非标准工具。即使本书的重点是 Bourne Shell 脚本(尤其是作为 Unix shell 脚本的通用语言),但这些工具非常普遍,如果我们至少不提及它们,那将是不负责任的。
除了前面讨论的工具之外,还有两个非常常见的 shell 命令:“stop”和“suspend”。
“stop”命令是许多与 System V 兼容的 Unix 系统的 shell 中出现的命令。它用于挂起后台进程——换句话说,它等效于后台进程的“CTRL+Z”。它通常接受作业 ID,就像大多数这些命令一样。在没有“stop”命令的系统上,您应该能够通过使用“kill”命令向后台进程发送 SIGSTOP 信号来停止后台进程。
suspend [-f]
您可能会遇到的另一个命令是“suspend”命令。“suspend”命令有点棘手,因为它并不总是对所有系统和所有 shell 都有相同的含义。目前作者知道有两种变体,上面都显示了。第一个明显的变体接受作业 ID 参数并挂起指定的作业;实际上,它与“CTRL+Z”相同。
“suspend”的第二个变体根本不接受作业 ID,这是因为它不会挂起任何随机作业。相反,它会挂起发出该命令的 shell 的执行。在此变体中,-f 参数指示即使 shell 是登录 shell 也应将其挂起。要恢复 shell 执行,请使用“kill”命令向其发送 SIGCONT 信号。
我们将讨论的最后一个工具是进程快照实用程序“ps”。此实用程序根本不是 shell 工具,但它以某种变体存在于几乎每个系统上,并且您会经常想要使用它。可能比“jobs”工具更频繁。
“ps”实用程序旨在报告系统中正在运行的进程。进程,而不是作业——这意味着它可以跨 shell 实例查看。以下是一个“ps”实用程序的示例:
$ ps x
PID TTY STAT TIME COMMAND 32094 tty5 R 3:37:21 /bin/sh 37759 tty5 S 0:00:00 /bin/ps
典型的进程输出包括进程 ID、进程连接到的(或在其上运行的)终端的 ID、进程已使用的 CPU 时间以及用于启动进程的命令。您可能还会获得进程状态。进程状态由字母代码指示,但总的来说,报告的状态与作业报告相同:R运行、S睡眠、sT停止和Z僵尸。但是,不同的“ps”实现可能使用不同的或更多的代码。
编写有关“ps”的主要问题在于它并非完全标准化,因此可以使用不同的命令行选项集。您需要查看系统上的文档以获取具体详细信息。不过,某些选项非常常见,因此我们将在此处列出:
- -a
- 列出除组领导者进程之外的所有进程。
- -d
- 列出除会话领导者之外的所有进程。
- -e
- 列出所有进程,而不考虑用户 ID 和其他访问限制。
- -f
- 生成完整列表作为输出(即所有报告选项)。
- -g 列表
- 将输出限制为组领导者进程 ID 在列表中提到的进程。
- -l
- 生成长列表。
- -p 列表
- 将输出限制为进程 ID 在列表中提到的进程。
- -s 列表
- 将输出限制为会话领导者进程 ID 在列表中提到的进程。
- -t 列表
- 将输出限制为在列表中提到的终端上运行的进程。
- -u 列表
- 将输出限制为由列表中提到的用户帐户拥有的进程。
“ps”工具可用于监控跨 shell 实例的作业,并发现用于信号传输的进程 ID。