1. 当前进程和子进程
tt() {
echo 'in tt'
exit 2
}
#tt # ①输出: in tt
t=`tt` # ②输出: main
echo 'main'
①中函数直接在当前主进程中被调用,函数tt退出整个进程就退出了
②中函数被调用的结果被赋值给变量t,函数是在子进程中被调用的,子进程的退出不会影响主进程的退出,所以最终执行了echo 'main'
实例,函数的返回值和返回状态码:
num=$1
tt() {
echo 'it is tt'
if [ $num -eq 1 ];then
exit 2
else
exit 3
fi
}
t=`tt` #在子进程中调用,拿到函数的结果和返回状态码
return_tag=$? #状态码
echo "t: ${t}, return_tag: ${return_tag}"
如上,tt函数有返回值,并且状态码为2或者3(根据用户的参数)
执行函数得到如下输出结果:
[jinbo2@192 test]$ ./t2.sh 2
t: it is tt, return_tag: 3
[jinbo2@192 test]$ ./t2.sh 3
t: it is tt, return_tag: 3
[jinbo2@192 test]$ ./t2.sh 1
t: it is tt, return_tag: 2
通过t=
tt
& $?,拿到了函数tt的返回结果和状态码
2. 文件锁
当多个进程同时对同一个文件进行写入修改的时候,修改不成功或者产生脏数据的问题,有没有一种机制,让许多并行的任务能串行进行一系列操作
2.1 引子:让程序保持单实例运行
有的脚本我们希望同一时间只有一个实例在运行,可以这样:
lock_file=/var/run/rsync_files.pid
if [ -e $lock_file ];then
echo "Other instance is running!"
exit 0
else
touch $lock_file
chmod 600 $lock_file
fi
trap "rm -f ${lock_file}; exit" 0 1 2 3 9 15
....
通过判断文件是否存在来使脚本继续走下去
trap "rm -f ${lock_file}; exit" 0 1 2 3 9 15的意义:因为脚本是通过判断文件而运行的,我们必须保证当服务器上没有该实例时这个文件不能存在,所以**当程序不是自己退出的时候,trap能够捕获这些异常信号并删除这个文件
2.2 文件夹锁(不推荐!实际生产并没有达到要求)
有这样一个问题:我有一个定时任务脚本,脚本步骤的最后都是把检查的结果最后修改写入到一个文件里,这个脚本有很多定时任务并且很多任务是同一个时间运行的,我们发现那些同时运行的任务的结果并没有都写入到那个文件里面去,而部分又是正常的
有没有一种机制,能让许多并行的脚本能串行进行一系列操作。比如多个脚本对文件进行依次读写,以免产生脏数据。无疑,如果脚本如果shell中有一种“锁”的机制,就可以解决这个问题。原来只使用过API级别的锁,脚本中的锁还真没用过。其实,如果要在shell脚本实现锁,需要满足两个条件:
这句话出处: https://blog.goquxiao.com/posts/2013/08/31/filelock_in_script/
- 一个全局可见的状态
- 一种“检测 + 加锁”的原子操作
上面的2.1小结,通过检查文件是否存在并不能满足我们的需求,因为检测文件和新建文件无法做到原子性。我们手动对脚本进行启动的时候不存在那个问题,但是当同时启动多个进程的时候(比如定时任务或多进程),其并不能满足
检测文件和新建文件无法做到原子性,但是mkdir操作,却能做到原子地检测文件夹和创建文件夹:
........
## ------------------------
while :
do
# 加锁
mkdir ${LOCK_DIR}
if [ $? -eq 0 ];then
break
fi
done
trap "rm -rf ${LOCK_DIR}; exit" 0 1 2 3 4 9 15
## ----------------------保证这块区域的操作是顺序执行的----------
tag=`check`
return_tag=$? # return_tag=0或非0
t=`date +%F' '%T`
if [ $return_tag -eq 0 ];then
warning="${business_warning},checktime: ${t}"
to_beita "${tag}" "${warning}"
elif [ $return_tag -eq 2 ];then
warning="配置文件错误,checktime: ${t}"
to_beita 'configError' "${warning}"
elif [ $return_tag -eq 3 ];then
warning="sql执行失败,checktime: ${t}"
to_beita 'sqlError' "${warning}"
fi
# ----------------------------------------------------------------------------------------
# 解锁
rm -rf ${LOCK_DIR}
当脚本想对于竞争数据进行操作,就mkdir某个文件夹,根据返回码得知申请所是否成功,申请成功、完成操作之后再rm -rf就可以实现了。
同样使用trap捕获异常退出
更简单的理解‘原子性’: 多个进程mkdir同时只会有一个能成功,而多个进程判断文件是否存在或者touch文件是能够同时成功的
2.3 flock命令,更高级的用法(强烈推荐)
flock - Manage locks from shell scripts
flock [-sxon] [-w timeout] lockfile [-c] command...
flock [-sxon] [-w timeout] lockdir [-c] command...
flock [-sxun] [-w timeout] fd
-s 为共享锁,在定向为某文件的FD上设置共享锁而未释放锁的时间内,其他进程试图在定向为此文件的FD上设置独占锁的请求失败,而其他进程试图在定向为此文件的FD上设置共享锁的请求会成功。
-e 为独占或排他锁,在定向为某文件的FD上设置独占锁而未释放锁的时间内,其他进程试图在定向为此文件的FD上设置共享锁或独占锁都会失败。只要未设置-s参数,此参数默认被设置。
-u 手动解锁,一般情况不必须,当FD关闭时,系统会自动解锁,此参数用于脚本命令一部分需要异步执行,一部分可以同步执行的情况。
-n 为非阻塞模式,当试图设置锁失败,采用非阻塞模式,直接返回1,并继续执行下面语句。
-w 设置阻塞超时,当超过设置的秒数,就跳出阻塞,返回1,并继续执行下面语句。
-o 必须是使用第一种格式时才可用,表示当执行command前关闭设置锁的FD,以使command的子进程不保持锁。
-c 执行其后的comand。
使脚本同时只运行一个实例:
LOCKFILE=lockfile
exec 9 >${LOCKFILE}
flock -n -e 9 # 设置锁失败时,返回1;设置锁成功时,返回0
if [ $? -eq 1 ];then
echo "There is another script running, exit"
exit
fi
让许多并行的任务能串行进行一系列操作:
#!/bin/bash
basepath=$(cd `dirname $0`;pwd)
filename=${basepath}/file_be_writed
LOCK_FILE=${basepath}/lockfile
#LOCK_DIR
#while :
#do
# mkdir ${LOCK_DIR}
# if [ $? -eq 0 ];then
# break
# fi
#done
#trap "rm -rf ${LOCK_DIR}; exit" 0 1 2 3 4 9 15
exec 9>${LOCK_FILE}
flock -e -w 15 9 #等待LOCK_FILE释放锁,阻塞等待15s
sed -i "$1c $2" $filename
#rm -rf ${LOCK_DIR}
这个用法明显比通过创建目录来判断(死循环)更加高效和简洁
直接对脚本要修改的文件加锁不好,即不要直接exec 9>${filename},不解释
通过在脚本里面对文件加锁来保持部分操作的串行是很棒的,我们还可以通过在脚本执行的时候使用flock命令,比如: flock -e -w 15 {锁文件} -c "{脚本路径}"
,但是这种明显不够好,因为它阻塞了整个脚本的运行,而我们这种是能对指定的一些步骤加锁的。
3. 子进程和管道
3.1 组命令和子进程
参考文档: https://www.cnblogs.com/mianbaoshu/p/12069777.html
组命令:
子进程:
Shell 脚本是从上至下、从左至右依次执行的,即执行完一个命令之后再执行下一个。如果在 Shell 脚本中遇到子脚本(即脚本嵌套,但是必须以新进程的方式运行)或者外部命令,就会向系统内核申请创建一个新的进程,以便在该进程中执行子脚本或者外部命令,这个新的进程就是子进程。子进程执行完毕后才能回到父进程,才能继续执行父脚本中后续的命令及语句。
子进程总结:
子 Shell 虽然能使用父 Shell 的的一切,但是如果子 Shell 对数据做了修改,比如修改了全局变量,这种修改也只能停留在子 Shell,无法传递给父 Shell。不管是子进程还是子 Shell,都是“传子不传父”
子 Shell 才是真正继承了父进程的一切,这才像“一个模子刻出来的”;普通子进程和父进程是完全不同的两个程序,只是维持着父子关系而已。
3.2 管道
包含管道的shell命令,则管道前后的命令分别由不同的进程执行,然后通过管道把两个进程的标准输入输出连接起来,就实现了管道。
例如:
grep "error" minicom.log | awk '{print $1}'
这句话的作用非常简单
通过grep命令在minicom.log中检索含有error关键字的行
通过awk命令打印grep的输出结果中每一行的第一个字段
在shell中要实现这样的效果,有4个步骤:
- 创建pipe
- fork两个子进程执行grep和awk命令
- 把grep子进程的标准输出、标准错误重定向到管道数据入口
- 把awk子进程的标准输入重定向到管道数据入口
这样就实现了shell管道:grep把结果输出到管道,awk从管道获取数据
管道是如何执行的?
先看下面的测试:
[yw@manager2 ~]$ ps aux | grep sshd
root 933 0.0 0.0 106052 676 ? Ss Jan25 0:00 /usr/sbin/sshd -D
root 52152 0.0 0.0 148372 1680 ? Ss 15:04 0:03 sshd: root@pts/0,pts/1
root 56181 0.2 0.1 148372 5396 ? Ss 17:23 0:00 sshd: root@pts/2
yw 56270 0.0 0.0 112812 968 pts/2 R+ 17:24 0:00 grep --color=auto sshd
[yw@manager2 ~]$
[yw@manager2 ~]$ ps aux | (sleep 2;grep sshd)
root 933 0.0 0.0 106052 680 ? Ss Jan25 0:00 /usr/sbin/sshd -D
root 52152 0.0 0.0 148372 1680 ? Ss 15:04 0:03 sshd: root@pts/0,pts/1
root 56181 0.2 0.1 148372 5396 ? Ss 17:23 0:00 sshd: root@pts/2
[yw@manager2 ~]$
一个显示 grep sshd 的进程,一个不显示
参考一个文档: http://m.blog.chinaunix.net/uid-7692530-id-2567612.html
该文档中有这样一段话:Pipelined processes do not execute one after another, or one before another. They are all executed at once. So, “ps aux | grep mysqld | wc -l” immediately spawns ps, grep, wc, then sets the standard output of one to the standard input of the other
也就是说:管道子进程不会一个接一个地执行,它们全部立即执行。因此,“ ps aux | grep mysqld | wc -l”会立即生成ps,grep,wc,然后将一个的标准输出设置为另一个的标准输入
3.3 用命名管道代替普通管道
cd ${src}
/usr/local/inotify-tools/bin/inotifywait -mrq --format '%e %w%f' -e create,attrib,close_write,move ./ | while read file
do
......
done
从上面的讨论中我们知道,上面的脚本将产生两个子进程(普通管道),有没有办法改变脚本进程的结构呢?
先看一个简单的示例,基于生产消费者模型,使用命名管道:
#! /bin/bash
tmp_file=/tmp/$$.fifo
mkfifo $tmp_file
exec 8<>${tmp_file}
rm -f ${tmp_file}
echo '23' >&8
echo '11' >&8
echo hello
while read line
do
echo $line
done <&8
执行脚本依次打印hello 23 11,并等待(管道没有结束)
是的,我们使用命名管道代替普通管道,这样就不用同时创建两个子进程了:
# 使用命名管道代替管道
tmp_file=/tmp/$$.fifo
mkfifo $tmp_file
exec 8<>${tmp_file}
rm -f ${tmp_file}
cd ${src}
{
/usr/local/inotify-tools/bin/inotifywait -mrq --format '%e %w%f' -e create,attrib,close_write,move ./ >&8
}&
while read file
do
......
done <&8
inotify将监听到的事件写入到命名管道(生产者,放入后台)
while循环从管道中一直读取处理数据(消费者)
通过pstree查看当前脚本运行后的进程树: