Be Driven
  BeOS におけるデバイスドライバ Device Drivers in the Be Os
     
    Device Drivers

デバイスドライバのセマンティクス

覚えておこう。

  • C 言語で書く。

  • C 言語としてコンパイルする。

  • デバイスドライバとしてリンクする。

  • C++ を使わない。 [ 信じられないなら 言語の章 を読むこと ]

デバイスドライバは特殊なアドオン。

アドオンはvoid main( void )を持たず、 アプリケーション空間で使うために共有ライブラリとして コンパイルされる。

デバイスドライバも void main( void ) がない点では 似ているが、デバイスドライバオブジェクトとして コンパイル・リンクされ、直接カーネルにリンクされる。 また、メモリ配置に関して異なった要求があるので、 コンパイラは適切な -flags 集合によって そのための補正をする。

この二つの名称を混同してはいけないが、 分かりやすく言うなら、デバイスドライバはデバイスドライバ規約に 従うカーネルアドオンだ。

モジュールは特殊なデバイスドライバで、また別のプロトコルに従う。 つまり、ただ一つの関数 modules() だけをエクスポートする。

デバイスドライバは、add-onの親戚であり、また、 自分の役割を果たすために、
「ある決まった関数の集合を実装することが求められる。

その関数は今のところ init_hardware, init_driver, publish_devices, find_devices, uninit_driver だ。

init_hardwareはいつも最初に1回だけ呼ばれる。 ここでドライバがしなければならないのは、ハードウェアが使えるか 検査することと、ハードウェアを known state に設定することだ。 もし init_hardware がエラーを返せば、その他の関数が 呼ばれることはない。

init_driveruninit_driver は ドライバがロードされるときとアンロードされる時に毎回呼ばれる。 典型的にはここでメモリを確保したり開放したりする。

デバイスドライバがアンロードされ得るということを 理解するのが重要だ。なぜなら、そのために、 デバイスドライバがリロードされる度に 大域変数が未定義な状態にリセットされるからだ。 これらの変数の初期化は init_driver で行い、 init_hardware では行ってはいけない。 init_hardware はドライバが ロードされる度に呼ばれるわけではないからだ。 publish_devices/dev の階層構造を 作り上げる時にカーネルから呼ばれる。この関数は /devからの相対パスのデバイス名を NULL 終端した配列に納めて返す。

find_device は、あるプログラムが特定のデバイスを 使おうとしたときにカーネルが何をすればいいか調べるために呼ばれる。 この関数は、ユーザ空間の関数にマップする関数ポインタの構造体を 返し、カーネルはそれに従って呼び出しを行う。

これらの関数はエクスポートされていないということに注意。 この理由は、ドライバは複数のデバイスを提供することがあり、 それぞれの振る舞いが違っていれば、それぞれ別の関数が必要だからだ。」
- Jean-Baptiste Queru.

この関数の基本集合が現在のほとんどのデバイスを表せると仮定して、 次の構造体をエクスポートしよう。

device_hooks my_device_hooks = { 
    my_device_open, /* -> open entry point */ 
    my_device_close, /* -> close entry point */
    my_device_free, /* -> free cookie */
    my_device_control, /* -> control entry point */ 
    my_device_read, /* -> read entry point */ 
    my_device_write /* -> write entry point */ 
 };

バージョニング


バージョンコントロールの導入

不本意ながら、後になって我々はドライバ API が完璧でないことに 気付いた。しかし、後で改良、または「追加」をする余地はあった。 それが R4 でドライバ API にバージョンコントロールを 導入する理由だ。そういうわけで、全てのドライバは どの API を満たしているかを表すバージョン番号を 持つことになる。

正確に言うと、バージョン番号はドライバがエクスポートする 大域変数で、ロード時にデバイスファイルシステムにチェックされる。 Drivers.h には次のような宣言がある:

#define B_CUR_DRIVER_API_VERSION 2
extern _EXPORT int32 api_version;

ドライバのコードには、 次のような定義を追加しなければならない:

#include <Drivers.h>
...
int32 api_version = B_CUR_DRIVER_API_VERSION.

ドライババージョン2が新しい(R4)API を表す。バージョン1は R3 API だ。もしドライバ API が変更されたら、バージョン番号は 3 になるだろう。新しく作られたドライバは新しい API に従い、 API バージョン番号を 3 と宣言することになる。 古いドライバのバイナリは古いバージョン(1 か 2)を 宣言しているので、デバイスファイルシステムが新しい API(3)に 変換することになるだろう。 これで惹き起こされるのは、ロード時の些細な オーバーヘッドだけだ。

しかし、ちょっと待った。 R4 以前の、どのドライバ API を満たしているか宣言していない ドライバについてはどうなのか?

えぇと、 devfs はバージョン番号なしのドライバが従っているのは 最初のバージョンの API だとして扱う ― 現在 Be Book に 書かれているものだ。

device_hooks 構造体の新しいエントリ

わかってる、みんな R4 ドライバ API でなにが新しくなったのか 知りたくてたまらないんだろう... 本稿読者のため特別に公開しよう!

R4 では scatter-gather と(本物の)select が導入されるので、 ドライバがそれらの新しい呼び出しを取り扱えるように 新しいエントリが device_hooks 構造体に少し追加されるのだ。

Scatter-gather

Trey が http://www.be.com/aboutbe/benewsletter/volume_II/Issue35.html で別にお伝えしたように、2つの新しいシステムコールが追加された。 UNIX プログラマたちには良く知られているものだ:

struct iovec { 
    void *iov_base; 
size_t iov_len;
};
typedef struct iovec iovec; extern ssize_t readv_pos( int fd, off_t pos, const iovec *vec, size_t count);
extern ssize_t writev_pos( int fd, off_t pos, const iovec *vec, size_t count);

これらのシステムコールを使うと、1つのファイルまたはデバイスと 複数のバッファの間で読み書きができる。 呼び出しによって、fd が示すデバイスに対して、開始位置 pos から、 配列 vec が指定する count 個のバッファを使って、 入出力が開始される。

これは同じファイル記述子に対して、 単純な読み出しや書き込みを何回か行うのと同じだと 思うかも知れない ― そして、意味論の立場からはその通りだ。 しかし、性能を見ると違うのだ!

DMA を使うほとんどのデバイスには "scatter-gather" の機能がある。 その意味はメモリ上に散らばったバッファをいっぺんに 扱うように DMA をプログラムできるということだ。 つまり、一つのバッファに N 回の I/O をするように プログラムする代わりに、 別々のバッファを指すポインタの配列を使って、 たったひとつの I/O をすれば良いのだ。 それはより高い帯域を意味する。

より低水準で見ると、2つのエントリが device_hooks 構造体に 追加されている:

typedef status_t ( *device_readv_hook ) 
    ( void *cookie, off_t position, const iovec *vec, 
      size_t count, size_t *numBytes ); 
typedef status_t ( *device_writev_hook )
( void *cookie, off_t position, const iovec *vec, size_t count, size_t *numBytes ); typedef struct { ... /* scatter-gather read from the device */ device_readv_hook readv; /* scatter-gather write to the device */ device_writev_hook writev; } device_hooks;

文法が単独の読み出しや書き込みのフックと よく似ていることに注意:

typedef status_t (*device_read_hook) 
                 (void *cookie, off_t position, void *data, 
                 size_t *numBytes); 
typedef status_t (*device_write_hook) (void *cookie, off_t position, const void *data, size_t *numBytes );

異なっているのはバッファの記述だけだ。

scatter-gather を使う利点があるデバイスはこれらのフックを 実装すべきだ。 それ以外のデバイスは単に NULL と宣言すればいい。 readv() や writev() 呼び出しが scatter-gather を扱わない デバイスに対して行われた場合、その入出力は 別々のバッファを使った、細かい入出力に分解される。 もちろん、R3 ドライバは scatter-gather を知らないので、 同様に扱われる。

Select

こちらもニュースどおりだ。先週の記事で Trey が select() の 登場をお伝えした。 これがもう一つの UNIX プログラマお馴染みのシステムコールしだ:

extern int select(
            int nbits, 
            struct fd_set *rbits, 
            struct fd_set *wbits, 
            struct fd_set *ebits, 
            struct timeval *timeout);

rbits、wbits、ebits はビットのベクトルだ。それぞれのビットが 1つのファイル記述子の特定のイベントを監視することを表している:

  • rbits: 入力が得られるようになるのを待つ (読み出すとブロックすることなく即座に何かを返す)

  • wbits: 出力が出て行くまで待つ (1バイトの書き込みはブロックしない)

  • ebits: 例外を待つ。

select() は少なくとも1つのイベントが発生した時か、 タイムアウトした時に帰る。 終了する時、select() は (ビットベクトルを変更することで) どのファイル記述子がイベントを処理できるかを返す。

select() は一つのスレッドが複数の データストリームを扱えるのでとても便利だ。 現在のところ、他の選択肢は、制御したいファイル記述子全てに 1つずつスレッドを生成することだ。 これは状況によっては、特にたくさんのストリームを扱う時は、 やりすぎだろう。

select() はドライバ API レベルでは 二つの呼び出しに分解される: 一つはドライバに指定されたファイル記述子の監視を始めさせるフック、 もう一つは監視をやめさせるフックだ。

device_hooks 構造体に追加した二つのフックをここに示す:

struct selectsync; 

typedef struct selectsync selectsync; 


typedef status_t (*device_select_hook) 
           (void *cookie, uint8 event, uint32 ref, selectsync *sync);


typedef status_t (*device_deselect_hook) 
           (void *cookie, uint8 event, selectsync *sync);


#define B_SELECT_READ 1 

#define B_SELECT_WRITE 2 
#define B_SELECT_EXCEPTION 3 


typedef struct { 
    ... 
    device_select_hook select; /* start select */ 
    device_deselect_hook deselect; /* stop select */ 
} device_hooks;

cookie は監視するファイル記述子を示す。 event からそのファイル記述子に対して どの種類のイベントを待っているかがわかる。 deselect フックが呼ばれる前にイベントが発生した場合、 ドライバは以下の呼び出しをしなければならない:

extern void notify_select_event(selectsync *sync, uint32 ref);

sync と ref には select フックで渡されたものを指定する。 典型的には、割り込みが発生した時、 入力バッファが一杯になった場合、出力バッファが空いた場合に これを呼ぶことになる。

他に notify_select_evnet() が呼ばれそうなのは、 select フックで条件がすでに整っていた場合だ。

deselect フックは、ファイル記述子をもう監視すべきでないことを 示すために呼ばれる。 これは監視しているファイル記述子で一つ以上のイベントが発生したか、 タイムアウトした結果だ。 deselect フックが呼び出された後で notify_select_event() を呼ぶことは 重大な誤りなので、してはならない。

select() を実装していないドライバはフックを NULL と 宣言しなければならない。select() は、そういうドライバで呼ばれた場合、 エラーを返すことになる。

Volume II, Issue 36; September 9, 1998
Changes in the BeOS Driver API
By Cyril Meurilloncyril@be.com

ライブラリとカーネル空間

カーネル内部では...

  • 絶対にアドオンを呼ばない。

  • 絶対に実行中に共有オブジェクトにリンクしない。

  • 可能な限り静的ライブラリにリンクするのを避ける。

  • カーネル中の共有ライブラリに注意。


一般にカーネルコンポーネントは、および特定のドライバは、 カーネルに対してリンクされる。 BeOS の一部である共有ライブラリに対してではない。

ドライバで C ランタイムのサポートが必要になったら? 問題ない。ただし、関数を実装しエクスポートしているのが カーネルであり、共有ライブラリでないことを意識すること。 関数がどのように実装されているのか注意を払わないと、 これが問題になり得る。

malloc() 関数について考えてみよう。 ユーザモードで malloc() が呼ばれた時は、 その関数は共有ライブラリによって実装され、 割り当てられたメモリはページロックされない。 しかし、ドライバの中で malloc() が呼ばれた時は、 その関数はカーネルによって実装され、 確保されたメモリはページロックされる。 もう一つの例は acquire_sem() だ。ユーザモードから呼ばれた時は、 acquire_sem() は割り込み可能だが、 カーネルモードで呼ばれた時は割り込み不可能だ。 このお話の教訓は、 ライブラリとカーネル中の相当品の間の実装の違いに 注意を払うということだ。

理由は明らかだが、カーネルはすべての共有ライブラリの 手続を実装しエクスポートしてはいない。 もしドライバが共有ライブラリとリンクする必要があったらどうする? ソースコードを直接ドライバの中にコピーすればいい (もし手に入るなら)。 それがだめなら、動的にではなく 静的にリンクすることを考えてもいい。

しかし、このためには静的ライブラリが必要だ。 静的リンクは共有(つまり動的)ライブラリでは使えず、 BeOS に付いているライブラリは動的ライブラリだということに 注意すること。 静的ライブラリには ".a" 拡張子がついていて、 他方、動的(共有)ライブラリには ".so" 拡張子が 付いている。

DEVELOPERS' WORKSHOP:
Floating Point and Shared Libraries - Life's Different Down Under
By Steven Olson - solson@be.com

トリッキーなことをしたい?

警告:
共有ライブラリ関数にはリンクするな...

ドライバファイルの場所


ドライバディレクトリの新しい構成
R3 では/boot/beos/system/add-ons/kernel/drivers//boot/home/config/add-ons/kernel/drivers/ にドライバが置かれていた。

この水平な配置はうまく働いた。

しかしまずい点もあって、 ドライバがシステムに追加されるに従ってうまく拡張しなかった。 理由は、オープンするデバイスの名前とそれを提供する ドライバの名前の間に直接の関係が何もなかったからだ。 このために未知のデバイスがオープンされる時に すべてのドライバが検索される可能性があった。

以上がサブディレクトリに分割することになった理由で、 これによって新しいデバイスをオープンする時に デバイスファイルシステムがドライバを見つけやすくなる。

  • ../add-ons/kernel/dev/ はシンボリックリンクとディレクトリを使って devfs 名前空間をそのまま反映している。
  • ../add-ons/kernel/bin/ にはドライバのバイナリがある。

例えば、シリアルドライバは次のデバイスを公開している:

ports/serial1
ports/serial2

実体は ../add-ons/kernel/bin/ に "serial" という名前で置かれていて、 次のシンボリックリンクが設定されている:

../add-ons/kernel/drivers/dev/ports/serial -> ../../bin/serial

もしドライバ "fred" がデバイス ports/XYZ を公開したいなら、 このシンボリックリンクを設定しなければならない:

../add-ons/kernel/drivers/dev/ports/fred -> ../../bin/fred

もしドライバが複数のディレクトリでデバイスを公開するなら、 それぞれのディレクトリでシンボリックリンクを 設定しなければならない。

例えば、ドライバ "foo" を 以下のように公開するとしよう:

fred/bar/machin
greg/bidule

この場合、次のシンボリックリンクも一緒に 準備しなければならない:

../add-ons/kernel/drivers/dev/fred/bar/foo -> ../../../bin/foo
../add-ons/kernel/drivers/dev/greg/foo -> ../../bin/foo

この新しい構成によってデバイス名の解決がかなり高速化される。 我々がデバイス "/dev/fred/bar/machin" を提供するドライバを見つけようとしていると想像してみよう。

R3 では、システムが知っているドライバ全てに、 一度に一つずつ、正しものを見つけるまで 聞いて回らなければならなかった。 R4 では ../add-ons/kernel/drivers/dev/fred/bar/ にあるリンクが指しているドライバにだけ聞けばよい。

Volume II, Issue 36; September 9, 1998
Changes in the BeOS Driver API
By Cyril Meurilloncyril@be.com

デバイスドライバの互換性

「もしあなたが BeOS のデバイスドライバを以前書いたことがあるか 今書いているなら、将来の x86 ハードウェア用 BeOS でも 動くようにするにはどうすればいいか、知りたいと思うだろう。 異なったハードウェアプラットフォームでドライバを動かすには、 プロセッサ特有の機能には一切依存してはならない。 依存を作り込んでしまうのを避け易くするため、 デバイスドライバを書くための四つの一般的ルールを公式化した:

  • アセンブリ言語を使わないこと。 使う場合は他のプロセッサのために代替となるものを 提供すること。

  • 外部デバイスにアクセスするにはエンディアン非依存な コードを使うこと。

  • タイミングやディレイには必ずシステム関数を使うこと。

  • ISA 入出力にアクセスするにはマクロを使うこと。


Be Newsletter, Issue 102, December 3, 1997
A Remembrance of Things Past:
Processor-Independent Device Drivers
By Arve Hjønnevåg

ここにはもっと書かれているが、 不幸にもバージョン4への変更に伴って古い内容になってしまった。

うーむ、もっとしなきゃいけないことがあるな...間違いなく。


The Communal Be Documentation Site
1999 - bedriven.miffy.org
翻訳:鈴木克宗