Shell(十三):子Shell与进程处理
Linux是一种用户控制的多作业操作系统,系统允许多个用户同时提交作业,而一个系统用户又可能用多个Shell登录,每个系统用户可以用一个Shell提交多个作业。
下面来学习,bash Shell在多作业管理和进程处理方面的命令与机制。
1、子Shell
父子Shell是相对的,描述了两个Shell进程的fork关系,父Shell指在控制中断或xterm窗口给出提示符的进程,子Shell由父Shell创建的进程。在Linux中,只有一个函数可以创建子进程,即 fork函数。父Shell创建子Shell调用的是fork函数。
Shell命令分为内建命令(built-in command)和外部命令(external command),内建命令是由Shell本身执行的命令,而外部命令由fork创建出来的子Shell执行,简言之,两者的区别在于:内建命令不创建子Shell,外部命令创建子Shell。因此,内建命令的执行速度要比外部命令快。
1.1、内建命令
内建命令就是包含在bash Shell工具包中的命令,内建的英文单词 built-in。内建命令是bash Shell的骨干部分,保留字(reserved words)也是bash Shell的骨干部分,保留字用来构建Shell语法结构,for、if、then、while、until等都是保留字,保留字本身不是一个命令,而是命令结构的一部分。
1.2、圆括号结构
圆括号结构能够强制将其中的命令运行在子Shell中,它的基本格式为:
{
cmd1
...
cmdn
}
上述结构表示圆括号内的n条命令在子Shell中运行,bash版本3之后定义了内部变量BASH_SUBSHELL,该变量记录了子Shell的层次。
#!/bin/bash
# 圆括号结构用法和BASH_SUBSHELL变量
# 打印父 Shell 的层次
echo "The level of father Shell is: $BASH_SUBSHELL"
# 定义一个变量
outervar=OUTER
(
# 进入子Shell
echo "The level of SubShell is: $BASH_SUBSHELL"
# 在子Shell内定义一个变量
innervar=INNER
echo "innervar=$innervar"
echo "outervar=$outervar"
)
# 回到父Shell
echo "The level of father Shell is: $BASH_SUBSHELL"
# 测试子Shell中定义的变量是否为空
if [ -z "$innervar" ]
then
echo "The \$innervar is not defined in main body."
else
echo "The \$innervar is defined in main body."
fi
subsvar.sh脚本首先在父Shell中打印$BASH_SUBSHELL的值,从subsvar.sh脚本的执行结果可以看到,父Shell的$BASH_SUBSHELL值为0,然后父Shell定义变量outervar。接着利用圆括号结构创建子Shell,打印子Shell的$BASH_SUBSHELL,子Shell的$BASH_SUBSHELL值为1,然后在子Shell中定义变量innervar,并在子Shell中同时打印innervar和outervar,innervar的值就是子Shell所赋的值,outervar继承了父Shell所赋给它的值。圆括号结构执行结束就返回到父Shell,if/then结构测试子Shell中定义的变量innervar是否为空,结果输出innervar变量未定义的信息,说明innervar为空值,这说明子Shell中变量对父Shell是不可见的。
执行结果如下:
既然子Shell变量的作用域不能在父Shell中生效,那么,若在子Shell中将变量export改为环境变量,该变量是否在父Shell中生效呢?新建名为 subseq.sh 的脚本,详情如下:
#!/bin/bash
# 子Shell定义环境变量是否对父Shell有效
# 在父Shell中定义变量outervar
echo "------ IN MAINSHELL ------"
outervar=OUTER
echo "outervar=$outervar"
# 进入子Shell
(
echo "------ IN SUBSHELL ------"
innervar=INNER
echo "innervar=$innervar"
# 更改父Shell所定义的outervar变量值
outervar=OUTER-INNER
echo "outervar=$outervar"
# 将 innervar 和 outervar 声明为环境变量
export innervar
export outervar
)
# 回到父Shell,测试innervar和outervar的值是否与子Shell中的定义一样
echo "------ RETURN TO MAINSHELL ------"
echo "innervar=$innervar"
echo "outervar=$outervar"
subseq.sh脚本定义outervar变量,利用圆括号结构创建子Shell后,子Shell定义innervar变量,并更改outervar变量值,然后利用export命令将innervar和outervar声明为环境变量,圆括号结构执行结束返回父Shell后,输出innervar和outervar的值,测试是否与子Shell中定义的一致。父Shell定义outervar,其值为OUTER,子Shell定义innervar,赋值为INNER,并将父Shell所定义的outervar的值改为OUTER-INNE。
返回父Shell后,innervar为空值,outervar仍为原来的值;OUTER,这充分说明了尽管子Shell将innervar和outervar声明为环境变量,但是子Shell对innervar的定义和对outervar的更改仍然对父Shell不可见。
执行结果如下:
子Shell只能继承父Shell的一些属性,子Shell不可能反过来改变父Shell的属性,子Shell能够从父Shell继承得来的属性如下:
·当前工作目录
·环境变量
·标准输入、标准输出和标准错误输出
·所有已打开的文件标识符
·忽略的信号
子Shell不能从父Shell继承得来的属性归纳如下:
·除了环境变量和.bashrc文件中定义变量之外的Shell变量;
·未被忽略的信号处理。
2、Shell的限制模式
Shell有一种模式称为限制模式,简称RSH(Restricted Shell),处于限制模式的Shell下运行一个脚本或脚本片断,将会禁用一些命令或操作。
resshell.sh脚本是通过开启Shell的restricted选项进入限制模式的,还有一种以限制模式运行脚本的方式,将Sha-bang符号(#!)后的语句改成/bin/bash -r,-r表示在限制模式下运行该脚本。
新建 anotherres.sh 脚本,详情如下:
#!/bin/bash -r
# 以限制模式运行该脚本
# 验证限制模式下能否改变当前工作目录
echo "\$SHELLOPTS=$SHELLOPTS"
echo
# 验证限制模式下能否改变当前工作目录
echo "Changing current work directory"
cd /etc
echo "current directory $PWD"
echo
# 验证限制模式下能否改变$SHELL变量的值
echo "Trying to change \$SHELL"
SHELL="/bin/sh"
echo "\$SHELL=$SHELL"
echo
# 验证在限制模式下能否执行重定向操作
echo "Trying to redirect output to a file"
who > outputnull
ls -l outputnul
anotherres.sh脚本是在限制模式下运行的,在该模式下,Shell能够读取$SHELLOPTS变量的值。改变当前工作目录、改变$SHELL变量的值、以及执行重定向操作,限制模式与非限制模式下的执行结果一样,都是不被允许的。
anotherres.sh脚本执行结果如下:
3、进程处理
内建命令是由Shell本身执行的命令,而外部命令则需要创建新的进程来执行,从进程角度归纳Shell执行命令的过程如下:
Shell命令不是内建命令时,Linux系统利用fork对一个子进程执行该命令,父进程进入等待状态;然后,若该命令或脚本中包含编译过的可执行文件,内核将新程序装载到内存,并覆盖子进程,执行结束后,退出子进程,父进程被重新激活,开始读取Shell提示符后的下一条命令。
fork是Linux系统的一种系统调用(system calls),系统调用用于请求内核服务,这是进程访问硬件的唯一方法。fork是创建新进程的系统能够调用,fork创建的子进程是父进程的副本,两个进程具有同样的环境、打开的文件、用户标志符、当前工作目录和信号等。
3.1、进程和作业
正在执行的进程称为作业,一个作业可以包含多个进程。用户提交作业到操作系统,作业的完成可能依赖于启动多个进程。因此,作业时用户层面的概念,进程是操作系统层面的概念。
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,进程在运行中不断地改变其运行状态。通常,一个运行进程必须具有以下三种基本状态:
·就绪(Ready)状态:当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态;
·运行(running)状态:当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态;
·阻塞(Blocked)状态:正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件有多种,如,等待I/O完成、申请缓冲区不能满足、等待信号等。
一个进程在运行期间,不断从一种状态转换到另一种状态,可以多次处于就绪状态和执行状态,也可以多次处于阻塞状态。
Linux系统为每个进程分配一个数字以标识这个进程,这个数字就是进程号。同时,创建该进程的Shell为此进程创建一个数字,也用于标识这个进程,这个数字称为作业号。
作业号标识的是在此Shell下运行的所有进程,Linux是多用户的系统,多用户可能开启多个Shell,进程号就标识了整个系统下正在运行的所有进程。
& 符号使进程在后台运行,Shell将显示作业好和进程号,方括号中的[1]和[2]是作业号,方括号后面的 3233 和 3255 是进程号。
默认情况下,当输入下一条命令时,Shell才提示后台运行的作业已经结束,实际上该作业可能很早就运行结束。若要需要当后台运行的作业一结束,Shell就显示信息,就需要开启notify选项,该选项简写是b。
set -b 开启notify选项后,再次提交一个后台作业,该作业一旦结束Shell,立刻1提示作业完成。
3.2、作业控制
进程是针对Linux系统而言,作业是针对Shell而言。作业有两种运行方式:前台运行和后台运行。
前台运行的作业指作业能够控制当前终端或窗口,且能接收用户的输入;而后台运行的作业则不在当前激活的终端或窗口中运行。
内建命令fg可将后台运行的作业放到前台,而&符号使得作业再后台运行。
利用作业号对前台和后台作业的操作 就是 作业控制。
编写一个运行5秒钟的脚本,名为sleep5s.sh,详情如下:
#!/bin/bash
# 休眠5s后结束
sleep 5
利用fg密令将后台作业放到前台运行,Shell提示脚本名称,由于该作业放到前台,就控制了当前的Shell,Shell等待[1]号作业运行完毕后,才显示下一行提示符。
当有多个作业在后台运行时,不带任何参数的fg命令就将最近提交的那个后台作业放置到前台。若需要在其中挑选符合条件的作业,可以使用作业号、作业的命令字符等参数。
新建 sleep10s.sh 脚本,详情如下:
#!/bin/bash
# 休眠10s后结束
sleep 10
执行结果如下:
将sleep10s.sh作为第1个提交到后台运行的作业,将sleep5s.sh作为第2个提交到后台运行的作业。利用 fg%1 命令将作业号为1的作业放到前台运行,Shell提示 "./sleep10s.sh",[1]号作业时sleep10s.sh脚本,说明Shell确实将[1]号作业放置到了前台。Shell开始等待sleep10s.sh脚本的完成,由于[2]号作业仅需5秒就可完成,因此 在等待[1]号作业的过程中,Shell提示[2]号作业运行完毕的信息。
fg指定作业号的方法及含义:
参数 |
含义 |
%n |
n为后台作业的作业号 |
%string |
命令以string字符串开始的后台作业 |
%?string |
命令包含string字符串的后台作业 |
%+或%% |
最近提交的后台作业 |
%- |
最近第二个提交的后台作业 |
%string和%?string是以后台作业的命令来制定作业,%string表示提交作业的命令是string开头的,而%?string表示交作业的命令包含string。
3.3、信号
信号是Linux进程间通信的一个重要概念,是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个型号与处理器收到一个中断请求是一样的。
信号是异步的,一个进程不必通过任何操作来等待信号到达。信号事件的发生有两个来源:硬件来源,如按下的键盘操作;软件来源,发送信号的系统函数kill、raise等。
"Ctrl"组合键、信号类型及其意义:
组合键 |
信号类型 |
含义 |
Ctrl+C |
INT信号,即interrupt信号 |
停止当前运行的作业 |
Ctrl+Z |
TSTP信号,即terminal stop信号 |
使当前运行的作业暂时停止(转入阻塞状态) |
Ctrl+\ |
QUIT信号 |
当Ctrl+C无法停止作业时,使用此组合键 |
Ctrl+Y |
TSTP信号,即terminal stop信号 |
当进程从终端读取输入数据时,暂时停止该进程 |
演示案例如下:
将sleep10s.sh脚本提交到前台运行,按下"Ctrl+C"组合键,./sleep10s.sh立即停止。
3.4、trap命令
trap是Linux的内建命令,用于捕捉信号,trap命令可以指定收到某种信号时所执行的命令,如,trap可以制定能够收到由"Ctrl+C"组合键所触发的INT信号时,执行中断处理命令。trap命令的格式如下:
trap command sig1 sig2 ... sigN
trap命令表示当收到sig1、sig2、...、sigN中任意一个信号时,执行command命令完成后,脚本继续收到信号前的操作,直到脚本执行结束。
新建 triploop.sh 脚本,详情如下:
#!/bin/bash
# trap命令捕捉INT信号的语法
# 一旦收到INT信号,执行双引号内的echo命令
trap "echo 'You hit CONTROL+C!'" INT
# 使用冒号表示永真,无限循环
while :; do
# 记录进入循环的次数
let count=count+1
echo "This is the $count sleep"
# 每次循环休眠5秒
sleep 5
done
triploop.sh 脚本主题是一个while循环,条件是冒号,此时冒号表示永真,while循环是无限的,每次U型你换休眠5秒,并定义变量count记录进入循环的次数。在while中利用trap命令捕捉INT信号,即与Ctrl+C相绑定的中断信号,traploop.sh脚本一旦受到INT信号,就打印"You hit CONTROL+C!"的提示信息。
triploop.sh 脚本,执行结果如下: