linuxshell之⽂件锁
经常在 shell 脚本⾥要阻⽌其它进程,⽐如 msmtp ⾃带的mail queue 脚本,这个脚本的互斥做法是不正确的,下⾯介绍下发现的三个通过⽂件达到互斥的正确做法。
1. util-linux 的 flock
这个命令有两种⽤法: flock LOCKFILE COMMAND ( flock -s 200; COMMAND; ) 200>LOCKFILEflock 需要保持打开锁⽂件,对于第⼆种使⽤⽅式并不⽅便,⽽且 -s ⽅式指定⽂件句柄可能冲突。好处是不需要显式的解锁,进程退出后锁必然释放。
2. liblockfile1 的 dotlockfile
号称最灵活可靠的⽂件锁实现。其等待时间跟 -r 指定的重试次数有关,重试时间为 sum(5, 10, ..., min(5*n, 60), ...).锁⽂件不需要保持打开, 带来的问题是需要⽤ trap EXIT 确保进程退出时删除锁⽂件.
3. procmail 的 lockfile
跟 dotlockfile 类似, 但可以⼀次性创建多个锁⽂件.
在SHELL中实现⽂件锁,有两种简单的⽅式。
⼀是利⽤普通⽂件,在脚本启动时检查特定⽂件是否存在,如果存在,则等待⼀段时间后继续检查,直到⽂件不存时创建该⽂件,在脚本结束时删除⽂件。为确保脚本在异常退出时⽂件仍然能被删除,可以借助于trap "cmd" EXIT TERM INT命令。⼀般这类⽂件存放在/var/lock/⽬录下,操作系统在启动时会对该⽬录做清理。自愿离职申请书
另⼀种⽅法是是使⽤flock命令。使⽤⽅式如下,这个命令的好处是等待动作在flock命令中完成,⽆需另外添加代码。
( flock 300 ... flock -u 300 ) > /tmp/file.lock
但flock有个缺陷是,在打开flock之后fork(),⼦进程也会拥有锁,如果在flock其间有运⾏daemon的话,必需确保daemon在启动时已经关闭了所有的⽂件句柄,不然该⽂件会因为daemon⼀直将其置于打开状态⽽⽆法解锁。
⼀个实现linux shell⽂件锁的例⼦
最近看到很多讨论如何能不让脚本重复执⾏的问题,实际就是⽂件锁的概念,写了⼀个⼩例⼦:
把这个作为⽂件开头不会产⽣重复执⾏的情况。(我想两个执⾏脚本的⽂件名⼀模⼀样应该不会经常出现吧)
#!/bin/bash
春饼的家常做法LockFile()
{
find/dev/shm/* -maxdepth 0 -type l -follow -exec unlink {} \;
[ -f /dev/shm/${0##*/}]&&exit
ln -s /proc/$$/dev/shm/${0##*/}
trap "Exit" 0 1 2 3 15 22 24
}
Exit()
{
unlink /dev/shm/${0##*/};
exit 0;
}
张学良公馆
LockFile
# main program
# program ......
#Exit
/var/lock/subsys⽬录的作⽤的说明
很多程序需要判断是否当前已经有⼀个实例在运⾏,这个⽬录就是让程序判断是否有实例运⾏的标志,⽐如说xinetd,如果存在这个⽂件,表⽰已经有xinetd在运⾏了,否则就是没有,当然程序⾥⾯还要有相应的判断措施来真正确定是否有实例在运⾏。
通常与该⽬录配套的还有/var/run⽬录,⽤来存放对应实例的PID,如果你写脚本的话,会发现这2个⽬录结合起来可以很⽅便的判断出许多服务是否在运⾏,运⾏的相关信息等等。
实际上,判断是否上锁就是判断这个⽂件,所以⽂件存在与否也就隐含了是否上锁。⽽这个⽬录的内容并不能表⽰⼀定上锁了,因为很多服务在启动脚本⾥⽤touch来创建这个加锁⽂件,在系统结束时该脚本负责清除锁,这本⾝就不可靠(⽐如意外失败导致锁⽂件仍然存在),我在脚本⾥⼀般是结合PID⽂件(如果有PID⽂件的话),从PID⽂件⾥得到该实例的PID,然后⽤ps测试是否存在该PID,从⽽判断是否真正有这个实例在运⾏,更加稳妥的⽅法是⽤进程通讯了,不过这样的话单单靠脚本就做不到了。
flock命令在我的系统属于util-linux-2.13-0.46.fc6包,如果没有此命令,尝试更新您系统下的util-linux包。
介绍此命令的原因:
论坛中曾有woodie兄写的脚本串⾏化的讨论,已经很完善了。
但flock此命令既与shell脚本结合的很好,⽽且与C/PERL/PHP等语⾔的flock函数⽤法很相似,使⽤起来也很简单。相⽐之下,woodie兄那篇的内容需要不浅的shell功底来理解。
两种格式分别为:
flock [-sxon] [-w timeout] lockfile [-c]
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。
举个实⽤的例⼦:
#!/bin/bash
{
flock -n 3
[ $? -eq 1 ] && { echo fail; exit; }
echo $$
sleep 10
} 3<>mylockfile
此例的功能为当有⼀个脚本实例正在执⾏时,另⼀个试图执⾏该脚本的进程会失败退出。
sleep那句可以换成您需要执⾏的语句段。
这⾥请注意⼀点,我使⽤<>打开mylockfile,原因是定向⽂件描述符是先于命令执⾏的。因此假如在
您要执⾏的语句段中需要读写mylockfile⽂件,例如想获得上⼀个脚本实例的pid,并将此次的脚本实例的pid写⼊mylockfile。此时直接⽤>打开mylockfile会清空上次存⼊的内容,⽽⽤<;打开mylockfile当它不存在时会导致⼀个错误。当然这些问题都可以⽤其他⽅法解决,我只是点出这种最通⽤的⽅法。
【背景介绍】
CU上曾经有⼏个帖⼦讨论到⼀个实际问题,就是如何限制同⼀时刻只允许⼀个脚本实例运⾏。其中本版新⽼斑⽵和其它⽹友都参加了讨论,但以faintblue兄的帖⼦对⼤家启发最⼤,下⾯的背景介绍中许多内容都是来⾃于他。在此感谢faintblue兄,也感谢斑⽵和其它朋友!
woodie总结了⼀下现有的结果,⼤体上可以分为两种思路:
⼀、简单的⽅法是,⽤ps⼀类命令找出已经运⾏脚本的数量,如果⼤于等于2(别忘了把⾃⼰也算进去^_^),就退出当前脚本,等于1,则运⾏。这种⽅法简单是简单,不过有⼀些问题:
⾸先,ps取得脚本⽂件进程数量就有很多陷阱,例如有时⽆法ps到脚本⽂件的名称;
即使可以ps到脚本名,如果⽤到管道的话,由于⼦shell的原因,在⼤多数平台下会得到奇怪的结果,有时得到数字a,有时⼜得到数字b,让⼈⽆所适从;
就算计数的问题已经解决了,还有问题,不过不太严重:如果两个脚本实例同时计数,显然数字都应该等于2,于是两个都退出了。于是在这⼀时间点上没有⼀个脚本在执⾏;
⼆、加锁的⽅法。就是脚本在执⾏开始先试图得到⼀个“锁”,得到则继续执⾏,反之就退出。
加锁⽅法也存在⼀些问题,主要集中在两个⽅⾯:
其⼀,加锁时如何避免竞态条件(race condition)。即如何找到⼀些“原⼦”操作,使得加锁的动作⼀步完成,中间不能被打断。否则就可能出现下⾯的情况:
脚本1检测到没有锁被占⽤;
然后脚本2也检测到没有锁被占⽤;
脚本1加锁,开始执⾏;
然后脚本2(错误地)加锁,也开始执⾏;
看到吗,两个脚本在同时执⾏。:(
可能的⼀些加锁的“原⼦”操作有:
1.创建⽬录,当⼀个进程创建成功后其它进程都会失败;
2.符号链接:ln -s,⼀个链接创建后其它进程的ln -s命令会出错;
3.⽂件⾸⾏的竞争,多个进程以append的⽅式同时写到⽂件,只有惟⼀⼀个进程写到了⽂件的第⼀⾏,因为不可能有两个第⼀⾏。^_^
4.其它软件包的加锁⼯具,通常是c语⾔⼆进制程序,⾃⼰写的也⾏。
⽬前加锁时的问题已经可以解决。
其⼆,找到⼀种⽅法避免出现“死锁”的情况,这⾥是指:虽然“锁”被占⽤,但却没有脚本在执⾏。这通常在脚本意外退出,来不及释放占⽤的“锁”之后。如收到⼀些系统信号后退出,机器意外掉电后退出等。
对于前者的情况,可以⽤trap捕获⼀些信号,在退出前释放锁;但有些信号是⽆法捕获的。
对于后者,可以在机器重起后⽤脚本⾃动删除锁来解决。不过有点⿇烦。
所以⽐较理想的是脚本⾃⼰来检测死锁,然后释放它。不过问题的难点在于如何找到⼀种“原⼦”操作,将检测死锁和删除死锁的动作⼀步完成,否则⼜会出现与加锁时同样的竞态条件的问题。例如:
进程1检测到死锁;
进程2监测到死锁;
进程1删除死锁;
进程x(也可能是进程1⾃⼰)加锁,开始运⾏;
进程2(错误地)删除死锁;
此时锁没有占⽤,于是任意进程都可以加锁并投⼊运⾏。
这样⼜出现了两个进程同时运⾏的情况。:(
可惜的是:在迄今为⽌的讨论之后,woodie还没有找到⼀种合适的“原⼦”操作。:(只是找到了⼀种稍微好些的办法:就是在删除时⽤⽂件的inode作标识,于是其它进程新建的锁(⽂件名虽然相同,但inode相同的机率⽐较微⼩)不容易被意外删除。这个⽅法已经接近完美了,可惜还是存在误删的微⼩⼏率,不能说是100%安全。唉,⼭重⽔复疑⽆路啊!:(
最近⼜有⽹友问起这个问题,促使我⼜再次思考。从我以前的⼀个想法发展了⼀下,换了⼀种思路,便有豁然开朗的感觉。不敢藏私,写出来请⼤家debug。^_^
基本的想法就是:借鉴多进程编程中临界区的概念,如果各个进程进⼊我们设⽴的临界区,只可能⼀个⼀个地顺序进⼊,不就能保证每次只有⼀个脚本运⾏了吗?怎样建⽴这样⼀种临界区呢?我想到了⼀种⽅法,就是⽤管道,多个进程写到同⼀个管道,只可能⼀⾏⼀⾏地进⼊,相应的,另⼀端也是⼀⾏⼀⾏地读出,如此就可以实现并⾏执⾏的多个进程进⼊临界区时的“串⾏化”。这与faintblue兄以前贴出的append⽂件的⽅法也是异曲同⼯。
我们可以让并⾏的进程同时向⼀个管道写⼀⾏请求,内容是其进程号,在管道另⼀端顺序读取这些请求,但只有第⼀个请求会得到⼀个“令牌”,被允许开始运⾏;后续的请求将被忽略,对应的进程没有得到令牌,就⾃⼰退出。这样就保证了任意时间只有⼀个进程运⾏(严格地说是进⼊临界区)。说到“令牌”,熟悉⽹络发展史的朋友可能会联想到IBM的Token Ring架构,每⼀时刻只能有⼀个主机得到令牌并发送数据,没有以太⽹的“碰撞”问题。可惜如同微通道技术⼀样,IBM的技术是不错,但最终还是被淘汰了。不错,这⾥令牌的概念就是借⽤于Token Ring。^_^
当⼀个进程执⾏完毕,向管道发送⼀个终⽌信号,即交回“令牌”,另⼀端接受到后,⼜开始选取下⼀个进程发放“令牌”。
您可能会问了,那么死锁问题⼜如何解决呢?别急,我在以前的讨论中曾提出将检测处理死锁的代码单独拿出来,交给⼀个专门的进程来处理的想法,这⾥就具体实践这样⼀种思路。当检测和删除死锁
的任务由⼀个专门的进程来执⾏时,就没有多个并发进程对同⼀个锁进⾏操作,所以竞态条件发⽣的物质基础也就根本不存在了。^_^
再发展⼀下这个思路,允许同时执⾏多个进程如何?当然可以!只要设⽴⼀个计数器,达到限制的数字就停⽌发放“令牌”即可。
网上课堂
下⾯就是woodie上述思路的⼀个实现,只是在centos 4.2下简单地测试了⼀下,可能还有不少错误,请⼤家帮忙“除⾍”。^_^思路上有什么问题也请不吝指教:
脚本1,token.sh,负责令牌管理和死锁检测处理。与下⼀个脚本⼀样,为了保持脚本的最⼤的兼容性,尽量使⽤Bourne shell 的语法,并⽤printf代替了echo,d的⽤法也尽量保持通⽤性。这⾥是由⼀个命名管道接受请求,令牌在⼀个⽂件中发出。如果⽤ksh也许可以⽤协进程来实现,熟悉ksh的朋友可以试⼀试。^_^
#!/bin/sh
#name: token.sh
#function: rialized token distribution, at anytime, only a cerntern number of token given out
#usage: token.sh [number] &
#number is t to allow number of scripts to run at same time
#if no number is given, default value is 1
if [ -p /tmp/p-aquire ]; then
rm -f /tmp/p-aquire
fiwps插入pdf
if mkfifo /tmp/p-aquire; then
printf "pipe file /tmp/p-aquire created\n" >>token.log
el
printf "cannot create pipe file /tmp/p-aquire\n" >>token.log
exit 1
fi
loop_times_before_check=100
if [ -n "$1" ];then
limit=$1
el
# default concurrence is 1
limit=1
fi
number_of_running=0
counter=0心理管理
while :;do
#check stale token, which owner is died unexpected
if [ "$counter" -eq "$loop_times_before_check" ]; then
counter=0
for pid in `cat token_file`;do
pgrep $pid
if [ $? -ne 0 ]; then
#remove lock
printf "s/ $pid//\nwq\n"|ed -s token_file
number_of_running=`expr $number_of_running - 1`
fi
done
fi
counter=`expr $counter + 1`
#
if [ "$number_of_running" -ge "$limit" ];then
# token is all given out. bypass all request until a instance to give one back
pid=`d -n '/stop/ {s/\([0-9]\+\) \+stop/\1/p;q}' /tmp/p-aquire`
if [ -n "$pid" ]; then
# get a token returned
printf "s/ $pid//\nwq\n"|ed -s token_file
number_of_running=`expr $number_of_running - 1`
continue
fi
el
# there is still some token to give out. rve another request
read pid action < /tmp/p-aquire
if [ "$action" = stop ]; then
# one token is given back.
printf "s/ $pid//\nwq\n"|ed -s token_file
number_of_running=`expr $number_of_running - 1`
el
# it's a request, give off a token to instance identified by $pid
printf " $pid" >> token_file
number_of_running=`expr $number_of_running + 1`
fi
fi
done
--------------------------------------------------------------------------------------------
修订记录:
1.修正token.sh的⼀个BUG,将原来⽤d删除失效令牌的命令⽤ed命令代替。感谢r2007和waker两位指出错误!--------------------------------------------------------------------------------------------
脚本2:并发执⾏的脚本 -- my-script。在"your code goes here"⼀⾏后插⼊你⾃⼰的代码,现有的是我⽤来测试的。#!/bin/sh
# cond to wait that the ditributer gives off a token
a_while=1
if [ ! -p /tmp/p-aquire ]; then
printf "cannot find file /tmp/p-aquire\n" >&2
exit 1
fi
# try to aquire a token
printf "$$\n" >> /tmp/p-aquire
sleep $a_while
# e if we get one
grep "$$" token_file
if [ $? -ne 0 ]; then
# bad luck. :(
printf "no token free now, \n" >&2
秋毫无犯的意思exit 2
fi
这个脚本是将⽂件锁得,不过我对这脚本还有⼀些疑惑的地⽅,暂且不讨论,等以后回头再来谈
#!/bin/sh
# filelock - A flexible file locking mechanism.
retries="10" # default number of retries
action="lock" # default action
nullcmd="/bin/true" # null command for lockfile
while getopts "lur:" opt; do
ca $opt in
l ) action="lock" ;;
u ) action="unlock" ;;
r ) retries="$OPTARG" ;;
esac
done
shift $(($OPTIND - 1))
if [ $# -eq 0 ] ; then
cat << EOF >&2
Usage: $0 [-l|-u] [-r retries] lockfilename
Where -l requests a lock (the default), -u requests an unlock, -r X specifies a maximum number of retries before it fails (default = $retries). EOF
exit 1
fi
# Ascertain whether we have lockf or lockfile system apps
if [ -z "$(which lockfile | grep -v '^no ')" ] ; then
echo "$0 failed: 'lockfile' utility not found in PATH." >&2
exit 1
fi
if [ "$action" = "lock" ] ; then
if ! lockfile -1 -r $retries "$1" 2> /dev/null; then
echo "$0: Failed: Couldn't create lockfile in time" >&2
exit 1
fi
宿迁社保
el # action = unlock
if [ ! -f "$1" ] ; then
echo "$0: Warning: lockfile $1 doesn't exist to unlock" >&2
exit 1
fi
rm -f "$1"
fi
exit 0