bus_dma API概説


このテキストは FreeBSD Press No.12 特集「BSDで動かそう 後編」において「NetBSDにおけるデバイスドライバの読み方・書き方」のタイトルで掲載された記事の後半部分の bus dma API解説部分の抜粋です。記事の前半部分については公開用に整理するのがめんどくさいという理由で長らく放置していましたが、ようやくhtml化して別ページに置いてあります。

Webでの公開にあたり投げやりなhtml化を含め一部の表記は見直していますが、基本的にAPI解説としては当時のソースのままであり、最新のNetBSDバージョンにおける変更には追従していません。

本テキストの著作権は筒井泉が有しています。obsoleteな不正確な情報が拡散するのもあまりよろしくないので、転載は控えて下さい。


DMA転送処理とデバイスドライバ

OSがI/O操作においてDMAを使用する場合には、OSに対して要求される操作はキャッシュの種類やDMAコントローラ等、CPUやハードウェアの実装によって大きく異なる。MIのデバイスドライバにおいてDMA操作を行う場合には、どのようなハードウェアのマシンであってもOSに要求される操作を適切に実行できるようにしておく必要がある。その目的のために用意されたのが bus_dma APIである[注27]

デバイスに対するアクセスにおいて使用される bus_space API [注28] では、各 bus_space関数がそれぞれの機種依存の操作と1対1に対応していたが、DMA転送時の操作は転送の準備から完了までのシーケンス全体がアーキテクチャにより異なるため、bus_dma APIの構造はbus_space APIより複雑なものになっている。


[注27] bus_dma についてはmanのほか、Jason Thorpe氏によって書かれた bus_dma paper (NetBSDソースツリーの src/share/doc/papers/bus_dma/ 以下に置かれている)に詳しく記述されている。また、i386のDMA一般に関してはFreeBSDのハンドブック http://www.freebsd.org/doc/en_US.ISO8859-1/books/developers-handbook/dma.htmlにも一部説明があるようだ。

[注28] このもう一つの代表的なAPIである bus_space に関しては別項にまとめているのでそちらを参照して欲しい。


OSに要求されるDMA転送時の操作

DMA転送時に必要な操作は、基本的にはDMAを行うデバイスに対して転送を行うアドレスと長さを指示してやるだけである。しかし今日のOSや各種ハードウェアは性能追求のために様々な機能拡張が行われているため、OSやハードウェア動作においてイレギュラーとも言えるDMA操作に対しては各種の処理が必要になる。

(1)仮想アドレスとバスマスタ側アドレスの変換

BSDに限らず今日のほとんどのOSは仮想記憶システムを採用している[注29]。仮想記憶システムでは物理メモリはページ単位[注30]で管理されており、実際のカーネルやユーザープロセスが使用している仮想アドレス内のメモリが実際の物理メモリのどの領域が使われるかはMMUによって管理されている。

しかし、一般にバスマスタデバイスがDMA転送を行う場合には、CPU側の各プロセスにおける仮想アドレスと実際の物理アドレスの対応についてバスマスタデバイス側からは関与することはできない。したがって、DMA転送を行うバスマスタ側デバイスに対しては、CPU側の仮想アドレスとは異なる、デバイス側が使用する独自のアドレス値を渡してやる必要がある。たとえばi386(PC/AT互換機)アーキテクチャでは、バスマスタデバイスはメインメモリへのアクセスの際には図1のように物理メモリアドレスを使用する。

■図1 バスマスタデバイスのメインメモリへのアクセス
--------------------------------------------------------------------------------

+-----+          +-----+          +--------+         +--------+
|     |  仮想    |     |  物理    |        |  物理   |        |
|     | アドレス |     | アドレス |        | アドレス|  バス  |
| CPU |--------->| MMU |--------->|        |<--------| マスタ |
|     |          |     |          |        |         |デバイス|
|     |          |     |          |        |         |        |
+-----+          +-----+          | メモリ |         +--------+
   ^                              |        |             ^
   |                              |        |             |
   |             データ           |        |   データ    |
   +----------------------------->|        |<------------+
                                  |        |
                                  +--------+

--------------------------------------------------------------------------------

そのため、デバイスドライバはDMA転送を行う場合には転送を行うメモリの仮想アドレスをバスマスタ側から見えるアドレスに変換してやる必要がある。


[注29] 仮想記憶はアプリケーションが実際に存在する物理メモリよりも多くのメモリ空間が使用できるように考案されたシステムである。この辺りの説明については多くのOSの解説書が出ているのでそちらを参照してもらいたい。

[注30] ページのサイズはハードウェアとその実装に依存する。BSD系OSでは一般に4kバイトや8kバイトといった値が用いられることが多い。


(2)スキャッタ・ギャザ処理

仮想アドレスと物理アドレスの相関は一般には任意である[注31]。したがって、カーネル内のデバイスドライバが仮想アドレス空間上の連続したメモリ領域のデータを転送しようとした場合でも、実際にバスマスタ側が転送を行うアドレス空間においてはそれが連続した領域になっているとは限らない。

具体的に書くと、カーネルのデバイスドライバが図2の左側のような0x2000から0x5FFFまでの連続した領域を転送しようとしている場合でも、実際にバスマスタ側がアクセスしなければならない領域は図の右側のように領域、順序、サイズのいずれもがバラバラになっている可能性もある。

図2 カーネル内のデバイスドライバが仮想アドレス空間上の連続した
    メモリ領域のデータを転送しようとした場合
--------------------------------------------------------------------------------

	   CPU側		バスマスタ側
	仮想アドレス		 アドレス
0x0000	+---------+	0x0000	+---------+
	|         |		|         |
0x1000	|         |	0x1000	|         |
	|         |		|         |
0x2000	|---------|	0x2000	|---------|
	|   [A]   |		|   [D]   |
0x3000	|---------|	0x3000	|---------|
	|   [B]   |		|         |
0x4000	|---------|	0x4000	|---------|
	|   [C]   | ⇒		|   [A]   |
0x5000	|---------|	0x5000	|---------|
	|   [D]   |		|         |
0x6000	|---------|	0x6000	|---------|
	|         |		|   [B]   |
0x7000	|         |	0x7000	|---------|
	|         |		|   [C]   |
0x8000	|---------|	0x8000	|---------|
	|         |		|         |
	     :			     :

--------------------------------------------------------------------------------

このため、デバイスドライバは上位から転送要求があった場合に、DMAを行うバスマスタデバイスに対して複数の領域のメモリに対するDMA転送を指示しなければならない場合がある。バスマスタデバイスの種類によっては、このような複数の領域に対するDMA転送を一回の転送で処理できるものもある。このような転送機能をスキャッタ・ギャザ機能と呼ぶ[注32]

バスマスタ側がスキャッタ・ギャザ転送をサポートする場合には、転送が行われるたびにデバイスドライバはすべての分散メモリ領域の転送開始アドレスとそのサイズをすべて求めてバスマスタ側に指示する必要がある。DC-390で使われている Am53c974では、MDLという機能がこれに相当する。Am53c974の MDL機能を使う場合には、DMA転送を行う連続領域に対応する 4kバイト毎のデバイス側アドレス配列と転送開始アドレスのオフセットおよび転送長を用意してやればよい[注33]。バスマスタ側がスキャッタ・ギャザをサポートせず単一の連続領域しか転送を行えない場合は、デバイスドライバが一回の転送を複数回のDMA転送に分割するといった処理が必要になる[注34]


[注31] プロセスの状態によっては仮想アドレス空間の一部が外部デバイスにスワップアウトされている状態も考えられるが、DMA転送の際にはすべてのデータが物理メモリ上にあることがOSの別の部分により保証されていると考えてよいはずである。少なくともデバイスドライバが気にする問題ではない。

[注32] 英語で書くと scatter/gather である。デバイスからメモリへの転送であれば複数の領域に scatter つまり「ばらまく」必要があるし、メモリからデバイスへの転送であれば複数の領域から gather つまり「集める」機能が必要ということである。scatter/gather を日本語に訳して「拡散・収集」「分散・統合」などと記述した文献もあるが、それでは余計わけがわからないのでここではそのまま用語として「スキャッタ・ギャザ」と書く。

[注33] LinuxやFreeBSDのAm53c974用ドライバはなぜかこのMDLを使用していないようである。

[注34] ネットワークデバイスの場合は、送信の際に元となるmbuf上のデータはそもそも仮想アドレス上でも不連続である可能性があるので、デバイスがスキャッタ・ギャザをサポートしていない場合にはデータを一度連続したmbufにコピーする必要がある。


(3)バウンスバッファ処理

ISAバスは24ビットのアドレスバスを持ち、そのアドレス空間は 0〜0xFFFFFF までの16Mバイトである。i386(PC/AT)アーキテクチャではバスマスタデバイスは物理アドレスを使用してメインメモリをアクセスするため、ISA上のバスマスタデバイスは16M以上のメモリ領域にはアクセスができず、DMA転送は行えないことになる。

図3  ISAバスマスタデバイスにおけるメモリアクセス
--------------------------------------------------------------------------------

	  +- 4G	+--------+
	  |	|        |
	  |	|        |
	  |	          
	  |	~~~~~~~~~~~
	  |	~~~~~~~~~~~
	  |	|        |
	  |	|        |
i386CPUの |	|        |
全メモリ  |	|        |
空間	  |	|        |
	  |	|        |
	  |	|        |
	  | 16M	|--------|   -+
	  |	|        |    |
	  |	|        |    |-----i386のISA上のバスマスタデバイスが
	  |	|        |    |     直接アクセスできるメモリアドレス空間
	  +-  0	+--------+   -+

--------------------------------------------------------------------------------

i386のISAバスマスタデバイスに限らず、バスマスタ機能を持つデバイスがCPUの全メモリ空間に対してアクセス可能であるとは限らない[注35]。逆に、ハードウェアのコストを下げるためにバスマスタデバイス側が一部の特殊な共有メモリ上にしかアクセスできないように構成されたデバイスもある[注36]

このようなデバイスに対しては、バスマスタ側が直接アクセスできるメモリ領域内にデバイスドライバがあらかじめ転送用の専用メモリを確保しておき、実際のデータ転送時にバスマスタ側がアクセスできない領域に対する転送要求があった場合には、バスマスタには確保した専用メモリに対する転送を行わせて、その専用メモリと実際に転送要求のあったメモリとの間の転送はデバイスドライバ自身が行う。

つまり、デバイスからメモリへの読み込みの場合はデバイスドライバはバスマスタ側に対しては専用領域への転送を指示し、バスマスタのDMA転送が終了した時点でデバイスドライバ内で専用領域から本来要求のあったメモリへのコピーを行う。メモリからデバイスへの書き込みの場合はデバイスドライバがまず転送要求されたメモリから専用領域へのコピーを行い、その後バスマスタデバイスに対してその専用領域からのDMA転送を指示する。この専用領域のことをバウンスバッファと呼び、転送処理のことをバウンスバッファ処理と呼ぶ。

ここまでに述べたスキャッタ・ギャザ処理やバウンスバッファ処理は主に i386を例を挙げたが、(1)項で述べた i386の構成とは異なり、図4のようにDMA転送を行うデバイス側のバスコントローラにもアドレス変換を行うMMUを持たせたシステムもいくつか存在する。

図4 バスコントローラにMMUを持たせたシステム
--------------------------------------------------------------------------------

   +-----------------------------------------------+
   |                                               |
   |                                               V
+-----+        +-----+        +--------+        +-----+          +--------+
|     | 仮想   |     | 物理   |        | 物理   |     |  仮想    |        |
|     |アドレス|     |アドレス|        |アドレス|     | アドレス |  バス  |
| CPU |------->| MMU |------->|        |<-------| MMU |<---------| マスタ |
|     | (CPU)  |     |        |        |        |     |(デバイス)|デバイス|
|     |        |     |        |        |        |     |          |        |
+-----+        +-----+        | メモリ |        +-----+          +--------+
   ^                          |        |                             ^
   |                          |        |                             |
   |          データ          |        |            データ           |
   +------------------------->|        |<----------------------------+
                              |        |
                              +--------+

--------------------------------------------------------------------------------

上記のようにバスマスタ側もMMUを経由してメモリをアクセスするハードウェアの場合には、転送を行うメモリ領域が物理アドレス上で分散していたとしても、バス側のMMUにおいてそれらの物理メモリがバスマスタ側の仮想アドレス空間上で連続して配置されるように設定することできる。そのためデバイスがスキャッタ・ギャザ機能を持たないデバイスが存在してもDMA転送のオーバーヘッドを少なくすることができ、すべてのデバイスに対して個別にスキャッタ・ギャザの機能を実装する必要がなくなるといった利点がある[注37]。また、上記ISAのようにバスマスタ側デバイスが限られたアドレス幅しか持たない場合でも、MMUにおいて上位のアドレスを補完できるためi386のようなバウンスバッファも不要になるという利点もある。

この時のバスマスタ側のMMUの設定は上位のバス依存階層で行われる。デバイスドライバは上位層で設定されたバスマスタ側の仮想アドレスを受け取り、バスマスタデバイスに対してはその仮想アドレスに対する転送を指示することになる。

このような構成をとっているアーキテクチャとしては arcや sparcなどがある。これらのマシンにはPCIバスも接続されるし、そもそもバスマスタ機能を持つデバイスの中には複数のバスに対応したバリエーションを持っている場合もあるため、MIデバイスドライバではこれらのアーキテクチャにも対応する必要がある。


[注35] 例えば、32ビットのアドレスバスを持つPCI上のバスマスタデバイスであっても、alphaのような64ビットのアドレス空間を持つCPUのすべてのメモリ空間に対して直接データ転送をすることは不可能である。

[注36] ドリームキャストのブロードバンドアダプタがこのような構成を採っている。

[注37] 実際にはバスマスタ側MMUにおいて常に任意の長さの連続したアドレス空間を確保できるという保証をすることは難しいため、スキャッタ・ギャザが全く必要ないような一般的な実装をするためには様々な検討が必要である。


(4)cache-DMA coherency

バスマスタデバイスによるDMA転送はメインメモリに対して行われる。一方で今日のほとんどのCPUはメインメモリを直接アクセスすることはなく図のようにキャッシュを介してメインメモリをアクセスする。

図5 現在のCPUの一般的なメモリアクセス
--------------------------------------------------------------------------------

+-----+          +------+          +--------+          +--------+
|     |  物理    |      |  物理    |        |  物理    |        |
| CPU | アドレス |      | アドレス |        | アドレス |  バス  |
|  &  |--------->|      |--------->|        |<---------| マスタ |
| MMU |          |      |          |        |          |デバイス|
|     |          |データ|          |        |          |        |
+-----+          |キャッ|          | メモリ |          +--------+
   ^             | シュ |          |        |              ^
   |             |      |          |        |              |
   |    データ   |      |  データ  |        |    データ    |
   +------------>|      |<-------->|        |<-------------+
                 |      |          |        |
                 +------+          +--------+

--------------------------------------------------------------------------------

メインメモリをアクセスするのがCPUだけであればキャッシュとメインメモリの内容は一貫性が保たれるが、バスマスタデバイスがDMA転送を行う場合には一般にCPU側のキャッシュの中身は考慮されないため、DMA転送によってCPUとは独立にメインメモリが変更されたり読み出されたりするとキャッシュとメインメモリとの間の整合性が壊れてしまう可能性がある。このときの整合性を保つための手法は機種依存であるが、MIデバイスドライバはいずれのアーキテクチャにおいてもこの整合性確保の手段が講じられるような構成を採る必要がある[注38]。DMA転送の際に実際にキャッシュとメインメモリとの間で不整合が発生するのは以下のケースである[注39]

(a)デバイスからメモリへのデータ転送(リード)時
(a.1)ライトスルーキャッシュの場合

これからDMAによって転送が行われる領域のデータがCPUのキャッシュ上に保持されていると、DMA転送が終了してメインメモリが変更されたのにも関わらずCPUはキャッシュ上の古いデータを参照し続けてしまう。そのためDMA転送が終了後、変更されたメモリにCPUがアクセスにいく前にキャッシュを無効化する必要がある。

(a.2)ライトバックキャッシュの場合

ライトバックキャッシュの場合は、上記のライトスルーキャッシュの場合に加えて、キャッシュ上のデータの書き戻し(ライトバック)を考慮する必要がある。キャッシュ上にこれからDMA転送が行われる領域のデータが含まれていて、かつそのキャッシュの内容が変更されておりまだメインメモリに書き戻されていない場合、そのキャッシュのライトバックを行わずにキャッシュを無効化のみを行う必要がある[注40]

キャッシュの無効化の際にライトバックを行わないという選択ができるアーキテクチャであれば、ライトスルーキャッシュの場合と同様にDMA転送が終了するまでの間にキャッシュの無効化を行えばよい。キャッシュ無効化の時に無条件にライトバックが行われてしまうアーキテクチャの場合には、キャッシュ無効化をDMA転送後に行うと副作用として起こるライトバックによりせっかくDMAによって転送されたデータが壊されてしまうため、DMA転送が開始される前にキャッシュの無効化を行う必要がある。

(b)メモリからデバイスへのデータ転送(ライト)時
(b.1)ライトスルーキャッシュの場合

ライトスルーキャッシュの場合はCPUが書き込みを行ったデータはすべてメインメモリ上に即座に書かれるし、ライト時はDMA転送によってメインメモリ上のデータが書き変わることもないため、キャッシュ整合性保持に関して特に考慮する必要はない。

(b.2)ライトバックキャッシュの場合

ライトバックキャッシュの場合はCPUが変更しようとしたメモリデータがキャッシュ上に残っていてメインメモリ上のデータが更新されていない場合がある。そのため該当のメインメモリに対応するキャッシュ上のデータをDMA転送を行う前に書き戻し(ライトバック)をする必要がある。DMA転送開始後はライトスルーキャッシュの場合と同じくメインメモリが変更されることはないため、キャッシュに対して必要な操作はない。

このようにDMA操作時にはCPUのキャッシュに応じて各種の操作が必要になるが、実際にはこれらのDMA時のキャッシュ操作はハードウェアによって自動的に行うことも可能である。CPUやDMAを行うデバイスはいずれも共通のアドレスバスおよびデータバスに接続されるので、デバイスによるDMA転送実行時にもCPUはDMA転送が行われているアドレスを検出可能でありそれに応じてキャッシュ無効化をしたりバスマスタデバイスに対してメインメモリの代わりにキャッシュからデータを返したりといったことが可能である。これらの動作はバススヌープと呼ばれ多くのCPUで実装されているが、一部のRISC CPUなどでは高速化や消費電流削減のためこれらの機能が省かれている。そのようなアーキテクチャに対してはOSのデバイスドライバが前述のようなキャッシュの面倒見る必要がある。


[注38] この整合性のことを英語ではcoherencyという単語で表記する。

[注39] このあたりのキャッシュ整合性の話はUNIX カーネル内部解析(Curt Schimmel, 前川守 監訳, ソフトバンク, 1996)に詳しい。DMA以外にもMPシステムにおけるキャッシュ整合性にも触れられているので興味のある方はそちらを参照してもらいたい。

[注40] 正確にはキャッシュのラインサイズと転送を行うアドレスによってはより複雑な処理が必要になるがここでは省略する。


bus_dma API概要

以上の記述をまとめると、DMAの際には主に以下の3つの操作が必要になる。

  1. DMAデバイス設定用アドレス変換処理(含むスキャッタ・ギャザ処理)
  2. バウンスバッファ転送処理
  3. キャッシュ整合性確保処理

これらの処理はすべて独立である上、具体的な処理の内容はCPUやバスのアーキテクチャに依存するが、これらの処理をMIデバイスドライバ内で統一的に扱うために作られたのが bus_dma APIである。APIの個別の機能や内部で使用されるデータ構造についてはman pageに記載されているので、ここでは一般的な bus_dma APIの使い方のみを紹介する。

通常のDMA転送を行う場合には4つの APIを使うだけで十分である。デバイスのアタッチ時に bus_dmamap_create() を呼び、DMA転送前に bus_dmamap_load()bus_dmamap_sync() を、DMA転送後に bus_dmamap_sync()bus_dmamap_unload() を呼ぶ、というのがおおまかな流れである[注41]。具体的には表3のようになる。

表3 DMA転送におけるbus_dma APIの呼び出し手順
--------------------------------------------------------------------------------

アタッチ時:	bus_dmamap_create(...)
		DMA転送に用いる構造体などの内部データの割り当てを行う。

		最大DMA転送長、スキャッタ・ギャザ時の分割数、各スキャッタ・
		ギャザ領域の最大サイズなどを引数として指定する。
		成功すればDMA転送時に使用されるbus_dmamap構造体へのポインタを
		返す。


転送開始時:	bus_dmamap_load(...)
		転送データ領域を指定しアドレス変換を行う。

		アタッチ時に与えられたbus_dmamapと、DMA転送を行う領域の
		アドレスと長さを引数として指定してbus_dmamap_load()を呼び出す。
		バウンスバッファ割り当てやバス側MMUの設定もここで行われる。
		成功すればbus_dmamap構造体中のdm_segs[]のds_addrとds_lenの
		メンバーに各スキャッタ・ギャザ領域のバスマスタ側アドレスと
		長さが入って返される。

	
		(読み込み時) bus_dmamap_sync( ,PREREAD)
			     キャッシュ無効化と書き戻しを行う。

		PREREAD引数をつけてbus_dmamap_sync()を呼び出すことで、
		必要に応じてキャッシュの無効化もしくは書き戻しが行われる。
		(前節参照)
		このあと実際のデバイス⇒メモリのデータ転送開始処理を行う。

		(書き込み時) bus_dmamap_sync( ,PREWRITE)
			    キャッシュ書き戻しとバウンスバッファ転送を行う。

		PREWRITE引数をつけてbus_dmamap_sync()を呼び出すことで、
		必要に応じてキャッシュ書き戻しと書き込みデータのバウンス
		バッファへの転送が行われる。
		このあと実際のメモリ⇒デバイスのデータ転送開始処理を行う。


転送終了時:	
		(読み込み時)    bus_dmamap_sync( ,POSTREAD)
			キャッシュ無効化とバウンスバッファ転送を行う。

		POSTREAD引数をつけてbus_dmamap_sync()を呼び出すことで、
		必要に応じてキャッシュ無効化とバウンスバッファから本来の
		読み込み先メモリへの転送が行われる。


		(書き込み時)    bus_dmamap_sync(, POSTWRITE)
			(現状のアーキテクチャに対する実装では処理なし)

		POSTWRITE引数をつけてbus_dmamap_sync()を呼び出す。
		現状ではDMA書き込み完了後に特別な処理を要求するハードウェアの
		実装は存在しないため通常は何も行わないが、
		APIの一貫性保持と将来の拡張のために用意されている。


		bus_dmamap_unload(...)
		転送終了・内部データおよび転送用領域解放

		bus_dmamapを引数としてbus_dmamap_unload()を呼び出し、
		bus_dmamap_load()で割り当てられた各種資源を解放する。

--------------------------------------------------------------------------------

DC-390用ドライバでは、pcscp_dma_setup()bus_dmamap_load() を呼びMDL用のアドレス設定などDMA転送開始前処理を行い、pcscp_dma_go()bus_dmamap_sync() の呼び出しのあと Am53c974のレジスタに対しコマンドを送り転送を開始する。転送終了後はMI側からDMA転送終了時の割り込みの際に呼ばれる pcscp_dma_intr() 内で bus_dmamap_sync()bus_dmamap_unload() を呼ぶようになっている[注42]

バスマスタデバイスによるDMA転送では、スキャッタ・ギャザ領域のアドレスや長さなど多くのパラメータをデバイスに対して設定する必要がある。それらの設定をPIO経由で行うと各転送毎に大きなオーバーヘッドが発生してしまうため、多くのバスマスタデバイスではそれらの転送時に使用されるパラメータについてもDMA経由で渡すようになっている。これらのパラメータ受渡し用のメモリについても上記のような bus_dma APIを用いてデバイス側アドレスの設定を行うが、これらのメモリはすべてのDMA転送時にアクセスされるため、キャッシュ整合性確保やバウンスバッファ転送が必要ないように設定できればそのほうが有利である。bus_dma APIにはこの目的のために bus_dmamem_alloc()bus_dmamem_map() といった関数が用意されている。

bus_dmamem_alloc() では対象となるデバイスが直接DMA転送可能なメモリ領域を確保する[注43]。そうして確保された物理メモリを bus_dmamem_map() を使用してデバイスドライバ内で使用する変数にマップして使用することで、これらのパラメータのDMA転送時にバウンスバッファ転送が行われないことが保証される[注44]

DC-390ドライバでは、sys/dev/pci/pcscp.cpcscp_attach() 中でMDL機能のための配列を bus_dmamem_alloc() で確保し、その直後で bus_dmamem_map() を使ってそのメモリを softc 構造体のポインタである sc_mdladdr メンバにマップしそれを経由してアクセスしている。データ転送時には pcscp_dma_go() 内で bus_dmamap_sync() を呼んでいる。ここでのデータ転送方向は常にメモリ→デバイスの方向であるため bus_dmamap_sync() の引数として BUS_DMASYNC_PREWRITE を与えていることに注意してもらいたい。これとは逆にデバイス側からパラメータをDMA経由で受け取る場合には DMASYNC_PREREADが必要になる[注45]

以上がDMA転送時に bus_dma APIが想定している使用法であるが、実際のデバイスドライバを見ると bus_dmamap_sync() が正しく呼ばれていないものがかなりある。これは先に示したDMA時に必要となる3つの操作のうち、どのアーキテクチャでも必要となるアドレス変換処理が bus_dmamap_load() で行われ、bus_dmamap_sync() では必ずしも必要とされないバウンスバッファ処理とキャッシュ整合性確保しか行われないことに起因する。

特にi386ではバウンスバッファが必要なのは一部のISAバスマスタデバイスのみであるし、キャッシュに関してはすべてハードウェアによって処理されてしまうため[注46]bus_dmamap_sync() を呼ばなくてもたいていは問題なく動いてしまう。

しかし、新規RISC CPUを含む複数アーキテクチャのサポートや、i386アーキテクチャにおいても4Gバイトを超える物理メモリ[注47]のサポートを考えた場合には bus_dmamap_sync()bus_dmamem_alloc() の処理を適切に行う必要があるだろう。


[注41] bus_dmamap_load() の処理の一部は旧来の i386用デバイスドライバでの vtophys() によるアドレス変換に相当する。しかしここで返されるアドレスは物理アドレスであるとは限らないし、アドレスの型も API上物理アドレスや仮想アドレスとは独立した型 (bus_addr_t) である。

[注42] 簡単に書いているが、DMAが動くようになるまでには最初にPIOで動作してから実に半年以上の期間が必要であった。そのほとんどはDMAとSCSI転送に関する知識が足りなかったせいなのではあるが……

[注43] ここで確保されるメモリ領域はバウンスバッファ用として使用されるメモリと同等の領域である。

[注44] bus_dmamem_map() では対象となるメモリ領域に対してキャッシュを無効にする指定も可能であり、その場合は bus_dmamap_sync() 内で行われるキャッシュ無効化の処理のオーバーヘッドを軽減することができる。しかしマップ時のキャッシュ無効化の指定はすべてのアーキテクチャで実装可能というわけではないので、bus_dmamap_sync() の呼び出しそのものを省略することはできない。また、bus_dmamem_alloc() で割り当て可能なメモリ領域には限りがあるためDMA動作上必要なメモリ確保のみに使用し、それ以外のメモリについては通常の malloc() を用い別に確保すべきである。

[注45] pcscp_dma_intr() ではこの sc_mdladdr に対応する bus_dmamap に対して bus_dmamap_sync() を呼んでいない。現状の実装では POSTWRITE 操作はNO-OPであるため問題はないが、厳密に言うとAPIの仕様上は誤りである。デバイス側からパラメータを受け取る場合には転送終了後にも POSTREAD を引数とした bus_dmamap_sync() の呼び出しが必要である。

[注46] これはもちろんキャッシュを持たない素のi386との互換性確保のためである。

[注47] i386 CPUの仮想アドレス空間は32ビット(4Gバイト)であるが、Pentium Pro以降のCPUでは36ビット(64Gバイト)の物理アドレスまで扱えるように拡張されている。


おわりに

気の向くままに筆を進めた(キーボードを叩いた?)ためにやたらと長くなってしまったが、日頃から思っていたデバイスドライバに関する内容はすべて記したつもりである。ソース構成やその読み方あたりまではともかく、各種APIやエンディアンなどMIに関する記述はいささか余計なことを書き過ぎた部分もありカーネル初心者の方にとっては腰の引けてしまう内容もあったかもしれない。特にDMAに関しては既存の(特にNetBSD以外の)ドライバできちんとしているものがあまりにも少ないという日頃の思いもあったため、かなり長くなってしまった。

しかし、今後増えていくであろう各種のアーキテクチャをサポートしていく上でこれらのデバイスドライバのMI/MD独立性確保のためのAPIは重要であるし、最初に述べたがこれらのAPIを理解さえすればそれらのAPIに適合した既存のドライバの移植は非常に容易になる。

NetBSDだけではなくFreeBSDもsparc64やStrongARMといった複数アーキテクチャサポートの動きがあるが、それまで個別のCPUやデバイスそのものの記述にしか目を向けていなかった開発者の方も、あらためてカーネルソースの各種階層を結ぶAPIという観点からソースを見直してみるとまたあらたな発見があるのではないだろうか。


記事の前半部分へ
コラム1: NetBSDのbus_space API詳細
コラム2: エンディアンの話
コラム3: デバイスドライバにありがちなi386依存の罠
コラム4: カーネルデバッグのヒント
コラム5: NetBSDとMI構造

NetBSDのページに戻る
tsutsui@ceres.dti.ne.jp