「生活可以更简单, 欢迎来到我的开源世界」
  1. 一、简介
  2. 二、详解
    1. 1、子Shell
    2. 2、Shell的限制模式
    3. 3、进程处理
(转载)关于子shell
2020-11-20

原文:shell浅谈之九子shell与进程处理

作者:乌托邦2号

一、简介

Linux是一种用户控制的多作业操作系统,系统允许多个系统用户同时提交作业,而一个系统用户又可能用多个shell登录,每个系统用户可以用一个shell提交多个作业。

了解Bash Shell在多作业管理和进程处理方面的命名和机制有助于理解多用户、多作业的系统。

二、详解

1、子Shell

父子Shell是相对的,它描述了两个Shell进程的fork关系,父Shell指在终端模拟器的shell进程,子Shell是由父Shell创建的进程。父Shell创建子Shell调用的是fork函数。

创建子shell的情况:

Shell命令可以分为

冒号是Shell中一个特殊的符号

#圆括号结构用法
#!/bin/bash

echo "Father Shell is: $BASH_SUBSHELL" #打印父shell的层次,为0
outervar=OUTER #父shell的变量outervar

( #利用圆括号结构创建子shell
echo "SubShell is: $BASH_SUBSHELL" #子shell的层次为1
(
echo "GrandSubShell is: $BASH_SUBSHELL" #孙shell的层次为2
)
innervar=INNER #子shell的变量
echo "innervar=$innervar"
echo "outervar=$outervar" #outervar继承了符shell所赋给它的值
) #回到父shell

echo "Father Shell is: $BASH_SUBSHELL"

if [ -z "$innervar" ] #子shell中定义变量为空,则说明
then
echo "The \$innervar is not defined in main body."
else
echo "The \$innervar is defined in main body."
fi

img

子shell不影响父shell的环境:

子shell能继承父shell的一些属性,且子shell不能反过来改变父shell的属性:

shell是允许嵌套调用的,可以在函数或圆括号结构内再次调用圆括号结构创建子shell

利用子shell测试变量是否已经定义的例子:

#!/bin/bash

if (set -u; : $var) #冒号与$间有空格
then
echo "Variable is set."
fi

set -u命令用于设置shell选项,u是nounset表示当使用未定义的变量时,输出错误信息并强制退出。

: $var中冒号是不做任何事只是参数展开,若没有冒号则$var被解释成shell命令,shell试图去执行var变量的值。加上冒号,shell试图将var变量进行参数展开但不会试图去执行var变量的值。

shell可以接收到父shell从管道传送过来的数据,例:cat /etc/passwd | (grep 'root'),使用管道符向子shell发送数据,符shell将cat的结果通过管道发送给子shell,子shell执行grep命令。

shell应用将一个计算量较大的任务分成若干个小任务并行执行。

#子shell用于并行计算的用法
#!/bin/bash

#用圆括号结构创建三个子shell同时执行
(grep -r "root" /etc/* | sort > part1) & #与root关键字匹配的行,排序后输出到某文件
(grep -r "root" /usr/local/* | sort > part2) &
(grep -r "root" /lib/* | sort > part3) &

wait #等待后台执行的作业全部完成
cat part1 part2 part3 | sort > parttotal
echo "Run time of this script is:$SECONDS" #输出该脚本执行时间

grep -r递归搜索,搜索时的计算量比较大,对每个目录创建一个子shell进行并行处理,然后合并。

每个圆括号之外有一个&符号,表示此命令放在后台执行,继续执行下一条命令;若无&符号则需要一条命令执行完毕后再执行下一条命令,就没真正实现并行计算。

wait是一个内建命令,用于等待后台执行的作业全部完成后再执行下面的命令;若没有wait,脚本将三个子shell放在后台执行后直接执行合并临时文件的命令,三个子shell可能并未执行完毕,此时临时文件中的结果不完整,合并后也将产生不完整的结果。

2、Shell的限制模式

处于限制模式下的shell运行一个脚本或脚本片段,将会禁用一些命令或操作。shell的限制模式是Linux系统基于安全方面的考虑,目的为了限制脚本用户的权限,并尽可能地减小脚本所带来的危害。

的限制模式限制的操作有:用cd命令更改当前工作目录、更改重要的环境变量的值($PATH、$SHELL、$BASH_ENV、$ENV 和$SHELLOPTS)、输出重定向符号(>、>>、>|、>&、<>和&>)、调用含有一个或多个斜杠的命令名称、使用内建命令exec、使用set+r等命令关闭限制模式。

#正常模式和限制模式的区别
#!/bin/bash

echo "Changing current work directory"
cd /etc #正常模式下改变当前工作目录
echo "Now in $PWD"

set -r #shell选项使代码运行在限制模式下(r是restricted)
echo "------IN RESTRICTED MODE---------" #开始运行在限制模式下
echo "Trying to change directory"
cd /usr/local #cd命令出错,被限制了
echo "\$SHELLOPTS=$SHELLOPTS" #可以读取$SHELLOPTS变量的值
echo "Now in `pwd`" #还是/etc为当前目录
echo
echo "Trying to change \$SHELL"
SHELL="/bin/sh" #$SHELL变量在限制模式下只读
echo "\$SHELL=$SHELL"
echo
echo "Trying to redirect output to a file"
who > outputnull #输出重定向失败,被限制了
ls -l outputnull #outputnull没有被创建

开启shell的restricted选项进入限制模式,两种方式:

  1. set -r
  2. #!/bin/bash -r

3、进程处理

进程角度看shell执行

内建命令是由shell本身执行的命令,而外部命令则需要创建新的进程来执行。从进程角度归纳shell执行内建命令和外部命令的过程。

img

当shell命令不是内建命令时,linux利用fork对一个子进程执行该命令,父进程处于等待状态。若该命令或脚本中包含编译过的可执行文件,则内核将新程序装载到内存,并覆盖子进程,执行结束退出子进程,父进程被重新激活开始读取shell的后一条命令。

fork是系统调用,fork创建的子进程是父进程的副本,两个进程具有同样的环境、打开的文件、用户标志符、当前工作目录和信号等。

fork后exec,重新装载另一个程序。

进程和作业

作业是用户层面的概念,而进程是操作系统层面的概念。其区别:一个正在执行的进程称为作业,一个作业可以包含多个进程,用户提交作业到操作系统,作业的完成可能依赖于启动多个进程。

进程的三种基本状态:

img

作业号标识的是在该shell下运行的所有进程,而进程号就标识整个系统下正在运行的所有进程。

img

其中[1]是作业号,7574是进程号。

作业控制

作业是针对shell而言的,有前台运行和后台运行。内建命令fg可将后台运行的作业放到前台,而&符号使得作业在后台运行。

fg可以指定作业的方法(Ctrl+Z组合键可将正在运行的作业阻塞):

img

bg命令可将阻塞状态的作业转入后台运行。

jobs查看作业列表。

disown用于从shell的作业表中删除作业。

wait命令用与等待后台作业完成。

信号

信号是在软件层次上对中断机制的一种模拟,原理上一个进程收到一个信号与处理器收到一个中断请求是一样的。

信号事件的来源:硬件来源(比如按下键盘或其他 硬件故障)、软件来源(比如系统函数kill、raise、alarm、setitimer和sigqueue函数)。

信号是进程间通信机制中唯一的异步通信机制。

向进程发送信号大多通过Ctrl键加上一些功能键来实现。

img

除了利用组合键发送信号外,内建命令kill可用于向进程发送TERM(即terminal)信号,功能和INT信号类似用于停止进程。kill可以通过进程号、作业号(kill %n)或进程命令名想任何作业发送信号。kill杀掉自己本身的进程(记录了运行该脚本的进程号),其中大于128的退出码表示脚本是被系统强行结束的。kill -l可看出,kill命令一共能发出64种信号。

trap命令

trap是Linux的内建命令,用于捕捉信号,trap命令可以指定收到某种信号时所执行的命令。trap命令的格式如下:trap command sig1 sig2 … sigN,当接收到sinN中任意一个信号时,执行command命令,command命令完成后继续接收到信号前的操作,直到脚本结束。

#!/bin/bash

trap "echo 'You hit Ctrl+c!'" INT

while :; do
let count=count+1
echo "This is the $count sleep"
sleep 5
done

img

利用trap命令捕捉INT信号(即与Ctrl+c绑定的中断信号)。trap还可以忽略某些信号,将command用空字符串代替即可,如trap “” TERM INT,忽略kill %n和Ctrl+c发送的信号(kill发送的是TERM信号)。LInux更强劲的杀死进程的命令:kill -9 进程号(或kill -9 %n作业号)等价与kill -KILL 进程号。

子shell的信号

子shell能继承父shell所忽略的信号,但是不能继承父shell未忽略的信号。

#!/bin/bash

trap "" QUIT #忽略kill -3信号,并且子shell能继承父shell所忽略的信号
trap "echo 'You want to kill me'" TERM #父shell处理的信号,子shell不能继承
( #子shell,子进程号比父进程号大1
while :; do
let count=count+1
echo "This is the $count sleep"
sleep 5
done
)

img

父shell忽略QUIT信号但不忽略TERM信号,9987为父 shell进程号9988为子shell进程号,kill -3 9987向父shell发送3信号和kill -3 9988向子shell发送3信号,均未退出,可以看出子shell对QUIT的忽略是从父shell继承而来的。

kill 9987向父shell发送TERM信号,父shell仍存活(因处理了TERM信号),kill 9988向子shell发送TERM信号,子shell退出,随后父shell执行完毕结束。TERM信号能杀掉子shell,说明子shell不能继承 父shel未忽略的信号。

img

最后出现父shell响应TERM信号的输出,是因为子shell执行fork一个子进程后父shell处于等待状态,只有子shell退出后父shell才会被激活执行输出。

<⇧>