脚本专栏 
首页 > 脚本专栏 > 浏览文章

linux shell之文件锁

(编辑:jimmy 日期: 2024/11/15 浏览:3 次 )

经常在 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 ...cmd... 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] 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。
举个实用的例子:

#!/bin/bash
{
flock -n 3
[ $"htmlcode">
#!/bin/sh 
#name: token.sh 
#function: serialized token distribution, at anytime, only a cerntern number of token given out 
#usage: token.sh [number] & 
#number is set 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 
fi 
if mkfifo /tmp/p-aquire; then 
 printf "pipe file /tmp/p-aquire created\n" token.log 
else 
 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 
else 
 # 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 [ $"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=`sed -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 
 else 
  # there is still some token to give out. serve 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` 
    else 
     # 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,将原来用sed删除失效令牌的命令用ed命令代替。感谢r2007和waker两位指出错误!
--------------------------------------------------------------------------------------------

脚本2:并发执行的脚本 -- my-script。在"your code goes here"一行后插入你自己的代码,现有的是我用来测试的。

#!/bin/sh 
# second 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 
# see if we get one 
grep "$$" token_file 
if [ $"no token free now, exitting...\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
 case $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
else  # 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

上一篇:数值运算shell脚本
下一篇:控制输出颜色的shell脚本