このテキストはFreeBSD Press No.12 特集「BSDで動かそう 後編」のNetBSD関連記事のコラムとして掲載されたものです。Webでの公開にあたり投げやりなhtml化を含め一部の表記は見直していますが、基本的にAPI解説としては当時のソースのままであり、最新のNetBSDバージョンにおける変更には追従していません。
本テキストの著作権は筒井泉が有しています。obsoleteな不正確な情報が拡散するのもあまりよろしくないので、転載は控えて下さい。
本文でも述べたように、bus_space
API群はMIとMDの分離を行うために用意された抽象化されたデバイスアクセスのための関数である。これらのAPIの使い方については「inb()/outb()
などのかわりに bus_space_read_1()
や bus_space_write_1()
などを使う」などという乱暴な説明だけでもデバイスドライバを書くだけならばあまり困らない。少なくとも既存のドライバを見ればそれほど難しい使い方が要求される部分は多くない。しかし、新しいマシンへの移植をする場合など bus_space
APIを新たなCPUやバスに対して実装しなければならない場合には bus_space
APIの内部的な構造も理解しておく必要がある。ここでは bus_space
APIの実際の実装について簡単に説明する。
bus_space
APIの必要性具体的な例として、シリアルポートのデバイスドライバにおいてその中からUARTのチップである16550のドライバ内からチップ内のアドレス0にあるレジスタを読み込むという操作をすることを考えてみる。
この場合、もしそのチップがPC/AT互換機のものでかつcom0に接続されているものだということがわかっていればI/Oポートの 0x3f8
に対して inb()
の操作を行えばよい。
しかしMIなドライバ上では、対象となるレジスタのアドレスも任意であるし、読み込みを行うための命令もバスやマシンによって異なったものになる。したがって、読み出しを行うAPIはデバイスのどのレジスタに対する操作であるかだけを引数とし、実際の読み出し方法や読み出すアドレスはそのデバイスが初期化された際(あるいはそのドライバがコンパイルされた際)に設定された各マシンやバスおよびデバイス固有の情報に基づいて切り替えられるようになっていなければならない。そのようなデバイスアクセス操作を実現するために用意されているのが bus_space
APIである。
bus_space
APIの考え方はすでにman pageの bus_space(9)
内に詳しく述べられているが、ここで簡単に触れておく。前項で述べたように、マシン、バスおよびデバイスによってアクセスする方法を切り替えるために、 bus_space
APIでは bus space tag
と bus space handle
という引数が用いられる。これらの引数の定義は各機種依存であり <machine/bus.h>
内(ソースの実際のありかは sys/arch/${MACHINE}/include/bus.h
)の中で定義されている。
bus space tag
はアクセスの対象となるデバイスが接続されているマシンのバスの属性を定義する。具体的には、memory mappedされたデバイス空間であるとかI/O空間にあるISAバスであるとかいうような属性に基づいて決められる。bus space tag
の実際の型は機種依存であり、単なるフラグとして使用されるだけの int
型であったり、そのバスに対応する bus_space
関数すべてを定義するための関数へのポインタを含む構造体へのポインタであったりと様々である。デバイスが接続されるバスの種類が一つしかないような機種では bus space tag
が使われていない場合もある。
bus space handle
はアクセスの対象となるデバイスそのものの属性を定義する。属性と言ってもそのデバイスのベースアドレス [脚注1] だけを持つ場合がほとんどである。実際に "grep typedef.\*bus_space_handle_t sys/arch/*/include/bus.h
" として検索してみるとほとんどの機種では bus space handle
は unsinged long
等の整数型である。一部の機種では同じバスに接続されるデバイスであってもデバイス毎に特有の属性を持っていたりアクセス方法が異なっていたりするものもあり、そのような機種では bus space handle
も構造体として定義されている。
bus_space
APIは大きく分けてマップ系の関数とアクセス系の関数の二つに分けられる。マップ系の関数はそのバスのカーネルメモリ空間への配置などのほか、上記 bus space tag
の初期化を行う。デバイスドライバの attach
関数は上記マップ系関数を呼び出し bus space tag
の初期化を行うほか、デバイスに応じて bus space handle
を設定する。
アクセス系の関数には region
系、multi
系、stream
系などのいくつか種類が存在するが、それらのバリエーションはある範囲のアドレス領域をまとめてアクセスするとか一つのアドレスに複数のデータをまとめて書き込むといったデバイスドライバ記述の際の利便性を高めるためのものであり、基本的構造は bus_space_read_N()
と bus_space_write_N()
の二つと同じである。
長々と説明を書いたが、頭の中で考えるよりも具体的な実装を見る方がわかりやすいので実際のソースを見てみよう。
まず一番簡単な例としてnews68kを取り上げてみる。news68kでは他の機種と共通に使用できるMIなバスは存在せず、[脚注2] 機種個別の内部バスもオンボード上のデバイスと拡張スロット上のデバイスのみでいずれも単純なmemory mappedなデバイスであるため、bus_space
のアクセス系の関数は一種類のみである。news68kではSCSIの ncr5380sbc
とEthernetの lance
、シリアルの zs
、そしてRTCの mk48t0x
の5つのMIのデバイスドライバを利用しているが、これらのうち実際に bus_space
を要求するのは mk48t0x
のみ [脚注3] であるため、bus_space
関数も非常に単純なものになっている。
実際に sys/arch/news68k/include/bus.h
を見てみると、bus space tag
はint、bus space tag
は unsigned long
、つまり32ビット整数になっている。
---------------------------------------------------------------------- sys/arch/news68k/include/bus.h: /* * Access methods for bus resources and address space. */ typedef int bus_space_tag_t; typedef u_long bus_space_handle_t; ----------------------------------------------------------------------
bus space tag
は sys/arch/news68k/news68k/bus_space.c
内の bus_space_map()
中で対象デバイスがオンボードのデバイスであるか拡張スロット上のデバイスであるかの判別にのみ使われており、 [脚注4] bus space handle
はデバイスのベースアドレスそのものが入っている。
bus_space_read_N()
の中身は次のように単純にベースアドレスと指定されたオフセットを足したアドレスを読み出しているだけである。ここでは bus space tag
は使われない。
bus_space_write_N()
やその他のアクセス系関数の中身も同様である。
---------------------------------------------------------------------- sys/arch/news68k/include/bus.h: #define bus_space_read_1(t, h, o) \ ((void) t, (*(volatile u_int8_t *)((h) + (o)))) #define bus_space_read_2(t, h, o) \ ((void) t, (*(volatile u_int16_t *)((h) + (o)))) #define bus_space_read_4(t, h, o) \ ((void) t, (*(volatile u_int32_t *)((h) + (o)))) ----------------------------------------------------------------------
次にもう少し込み入った例としてi386を取り上げる。i386の場合、デバイスが接続されるアドレス空間はI/O空間とメモリ空間と2種類あるため、アクセス時には少なくともこれら2種類を判別して切り替える必要がある。
sys/arch/i386/include/bus.h
を見ると bus space tag
と bus space handle
は前述のnews68kと同じく int
と unsigned long
になっているが、bus space tag
はそのバスがI/O空間上にあるのかメモリ空間上にあるのかという情報を持っていることがわかる。bus space handle
の値は sys/arch/i386/i386/mainbus.c
などでデバイスのアタッチメントを呼び出す際に設定される。
---------------------------------------------------------------------- sys/arch/i386/include/bus.h: /* * Values for the i386 bus space tag, not to be used directly by MI code. */ #define I386_BUS_SPACE_IO 0 /* space is i/o space */ #define I386_BUS_SPACE_MEM 1 /* space is mem space */ : /* * Access methods for bus resources and address space. */ typedef int bus_space_tag_t; typedef u_long bus_space_handle_t; ----------------------------------------------------------------------
実際のデバイスアクセス系命令の bus_space_read_N()
の中身は次のようになっている。
---------------------------------------------------------------------- sys/arch/i386/include/bus.h: #define bus_space_read_1(t, h, o) \ ((t) == I386_BUS_SPACE_IO ? (inb((h) + (o))) :\ (*(volatile u_int8_t *)((h) + (o)))) #define bus_space_read_2(t, h, o) \ (__BUS_SPACE_ADDRESS_SANITY((h) + (o), u_int16_t, "bus addr"), \ ((t) == I386_BUS_SPACE_IO ? (inw((h) + (o))) : \ (*(volatile u_int16_t *)((h) + (o))))) #define bus_space_read_4(t, h, o) \ (__BUS_SPACE_ADDRESS_SANITY((h) + (o), u_int32_t, "bus addr"), \ ((t) == I386_BUS_SPACE_IO ? (inl((h) + (o))) : \ (*(volatile u_int32_t *)((h) + (o))))) ---------------------------------------------------------------------- [注]ここで使われている__BUS_SPACE_ADDRESS_SANITY()はデバッグ用マクロである。
つまり bus space tag
を見てデバイスがI/O空間のものであれば inb()
/inw()
/inl()
のI/O系の命令を使い、そうでなければ直接メモリ空間を読み込んでいる。
最後に複雑な実装例としてamigaを取り上げる。amigaでは接続されるバスやデバイスによってアクセス方法を様々に切り替える必要がある [脚注5] ため、bus space tag
の構造も複雑である。
sys/arch/amiga/include/bus.h
では bus space tag
は構造体として定義されており、その中には amiga_bus_space_methods
という実際にそのバスのアクセスの際に使用される bus_space
関数群を指し示す構造体が含まれている。
---------------------------------------------------------------------- sys/arch/amiga/include/bus.h: /* * Access methods for bus resources and address space. */ typedef struct bus_space_tag *bus_space_tag_t; typedef u_long bus_space_handle_t; struct bus_space_tag { bus_addr_t base; const struct amiga_bus_space_methods *absm; }; : struct amiga_bus_space_methods { /* map, unmap, etc */ int (*bsm)(bus_space_tag_t, bus_addr_t, bus_size_t, int, bus_space_handle_t *); int (*bsms)(bus_space_handle_t, bus_size_t, bus_size_t, bus_space_handle_t *); void (*bsu)(bus_space_handle_t, bus_size_t); /* placeholders for currently not implemented alloc and free */ void *bsa; void *bsf; /* 8 bit methods */ bsr(*bsr1, u_int8_t); bsw(*bsw1, u_int8_t); bsrm(*bsrm1, u_int8_t); bswm(*bswm1, u_int8_t); bsrm(*bsrr1, u_int8_t); bswm(*bswr1, u_int8_t); bssr(*bssr1, u_int8_t); bscr(*bscr1, u_int8_t); /* 16bit methods */ bsr(*bsr2, u_int16_t); bsw(*bsw2, u_int16_t); bsr(*bsrs2, u_int16_t); bsw(*bsws2, u_int16_t); bsrm(*bsrm2, u_int16_t); bswm(*bswm2, u_int16_t); bsrm(*bsrms2, u_int16_t); bswm(*bswms2, u_int16_t); bsrm(*bsrr2, u_int16_t); bswm(*bswr2, u_int16_t); bsrm(*bsrrs2, u_int16_t); bswm(*bswrs2, u_int16_t); bssr(*bssr2, u_int16_t); bscr(*bscr2, u_int16_t); /* add 32bit methods here */ }; ---------------------------------------------------------------------- [注] amigaでは32ビットアクセスを使用するデバイスがないためか、 32ビットアクセス関連の関数は今のところ省略されている。
実際のアクセスの際の bus_space_read_N()
や bus_space_write_N()
では、bus space tag
中にある amiga_bus_space_methods
の中の対応する bus_space
関数を呼び出す形になっている。
---------------------------------------------------------------------- : #define dbsdr(n, t, h, o) ((t)->absm->n)((h), (o)) #define dbsdw(n, t, h, o, v) ((t)->absm->n)((h), (o), (v)) : #define bus_space_read_1(t, h, o) dbsdr(bsr1, t, h, o) #define bus_space_write_1(t, h, o, v) dbsdw(bsw1, t, h, o, v) : ----------------------------------------------------------------------
呼び出される側の関数は sys/arch/amiga/amiga/
以下の amiga_bus_space_simple_4.c
、amiga_bus_simple_16.c
、simple_busfuncs.c
や sys/arch/amiga/amiga/dev/gayle_pcmcia.c
などで記述されており、各デバイスの attach
関数内で対応する関数群が割り当てられる。
以上に述べたように、NetBSDにおける bus_space
APIでは、APIの定義自身はあくまでもどの機種にも依存しない形として、その実装のみが機種依存部において個別に定義される形を取っている。これによりMIのデバイスドライバ側では実際のアクセス方法を気にする必要はなくなるが、実際に bus_space
APIのアクセス系の関数が呼ばれる場合には間接参照や条件分岐が入ったり関数呼び出しが行われたりという形態になるため、特定の機種専用に書かれたデバイスドライバと比較すると確実にオーバーヘッドは大きくなる。
PIO転送を多用するドライバではそれらのオーバーヘッドが性能に与える影響が大きい場合もある [脚注6] ので、bus_space
MD部の実装については一通りの実装をするだけではなく性能的な検証も必要になる。しかし、デバイスドライバを一から作成する手間やその後のメンテナンスの手間と比べれば、そのようなオーバーヘッドは取るに足らないものであると言えるだろう。[脚注7]
[脚注1] 具体的には、デバイスが接続されているバス空間のどこにそのデバイスが配置されているかというアドレス情報のこと。
[脚注2] 実際にはnews68kの拡張ボードのいくつかはR3000系のnewsmipsでも共通に使えるものが存在するため、理想的にはそれらのドライバはMI部に存在するべきである。しかし実際にそれらのデバイスは単純なmemory mappedアクセスされるだけで特に複雑な設定を要しないとかそもそも対象となるデバイスがほとんど存在しないとかいった理由で今のところMIにはなっていない。マイナーなデバイスでどこまでMIとMDの分離にこだわるかというのは実際のところ実用性と一貫性とどちらをとるのかというほとんどポートメンテナーの趣味の領域の話になる。
[脚注3] これら lance
や zs
のドライバは bus_space
APIが確立する前にMI化されたため bus_space
を使用するようにはなっていない(ncr5380sbc
ドライバは bus_space
を使用するようにも切り替え可能であるが)。これらのドライバも理想的にはすべて bus_space
化されるべきであるが、これらのドライバを使っているポートのうちまだ bus_space
を持っていないものがあったりデバイスのアクセス方法が特異なものがあったりでこれも今のところ作業は進んでいないようである。
[脚注4] 実をいうと拡張スロット上のデバイスのサポートはかなり手抜きをしている上まだ実装していない部分もあり全然動作確認をしていない。そのあたりに謎なコードがあっても深く追求しないでいただきたい。
[脚注5] これは単に筆者がソースコード中の記述から判断しただけの話であり、筆者は別にamigaに詳しいわけではないので、ハードウェアが実際にそうなっているのかは未確認である。
[脚注6] 特に複数回のアクセスをループで行うmulti系やregion系の関数などでは極力条件分岐を減らすなどの最適化が必要である。
[脚注7] 特にマイナーな(怪しげな?)機種への移植では性能云々よりもまず「動かすこと」そのものが目的だろうから問題にはならないであろう ;-p