bash には、いくつかのビルトインコマンド (test や printf 等) があり、それらを用いた場合は、外部コマンドの起動 (fork & exec) を節約できます。一般に、fork & exec は重いので、できればもうちょっとビルトインを増やしてくれるといいのにと思うことがしばしばあります。もちろん肥大化しないため、最小限となっているものとは思うのですが・・・
この記事では、外部コマンドの sleep を呼び出すことなく、bash スクリプトを秒単位でスリープさせる方法を示します。本日、ふと思いつきました。
bash のビルトインの read には、入力待ちをタイムアウトさせるための -t オプションが備わっています。これを使えば sleep の代用にできるはずです。ただし、絶対に入力が発生しない何かを read して、かつ、ブロックさせなくてはいけません。もし、C であれば、pipe() システムコールを呼んで、作成されたパイプを read すれば良いと思いますが、bash の構文で pipe() 相当をやる方法はありません。そこで、mkfifo で名前つきパイプ (FIFO) を作成することにしました。
次が実験に使ったスクリプトです。
#!/bin/bash
#
# Name: test_my_builtin_sleep.bash
#
setup_my_sleep() {
local sleep_fifo=/tmp/.sleep_fifo.$$.$RANDOM$RANDOM
mkfifo $sleep_fifo
exec 9<>$sleep_fifo
rm -f $sleep_fifo
}
setup_my_sleep
my_sleep() {
read -t $1 0<&9
}
time {
for ((i=0;i<20;i++))
do
sleep 1
read < /proc/uptime
echo $REPLY
done
}
echo
time {
for ((i=0;i<20;i++))
do
my_sleep 1
read < /proc/uptime
echo $REPLY
done
}
exec 9<>$sleep_fifo という書き方で、fd 9 を使って FIFO を rw モードでオープンしています。また、直後に FIFO のパスを rm していますが、オープン中なのでスクリプトが終わるまでは、FIFO を利用できます。
次のような実行結果になりました。
ThinkPad X301 Core2 1.40GHz 上の CentOS 5 x86_64 で実行しています。
# dmidecode | grep Version
Version: 6EET54WW (3.14 )
Version: ThinkPad X301
Version: Not Available
Version: Not Available
Version: Intel(R) Core(TM)2 Duo CPU U9400 @ 1.40GHz
SBDS Version: 03.01
# cat /etc/redhat-release
CentOS release 5.7 (Final)
# uname -a
Linux my41 2.6.18-274.12.1.el5xen #1 SMP Tue Nov 29 14:18:21 EST 2011 x86_64 x86_64 x86_64 GNU/Linux
# ./test_my_builtin_sleep.bash
4066.52 3937.55
4067.53 3938.46
4068.54 3939.47
4069.55 3940.45
4070.55 3941.45
4071.56 3942.46
4072.57 3943.47
4073.58 3944.47
4074.59 3945.47
4075.59 3946.48
4076.60 3947.48
4077.61 3948.49
4078.62 3949.49
4079.63 3950.50
4080.63 3951.50
4081.64 3952.49
4082.65 3953.50
4083.66 3954.50
4084.67 3955.51
4085.68 3956.51
real 0m20.161s
user 0m0.012s
sys 0m0.040s
4086.68 3957.52
4087.68 3958.52
4088.69 3959.53
4089.69 3960.53
4090.69 3961.53
4091.70 3962.53
4092.70 3963.54
4093.71 3964.54
4094.71 3965.55
4095.72 3966.55
4096.72 3967.55
4097.72 3968.56
4098.73 3969.56
4099.73 3970.57
4100.74 3971.57
4101.74 3972.58
4102.74 3973.58
4103.75 3974.58
4104.75 3975.59
4105.76 3976.59
real 0m20.079s
user 0m0.000s
sys 0m0.000s
前半は外部コマンドの sleep を使っており、後半は read -t & FIFO 方式です。なお、時間の経過が読み取り易いように、/proc/uptime を表示しています。
外部コマンドを用いた場合は、user+sys で 52 ミリ秒のオーバーヘッドが見えているのに対して、read -t & FIFO 方式では、ゼロ (オーバーヘッドが1ミリ秒未満) に見えています。期待通りです。
システムの負荷が高いと、fork & exec が遅延する場合があるので、実験で用いたスクリプトのように小刻みに sleep する処理は、影響を受け易いです。そんな場面に遭遇したら、read -t & FIFO 方式を試してみては。
それだったら、無理に bash で書かなくてもいいんじゃないのという声もありそうですが。
そりゃその通りで、適材適所で perl に Ruby に Python に・・・豊かな時代ですね。
2014-05-11追記
RHEL7.0 RC を試していたのですが、最近は read -t で少数(0.5秒とか)を指定できるようです。sleep も、いつの間にか。
[root@hoge ~]# uname -a
Linux hoge 3.10.0-121.el7.x86_64 #1 SMP Tue Apr 8 10:48:19 EDT 2014 x86_64 x86_64 x86_64 GNU/Linux
[root@hoge ~]# rpm -q bash
bash-4.2.45-5.el7.x86_64
[root@hoge ~]# time sleep 0.5
real 0m0.501s
user 0m0.001s
sys 0m0.000s
[root@hoge ~]# time read -t 0.5
real 0m0.500s
user 0m0.000s
sys 0m0.000s
というわけで、read -t + FIFO でミリ秒単位の sleep も出来そうです。ご入用の方は、どうぞ。
なお、mkfifo を使わないことが可能なのかどうかは、未だに分からずです。bash のビルトインを自作すれば可能でしょうけども。