LinuxShell编程之常用技巧前言本文集中介绍了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-pc0bash]$ls-l/dev/sdabrw-rw----1rootdisk8,05月1210:47/dev/sda设备文件的主设备号对应了这种设备所使用的驱动是哪个,而辅助设备号则表示使用同一种驱动的设备编号。我们可以使用mknod命令手动创建一个设备文件:[zorro@zorrozou-pc0bash]$sudomknodharddiskb80[zorro@zorrozou-pc0bash]$ls-lharddiskbrw-r--r--1rootroot8,05月1809:49harddisk这样我们就创建了一个设备文件叫harddisk,实际上它跟/dev/sda是同一个设备,因为它们对应的设备驱动和编号都一样。所以这个设备实际上是跟sda相同功能的设备。系统还给我们提供了几个有特殊功能的设备文件,在bash编程的时候可能会经常用到:/dev/null:黑洞文件。可以对它重定向如何输出。/dev/zero:0发生器。可以产生二进制的0,产生多少根使用时间长度有关。我们经常用这个文件来产生大文件进行某些测试,如:[zorro@zorrozou-pc0bash]$ddif=/dev/zeroof=./bigfilebs=1Mcount=10241024+0recordsin1024+0recordsout1073741824bytes(1.1GB,1.0GiB)copied,0.3501s,3.1GB/sdd命令也是我们在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-pc0bash]$cattcp.sh#!/bin/bashipaddr=127.0.0.1port=80if!exec5/dev/tcp/$ipaddr/$portthenexit1fiecho-eGET/HTTP/1.0\n&5cat&5ipaddr的部分还可以写一个主机名。大家可以用此脚本分别在本机打开web服务和不打开的情况下分别执行观察是什么效果。/proc是另一个我们经常使用的目录。这个目录完全是内核虚拟的。内核将一些系统信息都放在/proc目录下一文件和文本的方式显示出来,如:/proc/cpuinfo、/proc/meminfo。我们可以使用man5proc来查询这个目录下文件的作用。函数和递归我们已经接触过函数的概念了,在bash编程中,函数无非是将一串命令起了个名字,后续想要调用这一串命令就可以直接写函数的名字了。在语法上定义一个函数的方法是:name()compound-command[redirection]functionname[()]compound-command[redirection]我们可以加function关键字显式的定义一个函数,也可以不加。函数在定义的时候可以直接在后面加上重定向的处理。这里还需要特殊说明的是函数的参数处理和局部变量,请看下面脚本:[zorro@zorrozou-pc0bash]$catfunction.sh|awk'{print\t$0}'#!/bin/bashaaa=1000arg_proc(){echoFunctionbegin:localaaa=2000echo$1echo$2echo$3echo$*echo$@echo$aaaechoFunctionend!}echoScriptbugin:echo$1echo$2echo$3echo$*echo$@echo$aaaarg_procaaabbbcccdddeeefffecho$1echo$2echo$3echo$*echo$@echo$aaaechoScriptend!我们带-x参数执行一下:+aaa=1000+echo'Scriptbugin:'Scriptbugin:+echo111111+echo222222+echo333333+echo111222333444555111222333444555+echo111222333444555111222333444555+echo10001000+arg_procaaabbbcccdddeeefff+echo'Functionbegin:'Functionbegin:+localaaa=2000+echoaaaaaa+echobbbbbb+echocccccc+echoaaabbbcccdddeeefffaaabbbcccdddeeefff+echoaaabbbcccdddeeefffaaabbbcccdddeeefff+echo20002000+echo'Functionend!'Functionend!+echo111111+echo222222+echo333333+echo111222333444555111222333444555+echo111222333444555111222333444555+echo10001000+echo'Scriptend!'Scriptend!观察整个执行过程可以发现,函数的参数适用方法跟脚本一样,都可以使用*、$@这些符号来处理。而且函数参数跟函数内部使用local定义的局部变量效果一样,都是只在函数内部能看到。函数外部看不到函数里定义的局部变量,当函数内部的局部变量和外部的全局变量名字相同时,函数内只能取到局部变量的值。当函数内部没有定义跟外部同名的局部变量的时候,函数内部也可以看到全局变量。bash编程支持递归调用函数,跟其他编程语言不同的地方是,bash还可以递归的调用自身,这在某些编程场景下非常有用。我们先来看一个递归的简单例子:[zorro@zorrozou-pc0bash]$catrecurse.sh#!/bin/bashread_dir(){foriin$1/*doif[-d$i]thenread_dir$ielseecho$ifidone}read_dir$1这个脚本可以遍历一个目录下所有子目录中的非目录文件。关于递归,还有一个经典的例子,fork炸弹:.(){.|.&};.这一堆符号看上去很令人费解,我们来解释一下每个符号的含义:根据函数的定义语法,我们知道.(){}的意思是,定义一个函数名子叫“.”。虽然系统中又个内建命令也叫.,就是source命令,但是我们也知道,当函数和内建命令名字冲突的时候,bash首先会将名字当成是函数来解释。在{}包含的函数体中,使用了一个管道连接了两个点,这里的第一个.就是函数的递归调用,我们也知道了使用管道的时候会打开一个subshell的子进程,所以在这里面就递归的打开了子进程。{}后面的分号只表示函数定义完毕的结束符,在之后就是调用函数名执行的.,之后函数开始递归的打开自己,去产生子进程,直到系统崩溃为止。bash并发编程和flock在shell编程中,需要使用并发编程的场景并不多。我们倒是经常会想要某个脚本不要同时出现多次同时执行,比如放在crond中的某个周期任务,如果执行时间较长以至于下次再调度的时间间隔,那么上一个还没执行完就可能又打开一个,这时我们会希望本次不用执行。本质上讲,无论是只保证任何时候系统中只出现一个进程还是多个进程并发,我们需要对进程进行类似的控制。因为并发的时候也会有可能产生竞争条件,导致程序出问题。我们先来看如何写一个并发的bash程序。在前文讲到作业控制和wait命令使用的时候,我们就已经写了一个简单的并发程序了,我们这次让它变得复杂一点。我们写一个bash脚本,创建一个计数文件,并将里面的值写为0。然后打开100个子进程,每个进程都去读取这个计数文件的当前值,并加1写回去。如果程序执行正确,最后里面的值应该是100,因为每个子进程都会累加一个1写入文件,我们来试试:[zorro@zorrozou-pc0bash]$catracing.sh#!/bin/bashcountfile=/tmp/countif![-f$countfile]thenecho0$countfilefido_count(){readcount$countfileecho$((++count))$countfile}foriin`seq1100`dodo_count&donewaitcat$countfilerm$countfile我们再来看看这个程序的执行结果:[zorro@zorrozou-pc0bash]$./racing.sh26[zorro@zorrozou-pc0bash]$./racing.sh13[zorro@zorrozou-pc0bash]$./racing.sh34[zorro@zorrozou-pc0bash]$./racing.sh25[zorro@zorrozou-pc0bash]$./racing.sh45[zorro@zorrozou-pc0bash]$./racing.sh5多次执行之后,每次得到的结果都不一样,也没有一次是正确的结果。这就是典型的竞争条件引起的问题。当多个进程并发的时候,如果使用的共享的资源,就有可能会造成这样的问题。这里的竞争调教就是:当某一个进程读出文件值为0,并加1,还没写回去的时候,如果有别的进程读了文件,读到的还是0。于是多个进程会写1,以及其它的数字。解决共享文件的竞争问题的办法是使用文件锁。每个子进程在读取文件之前先给文件加锁,写入之后解锁,这样临界区代码就可以互斥执行了:[zorro@zorrozou-pc0bash]$catflock.sh#!/bin/bashcountfile=/tmp/countif![-f$countfile]thenecho0$countfilefido_count(){exec3$countfile#对三号描述符加互斥锁flock-x3read-u3countecho$((++count))$countfile#解锁flock-u3#关闭描述符也会解锁exec3&-}foriin`seq1100`dodo_count&donewaitcat$countfilerm$countfile[zorro@zorrozou-pc0bas