Linux Shell编程之常用技巧

时间:2021-02-15 16:08:47

Linux Shell编程之常用技巧


前言


本文集中介绍了bash编程中部分高级编程方法和技巧。通过学习本文内容,可以帮你解决以下问题:

1、bash可以网络编程么?

2、.(){ .|.& };. 据说执行这些符号可以死机,那么它们是啥意思?

3、你是什么保证crond中的任务不重复执行的?grep一下然后wc算一下个数么?

4、受限模式执行bash可以保护什么?

5、啥时候会出现subshell?

6、coproc协进程怎么用?

/dev和/proc目录

dev目录是系统中集中用来存放设备文件的目录。除了设备文件以外,系统中也有不少特殊的功能通过设备的形式表现出来。设备文件是一种特殊的文件,它们实际上是驱动程序的接口。在Linux操作系统中,很多设备都是通过设备文件的方式为进程提供了输入、输出的调用标准,这也符合UNIX的“一切皆文件”的设计原则。所以,对于设备文件来说,文件名和路径其实都不重要,最重要的使其主设备号和辅助设备号,就是用ls -l命令显示出来的原本应该出现在文件大小位置上的两个数字,比如下面命令显示的8和0:

 

[zorro@zorrozou-pc0 bash]$ ls -l /dev/sdabrw-rw---- 1 root disk 8, 0 5月  12 10:47 /dev/sda

设备文件的主设备号对应了这种设备所使用的驱动是哪个,而辅助设备号则表示使用同一种驱动的设备编号。我们可以使用mknod命令手动创建一个设备文件:

 

[zorro@zorrozou-pc0 bash]$ sudo mknod harddisk b 8 0 [zorro@zorrozou-pc0 bash]$ ls -l harddisk brw-r--r-- 1 root root 8, 0 5月  18 09:49 harddisk

这样我们就创建了一个设备文件叫harddisk,实际上它跟/dev/sda是同一个设备,因为它们对应的设备驱动和编号都一样。所以这个设备实际上是跟sda相同功能的设备。

系统还给我们提供了几个有特殊功能的设备文件,在bash编程的时候可能会经常用到:

/dev/null:黑洞文件。可以对它重定向如何输出。

/dev/zero:0发生器。可以产生二进制的0,产生多少根使用时间长度有关。我们经常用这个文件来产生大文件进行某些测试,如:

 

[zorro@zorrozou-pc0 bash]$ dd if=/dev/zero of=./bigfile bs=1M count=10241024+0 records in1024+0 records out1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.3501 s, 3.1 GB/s

dd命令也是我们在bash编程中可能会经常使用到的命令。

/dev/random:Linux下的random文件是一个根据计算机背景噪声而产生随机数的真随机数发生器。所以,如果容纳噪声数据的熵池空了,那么对文件的读取会出现阻塞。

/dev/urandom:是一个伪随机数发生器。实际上在Linux的视线中,urandom产生随机数的方法根random一样,只是它可以重复使用熵池中的数据。这两个文件在不同的类unix系统中可能实现方法不同,请注意它们的区别。

/dev/tcp & /dev/udp:这两个神奇的目录为bash编程提供了一种可以进行网络编程的功能。在bash程序中使用/dev/tcp/ip/port的方式就可以创建一个scoket作为客户端去连接服务端的ip:port。我们用一个检查http协议的80端口是否打开的例子来说明它的使用方法:

 

[zorro@zorrozou-pc0 bash]$ cat tcp.sh#!/bin/bashipaddr=127.0.0.1port=80if ! exec 5<> /dev/tcp/$ipaddr/$portthen    exit 1fiecho -e "GET / HTTP/1.0\n" >&5cat <&5

ipaddr的部分还可以写一个主机名。大家可以用此脚本分别在本机打开web服务和不打开的情况下分别执行观察是什么效果。

/proc是另一个我们经常使用的目录。这个目录完全是内核虚拟的。内核将一些系统信息都放在/proc目录下一文件和文本的方式显示出来,如:/proc/cpuinfo、/proc/meminfo。我们可以使用man 5 proc来查询这个目录下文件的作用。

函数和递归

我们已经接触过函数的概念了,在bash编程中,函数无非是将一串命令起了个名字,后续想要调用这一串命令就可以直接写函数的名字了。在语法上定义一个函数的方法是:

 

name () compound-command [redirection]function name [()] compound-command [redirection]

我们可以加function关键字显式的定义一个函数,也可以不加。函数在定义的时候可以直接在后面加上重定向的处理。这里还需要特殊说明的是函数的参数处理和局部变量,请看下面脚本:

 

[zorro@zorrozou-pc0 bash]$ cat function.sh |awk '{print "\t"$0}'#!/bin/bashaaa=1000arg_proc () {    echo "Function begin:"    local aaa=2000    echo $1    echo $2    echo $3    echo $*    echo $@    echo $aaa    echo "Function end!"}echo "Script bugin:"echo $1echo $2echo $3echo $*echo $@echo $aaaarg_proc aaa bbb ccc ddd eee fffecho $1echo $2echo $3echo $*echo $@echo $aaaecho "Script end!"

我们带-x参数执行一下:

 

+ aaa=1000+ echo 'Script bugin:'Script bugin:+ echo 111111+ echo 222222+ echo 333333+ echo 111 222 333 444 555111 222 333 444 555+ echo 111 222 333 444 555111 222 333 444 555+ echo 10001000+ arg_proc aaa bbb ccc ddd eee fff+ echo 'Function begin:'Function begin:+ local aaa=2000+ echo aaaaaa+ echo bbbbbb+ echo cccccc+ echo aaa bbb ccc ddd eee fffaaa bbb ccc ddd eee fff+ echo aaa bbb ccc ddd eee fffaaa bbb ccc ddd eee fff+ echo 20002000+ echo 'Function end!'Function end!+ echo 111111+ echo 222222+ echo 333333+ echo 111 222 333 444 555111 222 333 444 555+ echo 111 222 333 444 555111 222 333 444 555+ echo 10001000+ echo 'Script end!'Script end!

观察整个执行过程可以发现,函数的参数适用方法跟脚本一样,都可以使用*、$@这些符号来处理。而且函数参数跟函数内部使用local定义的局部变量效果一样,都是只在函数内部能看到。函数外部看不到函数里定义的局部变量,当函数内部的局部变量和外部的全局变量名字相同时,函数内只能取到局部变量的值。当函数内部没有定义跟外部同名的局部变量的时候,函数内部也可以看到全局变量。

bash编程支持递归调用函数,跟其他编程语言不同的地方是,bash还可以递归的调用自身,这在某些编程场景下非常有用。我们先来看一个递归的简单例子:

 

[zorro@zorrozou-pc0 bash]$ cat recurse.sh#!/bin/bashread_dir () {    for i in $1/*    do        if [ -d $i ]        then            read_dir $i        else            echo $i        fi    done}read_dir $1

这个脚本可以遍历一个目录下所有子目录中的非目录文件。关于递归,还有一个经典的例子,fork炸弹:

 

.(){ .|.& };.

这一堆符号看上去很令人费解,我们来解释一下每个符号的含义:根据函数的定义语法,我们知道.(){}的意思是,定义一个函数名子叫“.”。虽然系统中又个内建命令也叫.,就是source命令,但是我们也知道,当函数和内建命令名字冲突的时候,bash首先会将名字当成是函数来解释。在{}包含的函数体中,使用了一个管道连接了两个点,这里的第一个.就是函数的递归调用,我们也知道了使用管道的时候会打开一个subshell的子进程,所以在这里面就递归的打开了子进程。{}后面的分号只表示函数定义完毕的结束符,在之后就是调用函数名执行的.,之后函数开始递归的打开自己,去产生子进程,直到系统崩溃为止。

bash并发编程和flock

在shell编程中,需要使用并发编程的场景并不多。我们倒是经常会想要某个脚本不要同时出现多次同时执行,比如放在crond中的某个周期任务,如果执行时间较长以至于下次再调度的时间间隔,那么上一个还没执行完就可能又打开一个,这时我们会希望本次不用执行。本质上讲,无论是只保证任何时候系统中只出现一个进程还是多个进程并发,我们需要对进程进行类似的控制。因为并发的时候也会有可能产生竞争条件,导致程序出问题。

我们先来看如何写一个并发的bash程序。在前文讲到作业控制和wait命令使用的时候,我们就已经写了一个简单的并发程序了,我们这次让它变得复杂一点。我们写一个bash脚本,创建一个计数文件,并将里面的值写为0。然后打开100个子进程,每个进程都去读取这个计数文件的当前值,并加1写回去。如果程序执行正确,最后里面的值应该是100,因为每个子进程都会累加一个1写入文件,我们来试试:

 

[zorro@zorrozou-pc0 bash]$ cat racing.sh#!/bin/bashcountfile=/tmp/countif ! [ -f $countfile ]then    echo 0 > $countfilefido_count () {    read count < $countfile    echo $((++count)) > $countfile}for i in `seq 1 100`do     do_count &donewaitcat $countfilerm $countfile

我们再来看看这个程序的执行结果:

 

[zorro@zorrozou-pc0 bash]$ ./racing.sh 26[zorro@zorrozou-pc0 bash]$ ./racing.sh 13[zorro@zorrozou-pc0 bash]$ ./racing.sh 34[zorro@zorrozou-pc0 bash]$ ./racing.sh 25[zorro@zorrozou-pc0 bash]$ ./racing.sh 45[zorro@zorrozou-pc0 bash]$ ./racing.sh 5

多次执行之后,每次得到的结果都不一样,也没有一次是正确的结果。这就是典型的竞争条件引起的问题。当多个进程并发的时候,如果使用的共享的资源,就有可能会造成这样的问题。这里的竞争调教就是:当某一个进程读出文件值为0,并加1,还没写回去的时候,如果有别的进程读了文件,读到的还是0。于是多个进程会写1,以及其它的数字。解决共享文件的竞争问题的办法是使用文件锁。每个子进程在读取文件之前先给文件加锁,写入之后解锁,这样临界区代码就可以互斥执行了:

 

[zorro@zorrozou-pc0 bash]$ cat flock.sh#!/bin/bashcountfile=/tmp/countif ! [ -f $countfile ]then    echo 0 > $countfilefido_count () {    exec 3< $countfile    #对三号描述符加互斥锁    flock -x 3    read -u 3 count    echo $((++count)) > $countfile    #解锁    flock -u 3    #关闭描述符也会解锁    exec 3>&-}for i in `seq 1 100`do     do_count &donewaitcat $countfilerm $countfile[zorro@zorrozou-pc0 bash]$ ./flock.sh 100

对临界区代码进行加锁处理之后,程序执行结果正确了。仔细思考一下程序之后就会发现,这里所谓的临界区代码由加锁前的并行,变成了加锁后的串行。flock的默认行为是,如果文件之前没被加锁,则加锁成功返回,如果已经有人持有锁,则加锁行为会阻塞,直到成功加锁。所以,我们也可以利用互斥锁的这个特征,让bash脚本不会重复执行。

 

[zorro@zorrozou-pc0 bash]$ cat repeat.sh#!/bin/bashexec 3> /tmp/.lockif ! flock -xn 3then    echo "already running!"    exit 1fiecho "running!"sleep 30echo "ending"flock -u 3exec 3>&-rm /tmp/.lockexit 0

-n参数可以让flock命令以非阻塞方式探测一个文件是否已经被加锁,所以可以使用互斥锁的特点保证脚本运行的唯一性。脚本退出的时候锁会被释放,所以这里可以不用显式的使用flock解锁。flock除了-u参数指定文件描述符锁文件以外,还可以作为执行命令的前缀使用。这种方式非常适合直接在crond中方式所要执行的脚本重复执行。如:

 

*/1 * * * * /usr/bin/flock -xn /tmp/script.lock -c '/home/bash/script.sh'

关于flock的其它参数,可以man flock找到说明。

受限bash

以受限模式执行bash程序,有时候是很有必要的。这种模式可以保护我们的很多系统环境不受bash程序的误操作影响。启动受限模式的bash的方法是使用-r参数,或者也可以rbash的进程名方式执行bash。受限模式的bash和正常bash时间的差别是:

1、不能使用cd命令改变当前工作目录。

2、不能改变SHELL、PATH、ENV和BASH_ENV环境变量。

3、不能调用含有/的命令路径。

4、不能使用.执行带有/字符的命令路径。

5、不能使用hash命令的-p参数指定一个带斜杠\的参数。

6、不能在shell环境启动的时候加载函数的定义。

7、不能检查SHELLOPTS变量的内容。

8、不能使用>, >|, <>, >&, &>和 >>重定向操作符。

9、不能使用exec命令使用一个新程序替换当前执行的bash进程。

10、enable内建命令不能使用-f、-d参数。

11、不可以使用enable命令打开或者关闭内建命令。

12、command命令不可以使用-p参数。

13、不能使用set +r或者set +o restricted命令关闭受限模式。


测试一个简单的受限模式:

 

[zorro@zorrozou-pc0 bash]$ cat restricted.sh #!/bin/bashset -rcd /tmp[zorro@zorrozou-pc0 bash]$ ./restricted.sh ./restricted.sh: line 5: cd: restricted

subshell

我们前面接触过subshell的概念,我们之前说的是,当一个命令放在()中的时候,bash会打开一个子进程去执行相关命令,这个子进程实际上是另一个bash环境,叫做subshell。当然包括放在()中执行的命令,bash会在以下情况下打开一个subshell执行命令:

1、使用&作为命令结束提交了作业控制任务时。

2、使用|连接的命令会在subshell中打开。

3、使用()封装的命令。

4、使用coproc(bash 4.0版本之后支持)作为前缀执行的命令。

5、要执行的文件不存在或者文件存在但不具备可执行权限的时候,这个执行过程会打开一个subshell执行。

在subshell中,有些事情需要注意。subshell中的$$取到的仍然是父进程bash的pid,如果想要取到subshell的pid,可以使用BASHPID变量:

 

[zorro@zorrozou-pc0 bash]$ echo $$ ;echo $BASHPID && (echo $$;echo $BASHPID)54845484548424584

可以使用BASH_SUBSHELL变量的值来检查当前环境是不是在subshell中,这个值在非subshell中是0;每进入一层subshell就加1。

 

[zorro@zorrozou-pc0 bash]$ echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL))012

在subshell中做的任何操作都不会影响父进程的bash执行环境。subshell除了PID和trap相关设置外,其他的环境都跟父进程是一样的。subshell的trap设置跟父进程刚启动的时候还没做trap设置之前一样。

协进程coprocess

在bash 4.0版本之后,为我们提供了一个coproc关键字可以支持协进程。协进程提供了一种可以上bash移步执行另一个进程的工作模式,实际上跟作业控制类似。严格来说,bash的协进程就是使用作业控制作为实现手段来做的。它跟作业控制的区别仅仅在于,协进程的标准输入和标准输出都在调用协进程的bash中可以取到文件描述符,而作业控制进程的标准输入和输出都是直接指向终端的。我们来看看使用协进程的语法:

 

coproc [NAME] command [redirections]

使用coproc作为前缀,后面加执行的命令,可以将命令放到作业控制里执行。并且在bash中可以通过一些方法查看到协进程的pid和使用它的输入和输出。例子:

 

zorro@zorrozou-pc0 bash]$ cat coproc.sh#!/bin/bash#例一:简单命令使用#简单命令使用不能通过NAME指定协进程的名字,此时进程的名字统一为:COPROC。coproc tail -3 /etc/passwdecho $COPROC_PIDexec 0<&${COPROC[0]}-cat#例二:复杂命令使用#此时可以使用NAME参数指定协进程名称,并根据名称产生的相关变量获得协进程pid和描述符。coproc _cat { tail -3 /etc/passwd; }echo $_cat_PIDexec 0<&${_cat[0]}-cat#例三:更复杂的命令以及输入输出使用#协进程的标准输入描述符为:NAME[1],标准输出描述符为:NAME[0]。coproc print_username {    while read string    do        [ "$string" = "END" ] && break        echo $string | awk -F: '{print $1}'    done}echo "aaa:bbb:ccc" 1>&${print_username[1]}echo okread -u ${print_username[0]} usernameecho $usernamecat /etc/passwd >&${print_username[1]}echo END >&${print_username[1]}while read -u ${print_username[0]} usernamedo    echo $usernamedone

执行结果:

 

[zorro@zorrozou-pc0 bash]$ ./coproc.sh31953jerry:x:1001:1001::/home/jerry:/bin/bashsystemd-coredump:x:994:994:systemd Core Dumper:/:/sbin/nologinnetdata:x:134:134::/var/cache/netdata:/bin/nologin31955jerry:x:1001:1001::/home/jerry:/bin/bashsystemd-coredump:x:994:994:systemd Core Dumper:/:/sbin/nologinnetdata:x:134:134::/var/cache/netdata:/bin/nologinokaaarootbindaemonmailftphttpuuidddbusnobodysystemd-journal-gatewaysystemd-timesyncsystemd-networksystemd-bus-proxysystemd-resolvesystemd-journal-remotesystemd-journal-uploadpolkitdavahicolordrtkitgdmusbmuxgitgnome-initial-setupzorronvidia-persistencedntpjerrysystemd-coredumpnetdata

最后

本文主要介绍了一些bash编程的常用技巧,主要包括的知识点为:

1、/dev/和/proc目录的使用。

2、函数和递归。

3、并发编程和flock。

4、受限bash。

5、subshell。

6、协进程。


至此,我们的bash编程系列就算结束了。当然,shell其实到现在才刚刚开始。毕竟我们要真正实现有用的bash程序,还需要积累大量命令的使用。本文篇幅有限,就不探讨外部命令的详细使用方法和技巧了。希望这一系列内容对大家进一步深入了解bash编程有帮助。

抓住2017年最后一次免费学习运维技术的机会!Linux云计算12月免费课程火热开讲中,5天免费运维干货内容免费听,点击文末“阅读原文”即可免费听运维课速速来抢~~~~

PS:记得查收小编送你的免费大礼包呦~

免费送 | 1000多套简历模板免费拿,附赠简历制作教程!

免费领 | 《Shell脚本 100例》电子书免费拿,运维必备干货~


Linux Shell编程之常用技巧Linux Shell编程之常用技巧


▼▼点击【阅读原文】,免费听5天Linux运维干货分享课,火热开讲中,速来抢!