学習用OSのXV6には簡易的なシェルが付属しているが、ヒストリ機能を扱えるようにしたという話。
シェル上でカーソルキーの入力を受け取るには
XV6のシェルはユーザ空間で走るプログラムshとして用意されている。 コマンドラインからの入力を1行ごとに受け取って、指定されたコマンドに従ってプログラムを起動する、ということを繰り返している。
入力された行の読み込みはgetcmd関数内のgets呼び出しで行っている。
gets
ではシステムコールread
を呼び出して1文字ずつ読み出して、改行が入力されるまでをバッファに格納している。
これを入力途中にカーソルキーの上下で過去に入力した行を指定、編集できるようにしたい。
read
で読み込んだキャラクタがカーソル上下だったら…とすればいいかと思いきや、そう単純にはいかない。
read
への1バイト要求に対して、なにかキーが入力された瞬間に処理が返されるわけではなくて、カーネル内で改行が入力されるまでバッファリングされるようになっている。
バックスペースによる行編集はカーネル内で行われて、最終的にENTERで確定した行がユーザ側では得られるのでその限りでは便利なんだけど、カーソル上下が押された場合に対応できない。 カーネルにヒストリ機能を持たせてしまうのは、ヒストリ機能の変更にカーネルの修正が必要になってしまうのでよくない。
そこでどうするか。 ENTERの入力までバッファリングされる機能の有効/無効を制御できるようにして、ユーザプログラム側で1文字ずつの入力ごとに処理できるようにする。
termios互換を用意する
Linuxにはioctl
というデバイスを制御するシステムコールがあって、これを呼び出せば標準入力のバッファリングの設定も制御できる。
これを模したシステムコールをXV6にも用意してやって、互換性をもたせられるようにする。
ioctl
を直接使ってもいいんだけど、それをラップする?ライブラリでtermiosというものがあるのでそのライブラリも合わせて用意してやる。
実装はtcsetattr
と、設定を復帰させるためのtcgetattr
を簡易的に用意した。
ioctl
経由で受け取ったカーネル側でバッファリングやエコーバックの有無を切り替えるようにする。
キーボード入力でカーソルキーを扱う
XV6をqemuでのエミュレーション(や実機)で動かす場合はI/Oから受け取ったキーコードが内部で変換され、カーソルキーは0xe2
〜0xe5
に割り当てられてコンソールで扱われている。
それに対してqemuの-nographic
オプションで動かす場合はターミナル経由になるので、エスケープシーケンス(カーソル上:\x1b
, [
, A
、カーソル下:…B
)で渡される。
シェル上では統一して扱えるほうが都合がいいので、キーボードからの入力もコンソールに渡す際にエスケープシーケンスに変換してやる。
ヒストリ機能を実装する
以上で1文字入力ごとにユーザプログラム側で受け取れるようになったので、後は好きなようにできる。
バッファリングやエコーバックの切り替えは、kiloのソースを参考に、
|
などという関数を用意してやって、
シェルで行入力を行う際には setRawMode(true)
で1文字ごとに受け取れるようにして、カーソル上下キーが入力されたら編集中の入力を消去して過去に入力された行を復帰させてやればヒストリ機能が実装できる。
行が入力されてコマンドを実行する際には setRawMode(false)
で通常のバッファリングに戻してやる。
またプログラム終了時に必ず戻すようにするために atexit()
でも設定しておく。
行入力を扱う関数のインタフェースは別に任意で構わないが、既存のReadlineに似せて作るといいかもしれない (できてないけど…)。
そんなこんなでXV6にPOSIX的な開発環境を少し整えたので、改造したsh.cはXV6で動くし、Linuxでもコンパイルして動かせる。 機能を実装するのはLinux上で行えて便利。