開發背景介紹:
有一臺 DBSERVER ,跑的是 MySQL5.5 。準備通過 crontab 執行計劃任務定時備份數據庫。安裝 crontab 時竟然報告與 MySQL 衝突,在網上找了一下,倒是有位仁兄有遇到過,並提供瞭解決方案。但是方法比較折騰,SERVER又是運行在線上環境, 不敢 亂動。於是就用 shell 腳本實現了一個簡單的計劃任務功能。
設計思路:
設想是任務封裝到函數中,並加上必要的初始化聲明,包括起始時間、運行週期等。每個任務單獨一個 sh 文件,存放在統一的目錄中。由主程序讀取並按計劃執行各任務。腳本以終端無關的形式在後臺執行 , 啓動命令 :nohup mytask.sh & 。結束運行的命令 :kill -15 `cat mytask.pid` 。腳本在 centos6 及 ubuntu12 測試通過。
實現功能:
1、多任務併發執行,不會互相影響,採用鎖機制避免單個任務的重疊執行。
2、每個任務以單獨腳本形式保存,相互獨立。
3、支持起始運行時間,如"2013/05/08"、"13:30"或“now”。並且支持給起始運行時間的修正值,比如"now+5m"表示當前時間的5分鐘後執行(另外還實現了負數修正值,比如-1h,現在覺得這個功能挺無聊的)。
4、支持多種類型的運行週期設定,包括秒、分、時、天、周、月、年還有一次性任務。
5、會根據任務執行間隔,自動設定休眠時間,主程序佔用資源極小。
程序主要結構及說明:
一、任務腳本編寫規範
每個任務腳本都必須包含初始化語句和任務函數這兩部分,函數名要保證唯一性。
初始化語句格式如下:
RunArg="<調用函數名>#<起始運行時間>#<運行週期>"
以#符分隔參數依次定義爲:調用函數名、起始運行時間、運行週期。
1、調用函數名,任務函數必須要在腳本中明確定義。
2、起始運行時間分兩部分。
第一部分爲初始時間,格式爲"yyyy/MM/dd hh:mm:ss"也可以是時間值片斷,例如:"2013/03/05"、"03/05"、“03/05 21:30”、"21:30"或"now"代表當前時間。
第二部分爲修正時間,格式爲"+時間單位"或“-時間單位”,意思爲在初始時間的基礎上做進一步的時間修正。例如:"+5s"、"-10m"等。時間的單位區別大小寫,具體定義如下:
y=年、M=月、d=日、h=時、m=分、s=秒、w=星期
3、運行週期即爲任務函數運行的間隔時間,取值與修正時間類似,只是取消了+-號,如果值爲不帶單位的0則表示只運行一次。
例如:
#在凌晨零點開始執行_backdb函數,每隔1天運行一次。
RunArg='_backdb#00:00#1d'
#在當前時間的2分鐘後開始執行_test1func函數,每隔5分鐘運行一次。
RunArg='_test1func#now+2m#5m'
#在5月12日14點30開始執行_test2func函數,只運行一次。
RunArg='RunArg='_test5func#5/12 14:30#0'
最後給一個完整的任務腳本:
#!/bin/bash
#啓動即開始執行_test4func函數,每隔1個月運行一次。
RunArg='_test4func#now#1M'
#定義任務函數_test4func
function _test4func()
{
#任務內容,此處以休眠5秒模擬任務運行時間。
sleep 5;
}
二、主程序說明
1、初始化
FUNCDIR=`dirname $0`"/tasks" #任務腳本存放目錄
LOGFILE=`dirname $0`"/mytask.log" #運行記錄文件名
PIDFILE=`dirname $0`"/mytask.pid" #pid存放文件名
LOCKFILE=`dirname $0`"/mytask.lock" #鎖文件名
... ... ... ... ... ...
#通過檢測鎖文件存在,判斷程序是否已經運行,防止重入
if [ -f $LOCKFILE ]; then
exit 0
else
touch $LOCKFILE
echo "mytask start at "`date` >$LOGFILE
fi
#捕獲系統信號,處理程序鎖。
trap "rm -f $LOCKFILE;rm -f $FUNCDIR/*_lock;echo 'exit';kill -15 $$" SIGINT EXIT
... ... ... ... ... ...
2、任務預處理
#循環執行指定目錄下的所有sh文件
for i in `ls $FUNCDIR/*.sh`
do
... ... ... ... ... ...
#確保每個任務腳本都包含了有效的初始化語句
RunArg=
. $i
if [ "${RunArg:-'none'}" = "none" ]; then
continue
fi
... ... ... ... ... ...
#處理任務初始執行時間,將初始執行時間全部統一爲標準總秒數(+%s)
startTime=${startRun%[+|-]*}
startSec=`date -d "$startTime" +%s`
fixTime=${startRun:${#startTime}:$[ ${#startRun} - ${#startTime} ]}
case ${fixTime:$[ ${#fixTime} - 1]} in
s|[0-9])
startSec=$[ $startSec + ${fixTime%s} ]
;;
m)
startSec=$[ $startSec + ${fixTime%m} * 60 ]
;;
h)
startSec=$[ $startSec + ${fixTime%h} * $ONEHOUR ]
;;
d)
startSec=$[ $startSec + ${fixTime%d} * $ONEDAY ]
;;
w)
startSec=$[ $startSec + ${fixTime%w} * $ONEWEEK ]
;;
M)
ty=`date -d $startTime +%y`
tm=$[ `date -d $startTime +%m` + ${fixTime%M} ]
td=$[ `date -d $startTime +%d` - 1 ]
tt=`date -d $startTime +%T`
if (( $tm > 12 )); then
tm=$[ $tm % 12 ]
ty=$[ $ty + $tm / 12 ]
fi
startSec=$[ `date -d "$ty-$tm-1 $tt" +%s` + $td * $ONEDAY ]
;;
y)
ty=$[ `date -d $startTime +%y` + ${fixTime%y} ]
td=$[ `date -d $startTime +%j` - 1 ]
tt=`date -d $startTime +%T`
startSec=$[ `date -d "$ty-1-1 $tt" +%s` + $td * $ONEDAY ]
;;
esac
#計算任務執行間隔時間,將除單位爲年和月以外的簡隔時間統一爲秒。由於以年和月爲單位的間隔時間要根據實際運行時間而定,所以不能預先計算。
tp=s
case ${atime:$[ ${#atime} - 1]} in
s)
addTime=${atime%s}
;;
m)
addTime=$[ ${atime%m} * 60 ]
;;
h)
addTime=$[ ${atime%h} * $ONEHOUR ]
;;
d)
addTime=$[ ${atime%d} * $ONEDAY ]
;;
w)
addTime=$[ ${atime%w} * $ONEWEEK ]
;;
M)
addTime=${atime%M}
tp=M
;;
y)
addTime=${atime%y}
tp=y
;;
... ... ... ... ... ...
esac
#將初始化後的任務參數存入數組,供後續程序調用
#任務參數以#分隔,分別爲任務函數名、開始時間(標準總秒數)、運行間隔時間、間隔時間單位。
#間隔時間單位爲s、M、y,即秒、月、年。
aRunList=(${aRunList[@]} "$fn#$startSec#$addTime#$tp")
fi
done
3、任務執行
#循環讀取任務數組,並根據任務參數適時啓動計劃任務
... ... ... ... ... ...
IntervalTime=$INIT; #主程序休眠時長
... ... ... ... ... ...
for i in ${aRunList[@]}
do
... ... ... ... ... …
#以動態變量的形式存放各任務的下一次運行時間
ntarg="${fn}_ntime"
flagfile="${FUNCDIR}/${fn}_lock"
eval ${ntarg}=\${${ntarg}:=$startSec}
eval tntarg=\$${ntarg}
tdiff=$[ $nowSec - $tntarg ]
if (( $tdiff >= 0 )); then
#當前時間超過任務計劃運行時間小於運行閥值時啓動任務
#爲避免因某個任務執行時間過長超出此任務間隔時間而導致重入,
#每個任務在執行時都會創建鎖文件,並在任務執行完後刪除。
#爲了保證多任務的併發性,每個任務都會以後臺運行方式執行。
if ! [ -e $flagfile ] && (( $tdiff < $MISSTIMES )) ; then
{
touch $flagfile;
echo "$fn start at "`date`\(`date +%s`\) >>$LOGFILE;
result=`$fn`;
echo "$fn finished at "`date`\(`date +%s`\) >>$LOGFILE;
rm -f $flagfile;
} &
else
echo "$fn has skipped" >>$LOGFILE
fi
#根據間隔時間單位計算下一次任務執行的時間
case $tp in
#秒
s)
addSec=$addTime
;;
#月
M)
ty=`date +%y`
tm=$[ `date +%m` + $addTime ]
td=$[ `date +%d` - 1 ]
if (( $tm > 12 )); then
tm=$[ $tm % 12 ]
ty=$[ $ty + $tm / 12 ]
fi
addSec=$[ `date -d "$ty-$tm-1 $nowTime" +%s` + $td * $ONEDAY ]
;;
#年
y)
ty=$[ `date +%y` +$addTime ]
td=$[ `date +%d` - 1 ]
addSec=$[ `date -d "$ty-1-1 $nowTime" +%s` + $td * $ONEDAY ]
;;
#將只執行一次的任務從任務數組中清除
*)
aRunList=(`echo ${aRunList[@]} |sed "s/$fn\(#[^#]*\)\{2\}#[^ ]*//g"`)
IntervalTime=0;
continue
;;
esac
tntarg=$[ $tntarg + ( $tdiff / $addSec ) * $addSec + $addSec ]
eval ${ntarg}=$tntarg
tdiff=$[ $nowSec - $tntarg ]
fi
if (( $tdiff > $IntervalTime )) ; then
IntervalTime=$tdiff;
fi
done
... ... ... ... ... …
遺留問題:每個任務腳本中聲明的函數名必須唯一不能重複,否則會導致任務函數覆蓋,目前沒有很好的解決。
因爲是第一次編寫稍複雜的腳本 , 代碼結構和水平還有待提高 , 希望能起到拋磚引玉的作用 .
題外話:ubuntu下的LibreOffice實在是用不好,排版有些亂,有興趣的朋友可以直接下載源碼看。
本文轉載自博客園,原文鏈接:http://www.cnblogs.com/lykyl/archive/2013/05/06/3063905.html