注意:このドキュメントは奥井がMuTerminal開発に当たり、勉強したことを98年12月にまとめたものです。自分自身のメモ程度で書かれており、必ずしも正しいことが書いてある保証はありませんので、ご了承願います。
通常、X-Windowシステム上での端末(xtermなど)は、疑似端末(Pseudo TTY:PTY)---仮想端末と訳されることがあるが、ここでは仮想端末とは実際にWindowシステム上で表示されるハードウェアエミュレータ端末とし、OSが用意しているPTYを疑似端末とする記述で統一する---を利用している。この仕組みを確立するために欠かせないのが、プロセスセッション、プロセスグループ、そしてプロセスの処理実装である。 通常シェルは、プロンプトにコマンドが入力されると、そのコマンドをフォアグラウンドとして実行する。フォアグラウンドジョブは端末へ入出力可能である。これに対して、&付きで実行されるコマンドなどは、バッググラウンドとして処理され、通常は端末への入出力は不可能である。これらはシェルのジョブコントロール機能と、端末ドライバの機能を利用している。
あるプロセスが端末に入出力できるかどうかは、端末が「どのプロセスの制御端末になっているか」で決まる。制御端末を持たないバッググラウンドジョブは、プログラム上で端末から入出力を試みるとSIGTTOU,SIGTTIUシグナルが送られる。特にトラップしてなければ、プログラムは終了する。
この「端末をどのプロセスに割り当てるか」を決めるのに重要なのがプロセスグループである。端末のプロセスグループをシェルではなく、実行中のフォアグラウンドプロセスにすることで、そのプロセスは端末への入出力が可能になる。逆に言うならば、バックグラウンドで処理したい場合、そのプロセスのプロセスグループを端末のプロセスグループとは別にすればよい。
さて、Windowシステム上で提供される仮想端末は、当然ハードウェア端末をエミュレートするプロセス(親プロセス)と、シェルが実行されているプロセス(子プロセス)の2つのプロセスを疑似端末を通じてやり取りするのだが、ここで重要なのが「子プロセスの制御端末」である。これは、当然親プロセスがエミュレートしている疑似端末でなければならない。このためには、子プロセスをプロセスグループリーダとすると同時に、子プロセスが扱う端末(つまり、親プロセスが提供する仮想端末)を、自分自身のプロセスグループにする(つまり、制御端末にする)必要がある。
一方、プロセスセッションはプロセスグループを包括するグループである。プロセスセッションリーダーをシェルにすることで、シェルに対して終了シグナル(通常はSIGHUP)を送ると、セッション中のすべてのプロセスにも同様にシグナルが送られる。当然、シェルをプロセスセッションリーダーとしておく必要がある。
システムコールfork()を呼び出して複製されたプロセス(子プロセス)は、親プロセスの制御端末をそのまま継承する。通常、アプリケーションは起動した時点で制御端末を持っているが、子プロセス(つまりシェル)に関しては、この端末、すなわちすでに親プロセスでオープンされている制御端末ではなく、親プロセスが提供する疑似端末を制御端末としなければならない。
実際に疑似端末を利用するには、次のような手順が必要になる。
まず、/dev/pt/XXから、利用可能な疑似端末を探す。このファイルは1文字目がpqrs、2文字目が0からfまでの2文字で表される、64個の疑似端末の中から検索する。
利用可能な疑似端末が見つかったら、次にプロセスを複製する。この時点で2つのプロセスが存在することになる。親プロセスは仮想端末であり、子プロセスはシェルである。
子プロセス側では、シェルを起動する前に、自分が利用する端末を変更する必要がある。前述の通り、fork()直後の子プロセスの制御端末は、親プロセスが起動したときに割り当てられている制御端末である。そこで、これらの端末デバイスファイルをクローズし、親プロセスが用意した疑似端末をオープンしたうえで、制御端末をこの疑似端末にする。
たとえば、親プロセスが疑似端末/dev/pt/p4をオープンしたならば、子プロセスは/dev/tt/p4を開き、このデバイスファイルをアクセスすることによって、親プロセスにデータを送ることが出来るようになる。
プロセス間での情報のやり取りならば、なにも疑似端末を用いなくても、パイプやリダイレクション、BeOSならばメッセージを利用できる。であるにもかかわらず、疑似端末を利用しなければいけない理由は、端末というデバイスの制御が必要だからである。
たとえば、パスワード入力時には、端末はエコーを禁止しなければならないし、シリアル回線に接続されているならば、ビットレートなどを決めておかなければ、正しい画面表示も出来ない。これらの設定は通常、シェル上で実行されているプログラムが『システムコールを利用して』変更する。端末をエミュレートする側のプロセスは、当然エコー禁止ならば文字表示を禁止しなければいけないが、そのような情報はシステムコールによって変更されるため、親プロセスがその情報を入手する術が無いのである。
このため、疑似端末を利用しなければいけない。
これらの端末制御用システムコールを利用すると、/dev/tt/XXがそのシステムコールにしたがって、挙動を変更する。通常、親プロセスである仮想端末にキー入力があると
Virtual Terminal → /dev/pt/XX → OS → /dev/tt/XX → shell
というふうにシェルにその情報が伝わるのと同時に、OSによって
OS → /dev/pt/XX → Virtual Terminal
というように、仮想端末にもその情報が届く。当たり前の話だが、これがエコー動作である。エコーを禁止するシステムコールを使うと、このOSからの戻りが無くなる。(パイプやリダイレクトではなく)疑似端末を利用することによって、仮想端末はただ単純に疑似端末から文字を読み出すルーチンと文字を書き込むルーチンをを書くだけで(正確にはもう1つ、エスケープシーケンスを解釈するルーチンが必要である。後述)、シェルやその他のコマンドが意図した動作を実現することが出来るのである。
これらの端末制御用システムコール(インターフェース)は、BSDではsgtty, System Vではtermio, POSIXではtermios(termioのスーパーセット)の3つがある。BeOSはPOSIX準拠だから、当然termiosを装備している。ただし、一部サポートされていないコマンドもあるようである。
termiosは、1つの構造体の値を変更することで、次のようなことができる。
入出力別に復帰改行を改行のみにしたり、復帰を改行にしたりなど、改行コードの変更をする 。^S ^Qなどの出力のスタートストップを有効(または無効)にする エコーを有効(または無効)にする。 kill, erase, start, stop, suspendなどのキー割当の変更できる、等々。
Emacsなどでは、キーのバインドが大幅に変わる。^Sはインクリメンタルサーチであるし、^ZはSuspend Emacsである。もちろん、これはデフォルトであって、いくらでも変更できる。このように、アプリケーションがキー入力を変更した場合でも、どの制御文字がどの機能に割り当てられているかを仮想端末は知る必要はない。STOPキー(^C)が押されたときに、プロセスにシグナルを送るのは、仮想端末の役割ではなく、OSのと端末デバイスドライバの役割だからである。
シグナルは、POSIX APIが実装する最も単純なメッセージ通信である。通常、プロセスにシグナルを送る仕事はOSであることが多いが、プロセスが他のプロセスに対してシグナルを送ることも可能である。
このうち、仮想端末において重要なのは制御端末が閉じられたときに送られるSIGHUPと端末のサイズが変わったときに送られるSIGWINCH、子プロセス、すなわちシェルが終了したときに親プロセス、すなわち仮想端末側に送られるSIGCHLD、そしてキーボードが生成するシグナルSIGINT, SIGSTOP, SIGCONTである。
まず、SIGCHILDについてはトラップを行わない。これは、起動した子プロセスのどれか1つでも終了すると、MuTerminalにシグナルが送られるためである。MuTerminalからプログラムを起動し、それが終了すると、シェルが終了していないにもかかわらず、MuTerminalが終了するのは具合が悪いからである。
次にキーボードが生成する3つのシグナルだが、このうち、現在はSIGINTのみを実装している(デフォルトのTerminalもSIGINTのみ)。このシグナルは、キー入力時に、端末ドライバからVINTR文字を取得し、キー入力がこれと同じ文字ならばkillシステムコールを使用してシェルのプロセスグループにシグナルを送っている。
通常、端末に対してread()システムコール(あるいは、read()システムコールを利用している、scanf()ライブラリルーチン)などを行うと、入力があれば即座にプロセスに制御が戻るが、なければプロセスはブロックされる(つまり入力があるまでプロセスに制御が戻らない)。UNIXなどでは、仮想端末アプリケーションのように、入力と出力を同時に行う、つまり多重入出力を行うアプリケーションを実現するには、selectシステムコールを使うのが一般的である。
ところが、BeOSでは、ソケットディスクリプタが抽象化されていないことから、selectシステムコールが利用できるディスクリプタはソケットディスクリプタのみであり、ファイルディスクリプタには対応していない。
UNIXでは、もう1つの実装方法、すなわちalarmシグナルを利用することで、入出力の多重化が可能である。ただし、alarmは秒単位でのタイムアウトしか設定できず、ここでの利用に適していない。
そこで、スレッドを使う。
スレッド中ではreadコールによってたとえブロックされても、他のスレッドには影響ない。そこで、このスレッドではreadコールによるシェルからの文字の取得、および文字コード変換やエスケープシーケンス解析を行う。
一方、端末への出力、すなわちキーボードからの入力処理は、Viewオブジェクトの方でおこなう。KeyDown()によってイベントを取得できるため、これをwriteシステムコールを使って、Viewオブジェクトから直接疑似端末へ出力する。この処理は(文字コードのエンコーディングがなければ)、特に難しいことはない。
シェルからの出力にエスケープシーケンス文字が含まれていた場合、仮想端末はその文字を解釈したうえで、適切な処理(カーソル移動や、スクロール)を行わなければいけない。
MuTerminalは、最終的にはANSI端末のクローンとしてエスケープシーケンスを実装する予定である(すなわち標準のTerminalと同様のコマンドを装備する)。ただし、当初もっと低機能な端末機能の実装になるかもしれない。端末が持つべき機能は、多ければ多いほど端末上のアプリケーションは楽に作成できるが、今世界に出回っているほとんどのアプリケーションは、機能の低い端末でも正しく動作するように作られているため、複雑な動作はあとまわしにする、あるいは実装しなくてもまず支障はない。Emacsやviなどのプログラムは、いくつかのエスケープコマンドを組み合わせて、複雑な画面描画を行うルーチンを装備しているからである。
MuTerminalは文字の内部コードとしては、BeOSの内部コードと同じ、UTF8を利用する。また外部コードはユーザーのメニュー操作によって、将来的にはSJISやEUC,Big5,ISO-8859-X,ISO-2022-XXにも対応していきたい。
多国語、特に多バイト文字を扱う場合の重要な問題は、多バイト文字コードの2バイト目に制御文字が含まれる場合である。外部コードとしてSJISなどを利用する場合、SJISは2バイト目に制御文字が使われることがあるため、特にシェルからの出力文字を正しく処理しなければ、2バイト目を間違って解釈し、画面表示がめちゃくちゃになる。このため、外部<->内部コード変換は、端末との入出力にいちばん近い部分で行わなければならない。MuTerminalでは、エスケープシーケンスを解析するクラスとViewクラスのKeyDownで、文字コードの変換も同時に行うことにする。
POSIX APIとBeAPIの両方を使わなければならない端末エミュレータでは、どのタイミングでどちらのAPIを使うかが重要になる。ここでは、各クラスの説明と、実装方法を記述する。
POSIX API部(主にCで記述)
spawn_shell()・・・シェルの起動
シェルの起動は、アプリケーションが起動した直後(すなわち、main()ルーチンの中で)行う。シェルを起動する際には、forkやexecなどのPOSIX APIが多用されるが、Windowオブジェクトなどを生成した後では、これらのシステムコールが正しく動作する保証はない(ためしたわけではないが)。このルーチンはCのみを用いて記述されており、どのクラスにも属していない。 この関数では、シェルの起動以外に、端末の設定、シグナルハンドラの設置なども行われる。
この2つのクラスでは今のところこれといって複雑な作業は行なっていない。
TermParseクラス
TermParseクラスは、MuTerminalで最も重要なクラスである。EscParseメンバ関数がスレッドとして生成され、この関数内部で、シェルからの出力を、文字コードの変換やエスケープシーケンスの解析を行う。
TermViewクラス
TermParseクラスからの呼び出しに応じて、画面を更新する。BTextViewが使えないため、独自のテキストエンジンを装備し、カーソル移動など、端末として動作するために必要な描画用メンバ関数をもつ(参考:TermViewについて)。また、キー入力はこのオブジェクトが受け取り、直接疑似端末に書き込む。
TermBufferクラス
MuTerminalの内部バッファクラスである。バックスクロール用に512行分の文字をループバッファ形式で保存している。内部コードは4バイト固定で、先頭バイトが端末属性(色や反転など)、のこり3バイトが文字情報である。TermBufferでは、処理を簡便化するため、1byteASCII文字も4バイトのメモリ空間を占有する。