開發背景介紹:
有一台DBSERVER,跑的是MySQL5.5。准備通過crontab執行計划任務定時備份數據庫。安裝crontab時竟然報告與MySQL沖突,在網上找了一下,倒是有位仁兄有遇到過,並提供了解決方案(http://blog.csdn.net/faye0412/article/details/7895366)。但是方法比較折騰,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實在是用不好,排版有些亂,有興趣的朋友可以直接下載源碼看。