(035) Linux之其他命令

十年运维系列之基础篇 - Linux

作者:曾林 

联系:1494445739@qq.com

网站:www.jplatformx.com

版权:文章未经同意请勿转载


一、引言

  本小节中将谈论一些比较琐碎零散的知识。这些知识虽然不太经常使用,但是却对特定的程序设计问题大有帮助。下面将学习这方面的内容。

 

二、组命令和子shell

  bash允许将命令组合到一起使用,这有两种方式,一种是利用组命令,另一种是使用子shell。下面是这两种方式的语法实例。

  组命令:

{ command1; command2; [command3;...] }

  子shell:

(command1; command2; [command3;...])

  这两种形式的区别在于,组命令使用花括号将其命令括起来,而子shell则用圆括号。指的注意的是,在bash实现组命令时,必须使用一个空格将花括号与命令分开,并且在闭合花括号前使用分号或是换行来结束最后的命令。执行结果如下图所示:

 

1. 执行重定向

  组命令和子shell有什么用途呢?它们都可以用于管理重定向。下面让我们看一个在多个命令中执行重定向的脚本段。

1 ls -al > output.txt
2 echo "Listing of foo.txt" >> output.txt
3 cat foo.txt >> output.txt

  显而易见,3条命令将输出重定向为output.txt文件。使用组命令,可以按照如下的方式编码。

{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
(ls -l; echo "Listing of foo.txt"; cat foo.txt;) > output.txt

  使用这个技术,可以减少一些输入,但是组命令或子shell真正有价值的地方在于管道的使用。当创建命令管道时,通常将多条命令的结果输出到一条流中,这很有用。组命令和子shell使得这一点变得简单。

 

2. 进程替换

  虽然组命令和子shell看起来相似,都可以用来为重定向整合流,但是,它们有一处主要的不同。子shell(正如名字所示)在当前shell的子拷贝中执行命令,而组命令在当前shell里执行所有命令。这意味着子shell复制当前的环境变量以创建一个新的shell实例。当子shell退出时,复制的环境变量也就消失了,因此,任何对子shell的环境(包括变量赋值)的改变也同样丢失了。所以,大多数情况下,除非脚本需要子shell,否则组命令比子shell更可取。组命令更快,并且需要更少的内存。

  在前面其实我们已经接触到了子shell环境存在问题的一个实例,管道中的read命令没有像先前预期的那样工作。我们可以采用如下的方式重建管道。

echo "foo" | read
echo $REPLY

  REPLY变量的内容总是为空,因为read命令是在子shell中执行的,并且当子shell终止的时候,REPLY的拷贝也遭到了破坏。

  由于总是在子shell中执行管道中的命令,任何变量赋值的命令都会遇到这个问题。很幸运地是,shell提供了一种叫做进程替换的外部扩展方式来解决这个问题。

  实现进程替换有两种方式,一种是产生标准输出的进程,如下所示:

<(list)

  另一种是吸纳标准输入的进程,如下所示:

>(list)

  这里的list是一系列的命令。

  为了解决上述read命令的问题,我们可以像这样使用进程替换。

read < <(echo "foo")
echo $REPLY

  进程替换允许将子shell的输出当做一个普通的文件,目的是为了重定向。事实上,这是一种扩展形式,我们可以查看它的真实值。

  通过使用echo来查看扩展结果,可以看到文件/dev/fd/63正为子shell提供输出。

 

三、trap

  在以前的章节中我们了解了程序如何响应信号。同样我们也可以将这种功能应用到脚本中。虽然到目前为止,我们所写的脚本还不需要这种功能(因为它们有着更短的执行时间,并且不产生临时文件),但是,拥有一个信号处理程序可能对庞大复杂的脚本大有裨益。

  当设计一个庞大复杂的脚本时,我们一定要考虑到,如果在脚本正在运行时,用户注销或是关闭电脑时会发生什么情况。当这样的情况发生时,将会把信号发送到所有受影响的进程。相应地,执行那些进程的程序能够通过一些操作来保证程序合理有序的结束。设想一下,比如说,脚本在执行的时候创建了一个临时文件。在一个好的设计中,当脚本结束工作时,该临时文件会自动删除。如果接收的信号表明将要过早的结束程序,这个时候让脚本删除这个文件也是很明智的。

  为了实现这个目的,bash提供了一种trap机制。内置命令trap可以恰如其分地实现trap机制。trap命令使用的语法如下:

trap argument signal [signal...]

  这里的argument是作为命令被读取的字符串,而signal是对信号量的说明,该信号量将会触发解释命令的执行。下面是一个简单的例子:

 1 #!/bin/bash
 2 
 3 #trap-demo: simple signal handling demo
 4 
 5 trap "echo 'I am ignoring you.'" SIGINT SIGTERM
 6 
 7 for i in {1..5}; do
 8     echo "Iteration $i of 5"
 9     sleep 5
10 done

  每当运行中的脚本接受到SIGINT或者SIGTERM信号时,脚本定义的trap将执行echo命令。当用户通过按下ctrl+c键来试图结束脚本时,程序的执行情况如下。

  可以看出,每次用户试图中断程序时,都会输出这样的信息。

  构造一个字符串来形成一系列有用的命令看起来很笨拙,因此,通常的做法是指定shell函数来代替命令。在下面的例子中,我们为每个将要处理的信号指定一个独立的shell函数。代码如下:

 1 #!/bin/bash
 2 
 3 # foo.sh: simple signal handling demo
 4 
 5 exit_on_signal_SIGINT()    {
 6     echo "Script interrupted." 2>&1
 7     exit 0
 8 }
 9 
10 exit_on_signal_SIGTERM()    {
11     echo "Script terminated."  2>&1
12     exit 0
13 }
14 
15 trap exit_on_signal_SIGINT SIGINT
16 trap exit_on_signal_SIGTERM SIGTERM
17 
18 for i in {1..5}; do
19     echo "Iteration $i of 5"
20     sleep 5
21 done

  这个脚本为两个不同的信号定义了相应的trap命令。当接收到特定信号的时候,每个trap相应地执行指定的shell函数。注意到每个信号处理函数中的exit命令。如果没有exit命令,脚本会循环执行该函数。

 

四、异步执行

  有的时候我们希望执行多项任务。众所周知,所有现代的操作系统即便不是多用户系统,至少也是多任务系统。脚本可以在多任务中运行。

  这涉及到父脚本以及一个或多个子脚本的加载问题,子脚本可以在父脚本运行时执行其他额外的任务。但是,当一系列脚本以这种方式运行的时候,保持父脚本与子脚本的协调一致就会是一个问题。也就是说,试想这样一种情况,如果父脚本与子脚本彼此依赖,一个脚本必须等待另一个脚本任务完成之后才能继续完成自己的任务。

  bash提供了一个内置的命令来帮助管理异步执行。wait命令可以让父脚本暂停,直到指定的进程(比如子脚本)结束。

 

1. wait命令

  首先我们来演示wait命令。为此我们需要两个脚本来完成这个过程。下面首先是一个父脚本:

 1 #!/bin/bash
 2 
 3 # async-parent: Asynchronous execution demo(parent)
 4 
 5 echo "Parent: starting..."
 6 echo "Parent: launching child script..."
 7 async-child.sh &
 8 pid=$!
 9 echo "Parent: child (PID=$pid) launched..."
10 
11 echo "Parent: continuing..."
12 sleep 2
13 
14 echo "Parent: pausing to wait for child to finish..."
15 wait $pid
16 
17 echo "Parent: child is finished. Continuing..."
18 echo "Parent: parent is done. Exiting..."

  下面是子脚本:

1 #!/bin/bash
2 
3 # async-child: Asynchronous execution demo (child)
4 
5 echo "Child: child is running..."
6 sleep 5
7 echo "Child: child is done. Exiting..."

  在这个例子中,我们可以看出子脚本内容非常简单,父脚本执行实际的操作。在父脚本中,子脚本加载并在后台运行。通过将$! shell程序参数值赋值给pid变量来记录子脚本的进程ID,该参数值总是包含后台中最后一次运行的进程ID。

  父脚本继续执行,随后执行带有子进程PID的wait命令。这会导致父脚本暂停,直到子脚本退出;子脚本退出后,父脚本也结束。

 

五、命名管道

  大多数类UNIX系统支持创建一个叫做命名管道的特殊类型的文件。使用命名管道可以建立两个进程之间的通信,并且可以像其他类型的文件一样使用。虽然它们没有其他类型的文件那么受欢迎,但是它们仍然值得了解。

  客户端/服务器模式是一种常见的程序设计结构。它可以使用命名管道这样的通信方式,也可以使用网络连接这样的进程间通信方式。

  使用客户端/服务器程序设计架构最为广泛的地方当然是Web服务器与Web浏览器之间的通信。Web浏览器充当客户机,客户机对服务器提出请求,服务器通过网页的形式对浏览器做出回应。

  命名管道的工作方式与文件雷同,但实际上是两块先进先出(FIFO)的缓冲区。与普通的(未命名的)管道一样,数据从一端进入,从另一端出来。使用命名管道,也可以以如下方式设置。

process1 > named_pipe 以及 process2 < named_pipe

  以上的执行效果等效于如下语句:

process1 | process2

 

1. 设置命名管道

  首先,我们必须创建一个命名管道。使用mkfifo命令即可完成。如下所示:

   这里使用mkfifo命令创建一个名为pipe1的命名管道。使用ls命令查看文件属性,可以看到属性字段的第一个字母是p,这表明它是一个命名管道。

 

2. 使用命名管道 

  为了说明命名管道是如何工作的,我们需要两个终端窗口(或者两个虚拟的控制台)。在第一个终端中,我们输入一条简单的命令,并将其输出重新定位到这个命名管道,如下所示: 

   当按下enter键之后,这个命令看起来像是挂起来了。这是因为从管道的另一端还没有接收到数据。当这种情况发生时,也就是说命名管道被堵塞。一旦将一个进程连接到管道的另一端,情况就会有所改变,进程会从管道中读取数据。使用第二种终端窗口,输入如下命令:

cat < pipe1

  第一个终端窗口产生的目录列表作为cat命令的输出出现在第二个终端窗口。一旦它不处于阻塞状态,第一个终端窗口中的ls命令就可以成功完成。

 

posted @ 2015-03-24 11:08  jplatformx  阅读(222)  评论(0编辑  收藏  举报