免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12345下一页
最近访问板块 发新帖
查看: 15373 | 回复: 41
打印 上一主题 下一主题

[shell应用进阶]:限制同时运行脚本实例的个数 -- 串行化:换一个思路。  关闭 [复制链接]

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-10-11 20:00 |只看该作者 |倒序浏览
【背景介绍】
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,sed的用法也尽量保持通用性。这里是由一个命名管道接受请求,令牌在一个文件中发出。如果用ksh也许可以用协进程来实现,熟悉ksh的朋友可以试一试。^_^
  1. #!/bin/sh
  2. #name: token.sh
  3. #function: serialized token distribution, at anytime, only a cerntern number of token given out
  4. #usage: token.sh [number] &
  5. #number is set to allow number of scripts to run at same time
  6. #if no number is given, default value is 1
  7. if [ -p /tmp/p-aquire ]; then
  8.   rm -f /tmp/p-aquire
  9. fi
  10. if mkfifo /tmp/p-aquire; then
  11.   printf "pipe file /tmp/p-aquire created\n" >>token.log
  12. else
  13.   printf "cannot create pipe file /tmp/p-aquire\n" >>token.log
  14.   exit 1
  15. fi

  16. loop_times_before_check=100
  17. if [ -n "$1" ];then
  18.   limit=$1
  19. else
  20.   # default concurrence is 1
  21.   limit=1
  22. fi
  23. number_of_running=0
  24. counter=0
  25. while :;do
  26.   #check stale token, which owner is died unexpected
  27.   if [ "$counter" -eq "$loop_times_before_check" ]; then
  28.     counter=0
  29.     for pid in `cat token_file`;do
  30.       pgrep $pid
  31.       if [ $? -ne 0 ]; then
  32.         #remove lock
  33.             printf "s/ $pid//\nwq\n"|ed -s token_file
  34.             number_of_running=`expr $number_of_running - 1`
  35.       fi
  36.     done
  37.   fi
  38.   counter=`expr $counter + 1`

  39.   #
  40.   if [ "$number_of_running" -ge "$limit" ];then
  41.     # token is all given out. bypass all request until a instance to give one back
  42.     pid=`sed -n '/stop/ {s/\([0-9]\+\) \+stop/\1/p;q}' /tmp/p-aquire`
  43.     if [ -n "$pid" ]; then
  44.       # get a token returned
  45.       printf "s/ $pid//\nwq\n"|ed -s token_file
  46.       number_of_running=`expr $number_of_running - 1`
  47.       continue
  48.     fi
  49.   else
  50.     # there is still some token to give out. serve another request
  51.     read pid action < /tmp/p-aquire
  52.         if [ "$action" = stop ]; then
  53.           #  one token is given back.
  54.           printf "s/ $pid//\nwq\n"|ed -s token_file
  55.           number_of_running=`expr $number_of_running - 1`
  56.         else
  57.           # it's a request, give off a token to instance identified by $pid
  58.           printf " $pid" >> token_file
  59.           number_of_running=`expr $number_of_running + 1`
  60.         fi
  61.   fi
  62. done
复制代码


--------------------------------------------------------------------------------------------
修订记录:
1.修正token.sh的一个BUG,将原来用sed删除失效令牌的命令用ed命令代替。感谢r2007和waker两位指出错误!
--------------------------------------------------------------------------------------------

脚本2:并发执行的脚本 -- my-script。在"your code goes here"一行后插入你自己的代码,现有的是我用来测试的。
  1. #!/bin/sh
  2. # second to wait that the ditributer gives off a token
  3. a_while=1
  4. if [ ! -p /tmp/p-aquire ]; then
  5.   printf "cannot find file /tmp/p-aquire\n" >&2
  6.   exit 1
  7. fi
  8. # try to aquire a token
  9. printf "$$\n" >> /tmp/p-aquire
  10. sleep $a_while
  11. # see if we get one
  12. grep "$$" token_file
  13. if [ $? -ne 0 ]; then
  14.   # bad luck. :(
  15.   printf "no token free now, exitting...\n" >&2
  16.   exit 2
  17. fi
  18. # yeah, got token!
  19. # be sure to return the token before we exit
  20. trap 'printf "$$ stop\n" > /tmp/p-aquire' 0
  21. trap "exit 3" 1 2 3 15

  22. #get to run, your code goes here
  23. printf "$$: running...\n" >&2
  24. sleep 5
  25. printf "$$: exitting...\n" >&2
  26. #end of your code
复制代码

[ 本帖最后由 woodie 于 2006-10-29 21:36 编辑 ]

论坛徽章:
7
荣誉版主
日期:2011-11-23 16:44:17子鼠
日期:2014-07-24 15:38:07狮子座
日期:2014-07-24 11:00:54巨蟹座
日期:2014-07-21 19:03:10双子座
日期:2014-05-22 12:00:09卯兔
日期:2014-05-08 19:43:17卯兔
日期:2014-08-22 13:39:09
2 [报告]
发表于 2006-10-11 23:32 |只看该作者
直接写回原文件会出问题吧?还没有完全读明白整个意思,难道是有意这么安排的?
  1. sed "s/\ $pid//" token_file > token_file
复制代码

先占个位置明天再好好研究一下。^_^

新发现:如何防止多个token.sh同时运行?作为Daemon进程在开机时自动加载?

[ 本帖最后由 r2007 于 2006-10-11 23:41 编辑 ]

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
3 [报告]
发表于 2006-10-12 08:36 |只看该作者
原帖由 r2007 于 2006-10-11 23:32 发表
直接写回原文件会出问题吧?还没有完全读明白整个意思,难道是有意这么安排的?
  1. sed "s/\ $pid//" token_file > token_file
复制代码

先占个位置明天再好好研究一下。^_^

新发现:如何防止 ...

是有意安排的。^_^
因为token_file只有一行,内容是空格分隔的允许运行的进程号。
该行sed先读入只有一行的文件内容,删除一个进程号,回写到原文件。应该没有问题吧?^_^

token.sh是管理模块,设计时就是用来只执行一次的,通常是用&丢到后台去。所以token.sh应该不存在并发的问题。^_^如要防止意外运行两次,可以在该脚本的开始检测一个标志,有则退出就行了,不必考虑竞态的问题。谢谢七兄的意见!

又读了一下上面的代码,发现检测失效令牌的代码段中使用的pgrep命令兼容性不好,非linux平台可能没有这个命令。应该改成用ps+grep来做,但ps命令的语法在不同的平台上也不尽相同。怎样写兼容性最好呢?七兄有什么好的建议?有其它UNIX环境的朋友们也请发表意见,如何在您的平台下检测一个进程号对应的进程是否存在?SOLARIS, AIX, HP-UX, IRIS, *BSD, $CO, ...所有平台的意见都欢迎!

[ 本帖最后由 woodie 于 2006-10-12 08:39 编辑 ]

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
4 [报告]
发表于 2006-10-12 08:45 |只看该作者
其实和r007的问题是一样的,如何确保token.sh在my-script之前唯一的运行?

另外

pid=`sed -n '/stop/ {s/\([0-9]\+\) \+stop/\1/p;q}' /tmp/p-aquire`

这一句会从管道读出所有等待写入的内容,会不会忽略脚本的请求?

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
5 [报告]
发表于 2006-10-12 08:48 |只看该作者
原帖由 woodie 于 2006-10-12 08:36 发表

是有意安排的。^_^
因为token_file只有一行,内容是空格分隔的允许运行的进程号。
该行sed先读入只有一行的文件内容,删除一个进程号,回写到原文件。应该没有问题吧?^_^

token.sh是管理模块,设计时就是 ...



太大意了了吧

sed .. file >file会清空file

>file在命令运行前就清空了file ,sed 从哪儿读?

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
6 [报告]
发表于 2006-10-12 09:04 |只看该作者
原帖由 waker 于 2006-10-12 08:45 发表
其实和r007的问题是一样的,如何确保token.sh在my-script之前唯一的运行?

另外

pid=`sed -n '/stop/ {s/\([0-9]\+\) \+stop/\1/p;q}' /tmp/p-aquire`

这一句会从管道读出所有等待写入的内容,会不会忽略脚本的请求?

token.sh唯一性的问题已经说过了。
waker兄提到的这行sed,实际上也是有意这样做的。注意这行的前提条件是,令牌限额已经用完,此时应该忽略所有的请求,直到有令牌交回为止(即读到含有stop字样的行),然后才能继续发放令牌。这样做是为了提高读取的效率,避免一次循环读一行时,造成高并发情况下队列越排越长的拥塞问题。

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
7 [报告]
发表于 2006-10-12 09:13 |只看该作者
原帖由 waker 于 2006-10-12 08:48 发表



太大意了了吧

sed .. file >file会清空file

>file在命令运行前就清空了file ,sed 从哪儿读?

呵呵,你说的不错!应该改成:
cat token_file | sed "s/\ $pid//" > token_file
或用ed来做。
谢谢二位指正!

------------------------
附注:上面这个cat也是一个UUOC,不过是一个:Useful Usage Of Cat. ^_^一笑!

[ 本帖最后由 woodie 于 2006-10-12 09:27 编辑 ]

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
8 [报告]
发表于 2006-10-12 09:26 |只看该作者
问题是即使现在有脚本已经交回了令牌,请求也会被忽略吧?
一个脚本已经交了牌,但你的处理在收回令牌以前已经忽略了其它的请求
可能我的讲解有误

token.sh唯一性的问题已经说过了。
写这个东西的目的不就是避免竞争条件么?如果放宽一点,我们只需要touch一个文件就行了。
现在把my-script的竞争问题避免了,但途径是转移到token.sh中去,好像不完美

论坛徽章:
8
摩羯座
日期:2014-11-26 18:59:452015亚冠之浦和红钻
日期:2015-06-23 19:10:532015亚冠之西悉尼流浪者
日期:2015-08-21 08:40:5815-16赛季CBA联赛之山东
日期:2016-01-31 18:25:0515-16赛季CBA联赛之四川
日期:2016-02-16 16:08:30程序设计版块每日发帖之星
日期:2016-06-29 06:20:002017金鸡报晓
日期:2017-01-10 15:19:5615-16赛季CBA联赛之佛山
日期:2017-02-27 20:41:19
9 [报告]
发表于 2006-10-12 09:28 |只看该作者
cat token_file | sed "s/\ $pid//" > token_file
在bash中仍然有可能会清空file

用ed吧

论坛徽章:
1
荣誉会员
日期:2011-11-23 16:44:17
10 [报告]
发表于 2006-10-12 09:45 |只看该作者
原帖由 waker 于 2006-10-12 09:26 发表
问题是即使现在有脚本已经交回了令牌,请求也会被忽略吧?
一个脚本已经交了牌,但你的处理在收回令牌以前已经忽略了其它的请求
可能我的讲解有误

本来这个脚本要解决的问题就是高并发情况下可能发生的一些问题,高并发环境下,某些my-script的请求被拒绝是再正常不过的,这并不是世界末日。^_^
在它请求时没有令牌,所以被拒绝是很自然的处理方法。一次读一行的情况下,也会有请求被拒绝,不过几率低一些罢了,不是吗?不过那样的话,很可能发生上文所说的拥塞的情况。
------------------------
补充一下,在还有令牌要发的情况下,脚本走的是另一个分支,那时是用read逐行读取的,不会随意丢弃排队的请求。^_^

原帖由 waker 于 2006-10-12 09:26 发表token.sh唯一性的问题已经说过了。
写这个东西的目的不就是避免竞争条件么?如果放宽一点,我们只需要touch一个文件就行了。
现在把my-script的竞争问题避免了,但途径是转移到token.sh中去,好像不完美

再说一次,token.sh是一个监控的模块,只需要运行一次,不是用来并发执行的,所以考虑它自身的竞态问题是没有必要的。并发执行的任务都放到my-script中去做。不知道我表达得清楚了吗?^_^

[ 本帖最后由 woodie 于 2006-10-12 09:55 编辑 ]
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP