2011年12月17日土曜日

bash で外部コマンドを使わずに sleep する方法

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 のビルトインを自作すれば可能でしょうけども。

2 件のコメント:

  1. このオーバーヘッドが無くなるのはいいですね!!

    返信削除
  2. mkfifoはどうやってbashで実現すればいいですか

    返信削除

人気ブログランキングへ にほんブログ村 IT技術ブログへ