■ 問: シェルスクリプトで 0.01 秒間などのごく短い時間だけ sleep する方法は? それもできるだけ精確に。
この問いは一見して簡単な問いに思われるが、考えて見ると意外と難しい。
後で詳しく説明するが、POSIX (Unix の規格) では sleep
に小数は指定できない。また、外部コマンド sleep
の起動自体に余分な時間が掛かることも問題になる。
- 記事更新 (2020-04-11): 記事 "POSIX準拠シェルスクリプトだけで1秒未満sleepを実現する - Qiita" 言及
-
記事更新 (2018-08-18): Bash 5.0 以降の
EPOCHREALTIME
について追記 -
記事更新 (2018-08-18): 最近の Cygwin の
/dev/tcp/*/*
について追記・修正 - 記事更新 (2018-07-26): Bash 4.4 以降の Bash loadable builtins について追記
おまけ: sleep 0.01
を使ったシェルアニメーション (スクリプトはこの記事の最後)
先に答え? (現状の案 for bash-4.0+/zsh)
上記の問いがどう難しいのかだとかそういう説明を全部飛ばして先に結果を書いてしまう。色々考えたり測定してみたりした結果、今のところの最良案 (シェル関数版 sleep
) は以下である。組み込みコマンド read
のタイムアウト (小数の指定は bash-4.0 以上) を使っている。
# function sleep for bash-4.0+/zsh
##
## 関数 sleep time
## スリープする。
##
## @param[in] time 待ち時間。単位は秒。小数も可能。
##
## 例: $ sleep 0.1
##
## 注意: ファイルディスクリプタ 9 を別の用途に使うと駄目
##
function sleep { local REPLY=; ! read -u 9 -t "$1"; }
if [[ $OSTYPE == cygwin* ]]; then
# Cygwin work around
exec 9< <(
[[ $- == *i* ]] && trap -- '' INT QUIT
while :; do command sleep 2147483647; done
)
if [[ $BASH_VERSION ]]; then
function sleep {
local s="${1%%.*}"
if ((s>0)); then
! read -u 9 -t "$1" s
else
# 修正 (2018-08-18): 最近の Cygwin (もしくは最近の Windows?)
# では /dev/tcp/... を使うと失敗するので、/dev/udp/... に変更。
! read -t "$1" s < /dev/udp/0.0.0.0/80
fi
}
fi
else
rm -f ~/.sleep.tmp
mkfifo ~/.sleep.tmp
exec 9<> ~/.sleep.tmp
rm -f ~/.sleep.tmp
fi
シェル関数で sleep
を上書きする実装にしているので、sleep 小数
を使っている既存のスクリプトがあれば、source sleep.sh
するだけで POSIX 環境に対応できる。
もっと良い sleep 小数
の方法・綺麗な解があれば教えてほしい。
改善の余地?
- 予め
sleep
自体にかかる余分な時間を計測して補正を行う? -
/proc/uptime
が利用可能なシステムでは、それを見てずれを補正可能かも。 - bash-4.1+/zsh なら
{fd}<> ...
で未使用のファイルディスクリプタを自動で割り当てられる。
※本当はアニメーションにするには、単なる sleep
では余りよくない。フレームの描画に掛かる時間を差し引いて sleep
しなければならない。そのためには精確で高速な時刻計測が必要になるが、話がややこしくなるのでここでは考えない。
■ 前文
まあ、実際にこういう sleep 小数
の需要がそうそうあるのかどうかは分からないけれど…うーん…例えば シェルスクリプトで端末にアニメーションを表示したい とか? 正直なところ、まともに考えればそんなことでシェルスクリプトに拘る理由はない。もっと便利な別の言語を使えば良いのである。
しかし、稀にシェルスクリプトで書かなければならないときがある。例えば、bash
の拡張 (.bashrc
から読み込むようなの) をシェル関数で書くときである。特に、それを他の人に配布する場合には、できるだけ他のツールに依存せず bash
(+ POSIX 環境) だけ存在していれば動くようにしたい。実際に bash
用の拡張の ble.sh を書いていてちょっと困ったのである。
以降ではもう少し詳しくこの sleep
の問題について調べる。構成は以下の通り。1. で問題点を説明し、2. で複数の解決法を挙げる。3. で実測して比較し、4. で応用例としてアニメーションを表示してみる。ちなみに、この記事では bash
での実現が主目的であり zsh
は序でである。
■ 1. シェルスクリプトの sleep 問題
二種類の問題がある。
1.1 POSIX では sleep 小数
は使えない
ひとつ目の問題。POSIX では sleep
コマンドは整数 (単位は秒) を指定した時のことしか規定されていない (sleep - The Open Group)。つまり、sleep コマンドに小数を指定しても動かない UNIX 環境がありうるということである。少なくとも GNU/Linux だとか Cygwin では sleep (GNU coreutils)
が入っていて 0.01 などの小数を使うことができる。一方で AIX では小数は指定できないようだ (AIXで1秒未満のsleepを実施 - Qiita)。Mac OS X, FreeBSD, Solaris, HP-UX, etc. ではどうかしら。
ところで、それぞれの環境で sleep 小数
が使えるかどうか調べるために Qiita を徘徊していたら sleep 小数
を使っているスクリプトは結構たくさんある。やはり需要は大きいようだし、また、sleep 小数
は事実上の標準になっている気配がする。
Qiita内の調査結果
以下は
sleep 小数
を使っている記事。
- 2011/10/25 markdown をコマンドラインでプレビュー
- 2014/05/23 シェルスクリプトでコマンド多重コントロール
- 2014/05/26 シェルスクリプトで日付と時間を大小判定
- 2014/06/16 同じ行に出力する - Qiita
- 2014/09/24 Mplayerで再生中の曲をtmuxのステータスラインに表示する方法
- 2014/12/21 Zshで長い処理をしている間に読込中を表示する
- 2015/03/29 ターミナルで簡単にグラフを描くツール termeter
- 2015/05/09 bcコマンドの限界は68桁?(回避方法あり
- 2015/06/16 CI用にヘッドレスなAndroidエミュレータを複数台立ち上げるシェルスクリプト
- 2015/06/19 Self-extracting Shell Script
- 2015/10/22 ShellScriptで使えるメソッドまとめ
以下は環境を指定した記事なので sleep 小数
でも大丈夫。
- 2013/09/21 MacVim をコマンドラインから起動する
- 2014/06/03 Linuxサーバのパフォーマンス測定スクリプト
- 2015/08/04 Linux でミリ秒まで表示するワンライナー時計
- 2016/04/28 MSYS2で快適なターミナル生活
以下は分かりやすくするためにわざわざ sleep 1s
だとか sleep 5m
だとかとしているが、この指定方法は勿論 POSIX にはない。
- 2016/01/03 シェルスクリプトでだいたい1時間の間隔であれをやる
- 2016/03/15 bashコマンドの出力を延々とログとるスクリプト
下の記事では "おまけ" で無限スリープ sleep inf
についての考察がある。
追記: また usleep
や sleepenh
などの "秒以下を扱える強化版コマンド" を使っている記事もある。もちろんこれらのコマンドがあるかどうかは sleep 小数
にもまして環境に依存する。2番目の記事のように sleep
を fallback とするのが賢いやり方である。
- 2013/03/29 ディレクトリ配下のファイルを再帰的に並列でrsyncする
- 2015/03/18 ターミナルが256色使えるかどうか確認するbashスクリプトを作った
- 2016/01/14 シェル芸力向上のため moreutils を一通り試してみた
1.2 sleep
自体の起動に時間がかかる
もうひとつの問題は sleep
コマンドの起動にかかる時間である。シェルから外部コマンドの sleep
を立ち上げるためには、新しくプロセスを生成することになるので多少時間がかかる。sleep
は自分が起動される原因となったイベントがいつ起こったかを知らないから、当然自分が起動完了した瞬間から測ってスリープを行う。つまり、sleep
呼び出し元から見た停止時間は 起動時間 + 指定した時間
になる。
ではこの起動時間はいかほどだろうか。プロセスの起動時間はシステムによる。環境によっては大体ミリ秒程度で済むが、十ミリ秒ぐらいかかることも普通にある。Cygwin などの環境にいたっては数十ミリ秒かかるのが普通である。(Cygwin なんか使うのが悪いという声が聞こえてきそうだが、Cygwin でもシェルくらいはまともに動作してほしい。) 最近 "Bash が Windows 10 上で動く" という話で賑わしている Windows Subsystem for Linux ではもっと遅いという噂 (Windows Subsystem for Linux の fork は速いのか - Qiita) も。つまり、整数単位の sleep
では誤差にしかならなかった起動時間が、ミリ秒単位でのスリープでは深刻なずれを引き起こす。
また、プロセスの起動に時間が掛かるということは、それなりのコストがあるということである。例えば、アニメーションなどに使うとなると一秒間に何十回も sleep
を実行することになるがこれは結構な負荷である。たかが sleep ごときでホストに負荷をかけたくない。したがって bash
/zsh
の組み込みコマンドの範囲内でどうにかしたい。
■ 2. 色々の方法
ここでは sleep
を実現するための様々な方法を挙げている。特に 2.3.2/2.3.4 の方法を用いれば POSIX すら必要なくて、bash の組み込み機能だけで実現可能であることが興味深い。
2.1 ビジーウェイト
さて、シェルの組み込み機能だけで sleep
を実現したい。しかし bash/zsh で sleep
が組み込み機能として提供されていない以上は、普通に考えて不可能な気がする。そこで "最終手段" ビジーウェイトに手を出す誘惑に駆られる。つまり、何もしないループを回して時間を潰す。
while 指定の時刻になるまで; do :; done
この方法を忌避する第一の理由は、CPU に負荷をかけることである。sleep
するだけのために、ファンをいつもより多く回すという虚しさ。
しかし、CPU の負荷を気にしないとしてもこの方法はすぐ頓挫する。"指定の時間になるまで
" をミリ秒単位で実現する可搬な方法がBash 5.0 未満では[追記 2018-08-18]ないからである。
-
date: 例えば
date (GNU coreutils)
を使えば%N
で秒以下の単位を取得できるが、それならそもそもsleep (GNU coreutils)
を使っておけば良い話である。そして POSIX のdate
には%N
はないので秒単位の計測はできない。 -
printf '%()T': ミリ秒単位の精度を保証するためにはプロセスを起動せずにシェルの組み込み機能だけで実現したい。組み込み機能で時刻を計測するといえば Bash-4.2 で追加された
printf '%()T'
が使えるが、これも残念ながら%N
など秒以下の単位に対応していない。 -
/proc/uptime: もしシステムが
/proc/uptime
に対応していれば、ひたすらread time < /proc/uptime
で値を読みだすという手も使える。プロセスを起動するわけではないので高速である。しかし/proc/uptime
が使えるシステムはsleep
が小数に対応しているシステムよりも少なそうだ。 - EPOCHREALTIME: Bash 5.0 以降では特殊シェル変数
EPOCHREALTIME
が追加され、この変数を読み取ればその瞬間の Unix 時間を秒単位の小数で取得できるようになる。例えば、1534564384.985884
などの文字列が取得できる。[追記 2018-08-18] - ループ速度の計測: @ko1nksm さんの記事 "POSIX準拠シェルスクリプトだけで1秒未満sleepを実現する - Qiita" ではループ速度を計測してループ回数を事前に計算してビジーウェイトしている。さらにCPU負荷を下げるために、複数の
sleep 1
ループをバックグラウンドでずらして起動して、本体のシェルにシグナルを送っている。POSIX の範囲内で実現しているが、記事中にもあるように精度に難あり。sleep 0.01
相当を実現するにはバックグラウンドでsleep 1
を1秒に100回実行する負荷があるし、長時間の実行でバックグランド間のタイミングがずれる。ミリ秒精度の用途には難しそう。[追記 2020-04-11]
2.2 Bash Loadable Builtins (enable -f ./sleep.so sleep
) を使う
Bash には強力な機能がある。動的に組み込みコマンドを追加できる機能だ。それ専用にビルドされた共有ライブラリを用意して、それを enable -f
で読み込む。後付なのに "組み込みコマンド" という名前は妙だが、シェルコマンドの区分としては確かに "組み込みコマンド" として取り扱われる。つまり、外部コマンドとしてプロセスを起動するのではなく、bash
のプロセス内で実行される。C/C++ などで書いた自分の好きなプログラムを bash
内で実行させることができるのだ。ちなみに zsh
にも enable -f
はあるが意味はまったく異なり、この目的では使えない。
そして実際に Bash の開発者もスリープ問題に気づいていたのか、なんと bash のソースコードに附録として sleep.c
がついてくる。sleep.c
をコンパイルしてできる sleep.so
を使えば以下のようにして新しい組み込みコマンド sleep
を動的に読み込める。
enable -f ./sleep.so sleep
しかし、この sleep.so
はBash 4.4 未満では[追記 2018-07-26]デフォルトでビルドされないし、Linux の distribution でも配布されているのを見たことはなかった[修正 2018-07-26]。 Bash 4.4 以降ではデフォルトで附録のビルトインコマンドがビルドされる様になり、それに伴って一部の Linux distribution では bash-builtins
などのパッケージ名で配布されるようになった。[追記 2018-07-26]
Bash 4.3 以前において[追記 2018-07-26] sleep
をビルドするためには Bash 本体のソースコードが必要である。そして、様々なシステムや Bash のバージョンがあることを考えるとバイナリを直接配布するわけには行かない。つまり、この機能を利用するためには、シェルスクリプトのユーザの側でおのおの以下のようなことをしてもらう必要がある。
- 自分の使っているのと同じ version の bash のソースコードを手に入れる
-
./configure
&make
する -
sleep.c
はbash-4.3/example/loadables
的な場所にある。そこに入ってmake
する。 - 完成した
sleep
をシェルスクリプトによって予め定められた場所に配置する
可搬ではないどころかとてつもなく面倒くさい。また、Cygwin では Makefile やら何やらを色々書き換えてようやく sleep.c
をビルドできた。更に、共有ライブラリ (DLL) の仕組みが特殊なシステムの場合、そもそもこのような仕組みを使えないという可能性もある (MinGW などではそもそもビルドできるのかどうかすら怪しいんじゃないかこれ…)。
追記: Bash Loadable Builtins の使い方に関しては以下が詳しい。
-
bashの組込みコマンド自作によるスクリプトの高速化 (Qiitaより移動)
コンパイル方法・利用方法・利点・欠点などについてまとめられている。 -
本を読む bashの内蔵コマンドを自作してみた
特殊変数の定義方法についても述べられている。
2.3 読み込みのタイムアウトを使う: read -t 0.01
さて、bash/zsh には組み込みのコマンドとして sleep
はないものの、考える範囲を拡げれば実行をブロックする組み込みコマンドが存在する。read
だ。そして、なんとタイムアウト (単位は秒) を引数に指定することができる。特に bash-4.0 以上ではタイムアウトとして小数を指定することができる。
read -t 0.01
しかしこのままの形で使ってしまうと、なにかが標準入力にあるとタイムアウトする前に読み取りが完了して read
がすぐに終了してしまう。そこで、何か読み取りに無限に時間のかかる (つまり何も読み取れない) ストリームに標準入力をつなぐ。
2.3.1 /dev/null
read -t 0.01 < /dev/null
は駄目だ。"何も文字が読み取れない (ストリームの終端)" ということがすぐにばれてしまうので、read
はタイムアウトする前に読み取りを諦めてしまう。
つまり、必要なのは "内容を送る送るといいつつも、いつまで経っても送ってくれないストリーム" である。
2.3.2 /dev/tcp/0.0.0.0/80
次に試したのは bash の特別ファイル名 /dev/tcp/ip/port
である。このファイル名にリダイレクトを行うと、Bash が勝手に ip:port
に対して TCP 接続 (socket) を開いてくれる。ここで、存在しないサーバに対して接続を試みればブロックされるのではないか、ということである。0.0.0.0 という IP アドレスは使われないはずなのでこれで試してみた。
read -t 0.01 < /dev/tcp/0.0.0.0/80
TCP 接続のタイムアウト (少なくとも 1 秒よりは長かろう) よりも短い時間の sleep ならばこの方法で動く。
しかし、この方法の問題点は "実際に TCP 接続を試みる" ということである。一秒間に何十回も sleep を実行するとネットワークに負荷をかけるおそれがあるし、そもそも socket の数の上限とかそういうのにひっかかるかもしれない。お手軽ではあるが実用にはならないだろう。
2.3.3 exec 9<> 名前付きパイプ
あるいは名前付きパイプを開いて、書き込み側が何も書き込まずに待てば良い。読み書き両方を自分の適当なファイルディスクリプタに割り当ててしまい、書き込みはせずに読み込みだけすることにすれば良い。
# 前準備
mkfifo sleep.tmp
exec 9<> sleep.tmp
rm -f sleep.tmp
read -t 0.01 <&9
なかなかいい感じに動く。
名前付きパイプの扱いに慣れない人のために、前準備の部分について細かい説明を追加しておく:
- まず
mkfifo
は名前付きパイプをファイルシステムの何処かに作る。 -
exec
でその名前付きパイプを読み書き両用 (<>
) で開く。exec リダイレクト
を用いて自プロセス(シェル)の
ファイルディスクリプタを弄れることを使う。上記の例では 9 番にパイプを紐付ける。 - 設置した名前付きパイプを後で削除するのは面倒なので、すぐに
rm
で削除する。ファイルシステム上ディレクトリから unlink されても、ファイルディスクリプタが生きている限りは、その名前付きパイプはシステム上に残ることを利用したやり方。
2.3.4 exec 9< プロセス置換
上の名前付きパイプの方法はいい感じに動く。しかし Cygwin では名前付きパイプが完全には実装されていない (Eric Blake - bi-directional named pipe) ので、この方法をやろうとすると失敗する。read
で Communication error on send
とかいうエラーが出る。
一方で、プロセス置換は (内部的に名前付きパイプを使っているものの) Cygwin 上でも動く。というわけでプロセス置換にしてみる。
# 前準備
exec 9< <(
[[ $- == *i* ]] && trap -- '' INT QUIT
while :; do command sleep 2147483647; done
)
read -t 0.01 <&9
書き込みを行う側の子プロセスでは何も書き込みをせずにひたすら sleep する。
ただし、子プロセスが本体のシェルより先に終了してしまわない様に注意する。特に、対話シェル上 ([[ $- == *i* ]]
) では C-c
(SIGINT
) や C-\
(SIGQUIT
) を押しても本体のシェルは終了しない。しかし、子プロセスはこれで見事に終了してしまう。これを防ぐために trap
している。しかし、この様に対策したとしても子プロセスが勝手に死なないかどうか不安が残る。
3. 実測してみよう
2.2 enable -f
と 2.3 の read -t
のアイディアを利用して様々な sleep
を実装する。それぞれで sleep 0.001
の時間を計測してみる。Bash-4.3 を使う。結果が 1.00 ms に近ければ嬉しい。
外部コマンド (比較用)
先ずは比較のためにコマンド date (GNU coreutils)
、usleep
、sleepenh
を計測する。ソースコードは、適当に検索して function-src-8.53/usleep.c と nsc-deb/sleepenh-debian から持ってきた。
コマンド | 環境1 Cygwin | 環境2 GNU/Linux |
---|---|---|
command sleep 0.001 (GNU coreutils) |
61 ms | 4.17 ms |
command usleep 1000 |
62 ms | 4.03 ms |
command sleepenh 0.001 |
62 ms | 3.76 ms |
Bash Loadable Builtin
sleep
enable -f ./sleep.so sleep
コマンド | 環境1 Cygwin | 環境2 GNU/Linux |
---|---|---|
builtin sleep 0.001 (enable -f) |
1.8 ms | 1.21 ms |
Bash ネットワーク用特殊ファイルを
read
コマンド | 環境1 Cygwin | 環境2 GNU/Linux |
---|---|---|
read -t 0.001 < /dev/tcp/0.0.0.0/80 |
2.0 ms | 1.41 ms |
read -t 0.001 < /dev/tcp/127.0.0.1/80 |
2.0 ms | 1.41 ms |
read -t 0.001 < /dev/tcp/##.##.##.2/80 |
23 ms | 14.1 ms |
read -t 0.001 < /dev/udp/0.0.0.0/80 |
3.1 ms | 1.31 ms |
追記 (2018-08-18): 最近の Windows では
read < /dev/tcp/0.0.0.0/80
は失敗するようである。read < /dev/udp/0.0.0.0/80
は動く。$ time read -t 0.1 < /dev/tcp/0.0.0.0/80 bash: connect: Connection refused bash: /dev/tcp/0.0.0.0/80: Connection refused real 0m1.001s # ← 1秒間 (指定したよりも長い user 0m0.000s # 時間) 待って失敗している。 sys 0m0.000s # $ time read -t 0.1 < /dev/udp/0.0.0.0/80 real 0m0.101s user 0m0.000s sys 0m0.000s
名前付きパイプを
read
mkfifo tmp1
exec 7<> tmp1
mkfifo tmp2
(exec 3> tmp2; sleep 2147483647) & exec 8< tmp2
exec 9< <(
[[ $- == *i* ]] && trap -- '' INT QUIT
while :; do command sleep 2147483647; done
)
コマンド | 環境1 Cygwin | 環境2 GNU/Linux |
---|---|---|
read -t 0.001 <&9 |
12 ms | 1.21 ms |
read -t 0.001 -u 9 |
12 ms | 1.22 ms |
read -t 0.001 -u 8 |
12 ms | 1.21 ms |
read -t 0.001 -u 7 |
エラー | 1.20 ms |
3.1 グラフ
正しい待ち時間 1.00 ms からのずれ (遅延) をグラフにしてみる。縦軸は対数スケールであることに注意。短い方がよい。(※編集2016-06-27: コマンド sleep
たちを追加。縦軸を 1 ms からのずれ(対数スケール)に変更。)
GNU/Linux
- 基本的に **
read -t
& 名前付きパイプが速い(ずれが少ない)**ようだ。 - 驚くべきことに、
enable -f
を通して読み込んだ組み込みコマンドsleep
は別にread -t
と較べて速い訳ではないようだ。名前付きパイプと変わらない。
Cygwin
- 一番遅いのはもちろん外部コマンド起動だが、名前付きパイプも遅い。
- 代わりに、
/dev/tcp/0.0.0.0/80
が速い。ネットワークに負荷をかけるのではないかと懸念したが、実際にやってみると通信をしている気配はない。少なくとも Cygwin では/dev/tcp/0.0.0.0/80
にアクセスしまくっても問題はなさそうだ。 - 序でに、実際に存在する IP アドレス
##.##.##.2
について同様にread
してみたらネットワーク I/O が生じるようだし遅延時間も大きい。つまり、有効な範囲の IP アドレスで/dev/tcp
によるsleep
をするのは良くない。
■ 4. おまけスクリプト (bash-4.0+/zsh)
この記事で何か楽しいことをしているようにに見せかける ために sleep
を使って端末の中で動くアニメーションを作ってみる。スクリーンセーバー的な何か。ダウンロードは Shell Animation using sleep - GitHub Gist の Download Zip から。xterm 系の端末制御シーケンス(256色対応含む)を吐くので、端末によっては動かないことに注意。
#!/bin/bash
source sleep.sh
[[ $ZSH_VERSION ]] && setopt KSH_ARRAYS
: ${COLUMNS:=$(tput cols)} ${LINES:=$(tput lines)}
((xN=${COLUMNS:-80}-1,yN=${LINES:-24}-1,mfp=((xN+yN)/15+3)*2))
printf '\e[?47h\e7\e[m'
trap -- "printf '\e[m\e8\e[?47l'" 0
[[ $ZSH_VERSION ]] && trap -- exit INT QUIT
let color=196 'colors[cN++]=color+='{6,-36,1,-6,36,-1}{,,,,}
x_iswall='x==0 &&d==2||x==xN-1&&d==0||y==0 &&d==3||y==yN-1&&d==1'
movechar=CBDA
x_move=(x++ y++ x-- y--)
while :; do
((x=RANDOM%xN,y=RANDOM%yN,d=0,c=0))
printf '\e[H\e[2J\e[%d;%dH' $((y+1)) $((x+1))
for ((i=0;i<xN*yN;i++)); do
((((a=RANDOM%mfp)<2||x_iswall)&&(
d=(d+1+(a&1)*2)%4,
x_iswall&&(d=(d+2)%4)),
x_move[d]))
printf "\e[${movechar:$d:1}\e[48;5;${colors[c++/5%cN]}m \e[D"
sleep 0.01
done
printf "\e[H\e[${yN}B\r\e[m"
done
使い方
動作には sleep.sh
と上記 snake.sh
が必要。終了は C-c
(SIGINT
) で。
$ ls
sleep.sh snake.sh
$ ./snake.sh
幅と高さは既定では端末の画面の大きさになっている。これらを明示的に指定するときは環境変数 COLUMNS
と LINES
でどうぞ。
$ COLUMNS=15 LINES=10 ./snake.sh