UNIX 6th code reading - キャラクタデバイスドライバ

はじめに

今回からLions本の最終セクションを見ていきます。

今回は22章を扱います。最初にキャラクタデバイスとは何かを解説し、その後ラインプリンタのデバイスドライバを見ていきます。

キャラクタデバイスとは

UNIXではデバイスを二つの種類に分けて扱っています。キャラクタデバイスとブロックデバイスです。

特徴の比較です。

キャラクタデバイス ブロックデバイス
低速(1秒辺り1,000bytes以下) 高速
1byteずつアクセス ブロック単位でアクセス
先頭から順にしか辿れない(シーケンシャルアクセス) 好きな場所にアクセスできる(ランダムアクセス)
テープリーダ、ラインプリンタ、対話型端末など ディスク装置など

前回のエントリまでは主にブロックデバイスについて見てきました。今回のエントリからキャラクタデバイスをどう扱うかについて見ていきます。

special file(inode)

キャラクタデバイスを使用するために、予め/etc/mknodを実行してキャラクタデバイスに対応したspecial file(inode)を作っておく必要があります。適切なmajor, minor番号を設定しないといけません。

special file(inode)のi_addr[0]にはmajor, minor番号が格納されます。

キャラクタデバイスの処理もu.u_ofile[ ], file[ ], inode[ ]構成を使って行いますので、special file(inode)に対しても、通常のファイルと同じようにopen, close, write, readを使って処理を行います。

処理対象がキャラクタ型のspecial fileだとわかると、open, close, write, readシステムコールの中で、適切なキャラクタデバイスハンドラ(cdevsw[ ])を呼び出します。

デバイスドライバ

以前RK(ブロックデバイス。ディスク装置)のディスクドライバを見ましたが、キャラクタデバイスも同じようにデバイスドライバカーネルとデバイスインターフェイスを担います。

可能ならば、一つのデバイスドライバで似たタイプのデバイスを扱えるように、さらには複数のデバイスを同時に扱えるように、デバイスドライバのコードを書きます。

UNIXにはbuffer poolが設けられており、通常キャラクタデバイスデバイスドライバは、buffer poolにデータを送るputc( )と、buffer poolからデータを取得するgetc( )を使用して処理を行うように書かれています。

buffer pool, putc( ), getc( )の詳細は次回のエントリで扱う予定です。

LP11(ラインプリンタ) デバイスドライバ

実際に、キャラクデバイスドライバの一つであるLP11のデバイスドライバを見ていきます。

ソースで言うとunix/lp.cがそれにあたります。

LPのレジスタや関わるデータなど

LP11は二つのレジスタを持っています。

  • status register
  • data buffer register


status registerには、LPがready状態であることを示すDONEフラグ、LPの動作が完了した場合、もしくは、エラーが起きたときにシステムに割り込みをかけることを表すIENABLEフラグ、エラーが起きたときにセットされるERRORフラグが格納されます。

data buffer registerには出力する1文字のデータ(7bits)を格納します。data buffer registerに値が格納されると、LPの文字出力動作が開始されるようです(要確認)

もっと詳細が知りたい方はLP11のhandbookをご覧になってください。

カーネルの中ではLPのレジスタを示すアドレスが設定(8812行目、8823行目)されています。

#define LPADDR 0177514

struct {
   int lpsr;
   int lpbuf;
};

以下のように書くことで、LPのレジスタにアクセスすることができます。(memory mapped IO)

  LPADDR->lpst ;
  LPADDR->lpbuf ;

また、カーネル中にはlp11という構造体が定義されています。(8829行目)

struct {
  int cc;
  int cf;
  int cl;
  int flag;
  int mcc;
  int ccc;
  int mlc;
} lp11;

cc, cf, clはputc( ), getc( )の中で使用されます。ccはbuffer poolに格納されている文字数(byte数)を表すので、buffer poolが溢れないかどうかをチェックするときにも使用されています。

flagはLPが持っている機能やオープン済みかどうかを表します。mccは1行中に実際に出力した文字数、cccは1行中に出力する文字数、mlcはページ中の行数を表し、LPの出力文字の位置調整などに使用されます。

スペースやインデントのために空白を出力するときなどは、その度に空白を出力するのではなく、cccをインクリメントだけしておきます。

実際に何か文字を出力するときに、mccとcccの差分だけ空白をまとめて出力してから、その文字を出力するようになっています。

lpopen( )

lpopen( )(8850行目)はLP11のopen処理を行います。

open時、openシステムコール->open( )->open1( )->openi( )と処理が流れていき、openi( )の中でopenの対象がキャラクタデバイスだった場合(inodeのi_mode&IFMTがIFCHRのとき)cdevsw[ ].d_open( )が実行されます。

対象がLPの場合は、cdevsw[ ].d_open( )でlpopen( )が実行されます。(4669行目からのcdevsw[ ]の設定の中のLPのハンドラ設定である4675行目を参照)

lpopen( )は、既にデバイスがopenされているか、また、デバイスがエラー状態(電源が入っていないなど)にあるか調べます。オープンされている場合、もしくは、エラー状態ならばエラーでreturnします。

オープンされていない場合は、フラグやレジスタの設定を行います。

カーネル内部で扱っているLP11のフラグ情報に、IND(8文字のインデント) , EJECT(フォームフィード機能(=改ページ機能?)がある), OPEN(オープン済)をセットします。そして、LP11のstatus registerにIENABLEをセットして、LPの処理完了やエラー時にシステムに割り込みが発生するようにします。

最後にLPに改ページを行わせopen処理の完了です。

lpwrite( )

lpwrite( )(8870行目)はLPに文字列の出力を行わせようとするときに呼ばれます。

write時、writeシステムコール->write( )->rdwr( )->writei( )と処理が流れていき、writei( )の中でwriteの対象がキャラクタデバイスだった場合(inodeのi_mode&IFMTがIFCHRのとき)cdevsw[ ].d_write( )が実行されます。

対象がLPの場合は、cdevsw[ ].d_write( )でlpwrite( )が実行されます。

cpass( )を使ってユーザ領域から1byteデータを読み取り、lpcanon( )に送り1文字LPに出力させます。出力するデータがなくなるまでこれを繰り返します。

lpcanon( )

lpcanon( )(8879行目)は1byteの文字にフィルタをかけてから、状況に応じて出力位置を調整したり、lpoutput( )を実行してLPに1文字出力させたりします。

フィルタの内容は以下です。

  • CAPフラグが立っていたら、小文字を大文字に変換する
  • LP11で出力できない文字を、出力可能な文字に置き換える(例えば'{'は'('に変換されます。実際に出力されるときには変換された文字の上に'-'で打ち消されたものが出力されます)


フィルタをかけ終わったら、状況に応じて以下の処理を行います。文字の出力位置の調整が必要なので、lp11のccc, mcc, mlcの値を適切に変更します。

  • tab( '\t' )の処理。インデントが8文字単位になるように調整
  • 改ページ処理
  • 改行処理
  • back space処理
  • 空白処理
  • lpoutput( )でLPに文字出力
lpoutput( )

lpoutput( )(8986行目)はLPに1byte(=1文字)の出力を行います。

putc( )を使って、1byte dataをbuffer poolに送った後に、lpstart( )を実行してLPの動作を開始させます。

ただし、buffer poolのデータ(出力待ちの文字列の数)(lp11.cc)がある程度多い場合(LPHWAT=100以上)、buffer poolが溢れないように寝ます。buffer poolの文字列が消化されていくと、lpint( )により起こされます。

lpstart( )

lpstart( )(8967行目)はLPの動作を開始させます。

LPが入力待ちの状態で、buffer poolにデータが入っていれば、buffer poolから1byte取り出して、それをLPのdata buffer registerにセットします。

LPのdata buffer registerに値がセットされるとLPの文字出力動作が開始されます。

文字出力動作が完了すると、LPから割り込みがかけられlpint( )が実行されます。

lpint( )

lpint( )(8976行目)はLPの動作完了じにかけられる割り込みのハンドラです。

lpstart( )を実行し、buffer poolにデータが残っていれば出力処理を続けさせます。

buffer poolのデータ(出力待ちの文字列数)が0, もしくは、LPLWAT(=50)になったら、lpoutput( )で出力待ち文字列の消化を待って寝ているプロセスを起こします。

lpclose( )

lpclose( )(8863行目)はLPのclose処理を行います。

close時、closeシステムコール->close( )->closef( )->closei( )と処理が流れていき、closei( )の中でcloseの対象がキャラクタデバイスだった場合(inodeのi_mode&IFMTがIFCHRのとき)cdevsw[ ].d_close( )が実行されます。

lpclose( )はLPに改ページを送り、lp11.flagを0クリアします。

コードメモ

lpopen( )
  • arguments
    • dev : device No(unused)
    • flag : read, write flag?(unused)


この引数は、bdevsw[ ]のインターフェイスに合わせるために設定されているだけで、lpopen( )の中では使われていない

  • 8853-8855 : LPがopen済(lp11.flagのOPENがセット)か、もしくは、LPがエラー状態(電源が入っていないなど)ならば、u.u_errorにEIO(IOのエラー)をセットしてreturn
  • 8857-8859 : LPの初期設定。flag, status registerをセットしてLpに改行出力
lpwrite( )
  • 8874-8875 : cpass( )を使ってユーザ領域から出力するデータを1文字(1byte)ずつ取得してlpcanon( )経由でLPに出力
lpcanon( )
  • arguments
    • c : 出力する1byteの文字
  • 8884 : CAPフラグ(大文字しか出力できない?)が立っていたら
    • 8885-8886 : 小文字だったら大文字に変換
    • 8887 : 出力できない文字の置き換え。変換後の文字に'-'で打ち消し線を入れた文字が出力される
      • 8889-8891 : { -> (
      • 8893-8895 : } -> )
      • 8897-8899 : ' -> \'
      • 8901-8903 : | -> !
      • 8905-8906 : ~ -> ^
      • 上記文字の変換後、lapanon( )を再度呼んで変換後の文字を出力する。それが終ったら文字カウントcccをデクリメントし、c1に'-'をセット。これで、この後の処理により出力済の文字の上に'-'出力される
    • 8915 : 特殊文字の出力や出力位置の調整など
      • 8917-8919 : tab(\t)の処理。文字カウントcccをインデント8の倍数になるように調整しreturn
      • 8921-8932 : FORM(改ページ?)と改行(\n)処理。ページ回復機能(?)を持っていないか、ページ中に既に文字が出力されていたら、mccを0にし、mlcをインクリメントする。ページ中の行数がある程度多いか、ページ回復機能がなければ改ページ出力。そうでなければ元々の改行か改ページを出力。改ページを出力した場合は行数を示すmlcを0にクリアする
      • 8934-8938 : リターン(\r)の処理。cccを0にし、インデントフラグが立っていたらcccを8にする
      • 8940-8943 : back space処理。cccが1以上ならばデクリメント
      • 8945-8948 : 空白処理。cccをインクリメント
      • 8949-8962 : 通常文字ならば出力処理。cccがmccより少ない場合は、改行(リターン?)をしないといけないので、'\r'を出力してmccを0に戻す。cccがMAXCOL(1行中に出力できる文字数?)以下ならば、文字列の出力を行う。cccがmccより大きい場合は空白を出力する必要がある。cccとmccの差分だけ空白をまとめて出力して出力文字位置を調整する。その後lpoutput( )で文字を出力し、mccをインクリメント。
      • 8963 : cccをインクリメント
lpoutput( )
  • arguments
    • c : 出力する1byteのデータ
  • 8988-8989 : buffer poolにLPHWAT(100)文字以上データが入っていたら、データが消化されるまで寝る
  • 8990 : putc( )を使って1byteのデータをbuffer poolに送る
  • 8991 : LPの出力処理中に、LPからの割り込みが入らないようにspl4( )でプロセッサ優先度を上げておく
  • 8992 : lpstart( )を呼んで、LPの出力動作を開始する
  • 8993 : LPの動作が終ったらspl0( )を呼んでプロセッサ優先度を0にする
lpstart( )
  • 8971-8972 : LPがready状態(LPのstatus registerのDONEフラグが立っている)で、buffer poolにデータが残っていたら、buffer poolのデータ1byteをLPのdata bufferにセットする。data bufferに値がセットされるとLPの出力動作が開始される
lpint( )
  • 8980 : lpstart( )を実行して、buffer poolにデータが残っていたら出力処理を継続する。LP出力処理が完了して、割り込みが発生して、lpint( )が実行されて、再度lpstart( )が呼ばれるまでの間にLPのstatus registerのDONEフラグが立つことを想定している?
lpclose( )
  • 8865 : LPに改ページを出力
  • 8866 : flagをクリア

終わりに

テープリーダのデバイスドライバであるunix/pc.cは、大きな流れは今回見たラインプリンタのデバイスドライバと同じなので、時間があったら追うことにします。

次回はbuffer pool, putc( ), getc( )を追い、キャラクタデバイスの処理の詳細を理解していこうと思います。