アドオンは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_driver
と uninit_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つのファイル記述子の特定のイベントを監視することを表している:
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
「
ドライバディレクトリの新しい構成
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