リダイレクトの実装
yash のリダイレクトの実装を完全に POSIX 準拠にするためのメモ書き。
シェルスクリプトを書く上でのリダイレクトの使い方を解説したページはたくさんあるが、リダイレクトをどう実装するかを解説するページは見当たらないな。って、当たり前か。
普通の外部コマンドに対するリダイレクトの実装は簡単で、コマンドを起動する際、fork したあと exec する前に、リダイレクト先のファイルを open して、必要に応じて dup2/close すればよい。
問題は組込みコマンドや制御構造 (if や while など) に対するリダイレクトで、こちらは一筋縄では行かない。外部コマンドでは fork した後にリダイレクトを処理するので、シェル本体のプロセスのファイルディスクリプタは影響を受けないが、組込みコマンドは fork せずにシェル本体で実行する必要があるので、シェル本体のプロセスのファイルディスクリプタを直接変更しなくてはならない。なおかつ、コマンドの実行が終わったら、シェル本体のファイルディスクリプタは元の状態に戻っていなければならない。
その為には、ファイルディスクリプタを変更する前に元のファイルディスクリプタを覚えておく必要がある。つまり、fcntl で元のファイルディスクリプタを複製してからリダイレクトを行い、コマンドの実行が終わったら複製しておいたファイルディスクリプタを dup2 で元のファイルディスクリプタに戻し、複製しておいたものは close する。一般に、複数のリダイレクトを行うと複製したファイルディスクリプタが更に複製されることがあるが、このような場合でも正しく元の状態に戻すために、複製したのとは逆の順番で元に戻すようにする。
>/dev/null
はファイルを開くリダイレクトであるが、>&-
のようにファイルディスクリプタを閉じるリダイレクトもある。もちろんこの場合も同様にファイルディスクリプタを複製しておかなければならない。また組込みコマンドでは関係ないが、制御構造に対するリダイレクトでは、制御構造の中で外部コマンドが実行されることがある。この時、複製しておいたファイルディスクリプタが外部コマンドによって扱われてしまってはまずい。複製したファイルディスクリプタは本来存在しないはずのものだからだ。従って、外部コマンドを起動する前に複製しておいたファイルディスクリプタを閉じる必要がある。いちいち閉じるのが面倒臭ければ、ファイルディスクリプタに FD_CLOEXEC フラグを付けておくという手もあるが、複製したファイルディスクリプタが更に複製された場合にフラグが元に戻らないので注意を要する。そもそも、子プロセスが fork したらその子プロセスではもうファイルディスクリプタを元に戻す必要はないので、fork 直後に複製を削除するのが一番よい。
ファイルディスクリプタの複製に dup ではなく fcntl を使うのは、新しい複製が標準入出力などと被るのを防ぐためである。既に述べたように、複製したファイルディスクリプタというのは本来存在しないはずの物である。つまり、あってはならない物がある状態で組込みコマンドが実行されるのだ。従って、複製したファイルディスクリプタが組込みコマンドによって扱われては困るのである。幸いにして、全ての組込みコマンドは標準入力・標準出力・標準エラー出力以外のファイルディスクリプタを使用しない。よって、これら以外のファイルディスクリプタに複製するようにすれば問題ないわけだ。POSIX では、一桁の番号のファイルディスクリプタはユーザが自由に使ってよいことになっているので、単純に標準入力・標準出力・標準エラー出力の三つを避けるよりは、常に 10 番以上に複製するようにした方がよい。
ちなみに、古いシェルの実装では (今の yash の実装もそうだが) 制御構造に対してリダイレクトを行うと制御構造全体がサブシェルで実行されるものがあるが、これは POSIX に準拠しない動作である。
さて、もう一つ考慮すべきことがあって、それは制御構造の中でリダイレクトが exec される場合である。例えば簡単な例を挙げると、
{ exec >/dev/tty; } >/dev/null
普通の組込みコマンドや制御構造のリダイレクトは、実行前にファイルディスクリプタを複製し実行後にファイルディスクリプタを元に戻すようにすればよかった。しかし exec は例外で、exec に対するリダイレクトは exec が実行し終わった後も残る。このため、次のような二つの事態をどう処理するか考えなくてはならない。
一つ目は、既述の例のように、exec のリダイレクトとそれを囲む制御構造のリダイレクトが被っている場合。もし exec がなければ、制御構造に入るときに標準出力がリダイレクトされ、出るときに標準出力が元に戻るのだが、例では制御構造の中で標準出力がリダイレクトし直されている。制御構造から出るとき、標準出力は exec でリダイレクトされたままにすべきか、それとも制御構造に入る前の状態に戻すべきか? POSIX に明確な規定はないが、多くのシェルは後者の動作をするようだ。(実際、その方が実装が簡単だろう)
もう一つは、exec のリダイレクトが既存のファイルディスクリプタの複製を上書きしてしまう場合だ。exec がない場合では、ファイルディスクリプタの復元は複製とは逆の順番で行えばそれでよかった。つまり、スタックへのデータの出し入れと同様の順番で処理を行えば、制御構造が入れ子になったりしても問題はなかった。この仕組みでは、ファイルディスクリプタの複製が更に複製されても、最初の複製を復元する前に二回目の複製を復元するので、問題は起こらない。しかし exec のリダイレクトが複製を書き換えた場合、それはもう元には戻らないので、最終的に復元が正しく行えない。例えば以下のコードを考える:
{ exec 10>&-; } >/dev/null
最初、標準出力は端末に繋がっているとしよう。制御構造 { }
に入る前に標準出力が /dev/null にリダイレクトされるが、このとき元の端末へのファイルディスクリプタは 10 番のファイルディスクリプタに複製されたとする。すると、制御構造から出るとき、10 番のファイルディスクリプタを標準出力に戻すことで標準出力を復元しようとするが、制御構造の中の exec によって 10 番のファイルディスクリプタは閉じられているので、復元できない。
これに対する解決策は二つほど考えられる。一つ目は、exec のリダイレクトで 10 番のファイルディスクリプタを閉じる前に、10 番のファイルディスクリプタを他の番号に移すことだ。この場合、このファイルディスクリプタはもう 10 番には戻らないので、後でファイルディスクリプタを復元するときに 10 番からではなく移動先から復元する必要がある。二つ目は、複製したファイルディスクリプタを exec のリダイレクトで変更しようとしたときはエラーにすることだ。10 番以上のファイルディスクリプタはユーザにとって使えなくてもよいことになっているので、複製は常に 10 番以上に作るようにしておけば、10 番以上に対するリダイレクトを全てエラーにしても POSIX には反しない。
という考察を踏まえた上で、yash のリダイレクトの実装を完成させる予定。既にほとんどできているので、長くはかからないはず。方針は、
- 複製したファイルディスクリプタは、fork した子プロセスでは直に閉じるようにする。
- exec のリダイレクトとそれを囲む制御構造のリダイレクトが被っている場合は、他の多くのシェルと同様に、ファイルディスクリプタを元に戻す。
- exec のリダイレクトが他のファイルディスクリプタの複製を上書きしそうな場合は、エラーにする。ただし 10 番以上は全部エラーにするのではなくて、上書きしそうな場合だけエラーにする。複製の番号は、10 番からには拘らず、もっと大きめの値を取る。(といっても、あまり大きくしすぎると select が遅くなるのだが)
なお、この記事では10 番のファイルディスクリプタ
というような言い回しをしたが、これは厳密には正しくない。ファイルディスクリプタとは本来は番号そのものを指す言葉だからだ。
| 固定リンク
コメント