NetBSDにおけるデバイスドライバの読み方・書き方


このテキストは FreeBSD Press No.12 特集「BSDで動かそう 後編」において「NetBSDにおけるデバイスドライバの読み方・書き方」のタイトルで掲載された記事のうち、すでに公開済みの bus dma API解説 より前のページに掲載されていた部分の原稿です。

内容的に古くなっているのと、もともとが「BSDで動かそう」という特集タイトルで bus_dma 解説を書きたかったがための壮大な前振りとして書いた部分ということもあり、公開用に整理するのがめんどくさいという理由でサボっていましたが、デバドラ内部のコードの解説部分はともかくソースの追いかけ方についてはとっかかりとしてわかりやすいかな、ということで重い腰を上げてhtml化しました。

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

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


はじめに

前号や今号の特集のように「BSDで動かそう」という場合に、まずなによりも先に来るのはOS自身のインストール記事であろう。Windows系OSとは違い、現状ではBSD系OSがプレインストールされているマシンは少ないし、ましてやインストール対象が今回の特集で紹介されているようなPC以外のマシンであれば、まったく想定されていなかったであろう代物がインストールされて、それがさも当然のように動き出すというのはそれだけでもちょっとした驚きである。あまつさえ普段はグラフィックしか表示されないはずの画面にテキストのログインプロンプトが出てきたりすると、それだけでも感動めいたものがある。

しかし、インストールが終わった後にも引き続いて「BSDで動かそう」というテーマを掲げようとすると、これがなかなか難しい。一般にOSのインストールが終わった後に続いて出てくるのはアプリケーションのインストールであるが、これはOSがBSDである必然性はあまりなく、「BSDで動かそう」というテーマとはちょっとはずれている[注1]。やはり「BSDで(怪しげなマシンを)動かそう」といったテーマで主役になるのはカーネル本体、それも全マシンで共通な部分ではなく、それぞれのCPUやデバイスをサポートする部分ではないだろうか[注2]

とは言ったものの、BSDに限らずUNIX系OSのカーネルについての具体的な解説というものはあまり多くない。もちろん悪魔本 [注3] を始めとして、OSそのものについての解説本は多くあるが、いずれもその内容がそのままソースコードに反映できるようなものではないし、対象をデバイスドライバに限ったものはほとんどないように見える[注4]

ただ、少ないとは言え本誌連載のNomads' Commentary on PAOの記事を始めとして、いくつかの雑誌にはFreeBSD/NetBSDのデバイスドライバ開発や新規アーキテクチャへの移植についての解説記事が掲載されている。特にインターフェース増刊号に掲載されたIIJの齊藤さんのNetBSD/sh3移植の記事[注5]は、NetBSDの特徴とソース構成の解説から始まってクロスコンパイル環境や機種依存部に必要なファイルまでよくまとまっているので、怪しいマシンへのカーネル移植を目指している人は必読である[注6]。しかし新規アーキテクチャへの移植に重点を置いているため、この記事においても個々のデバイスドライバについての解説は省略されてしまっている。

一方で個別のドライバ解説の記事に目を向けると、いずれも内部の実装の詳細に紙面が割かれていることが多い。すでに最初の敷居を乗り越えてしまった開発者にとってはそこの部分が一番面白いのだからそうなるのは当然なのだが、カーネルソースいじり初心者にとってはその最初の敷居が高過ぎてなかなか次のステップに進めないというのが実情ではないだろうか。

というわけで前置きが長くなってしまったが、本稿では筆者が最初に書いたデバイスドライバである、Tekram PCI SCSIのDC-390用ドライバを作成した時の経験を元に、ハードウェア依存部のコードのうち、特にデバイスドライバを中心としたカーネルのソースの読み方、そして書き方の手がかりとなるポイントを紹介し、さらにNetBSDが移植性を確保する為に用意している各種のカーネル内部インターフェースについても解説していきたい。


[注1] もちろんパフォーマンスの高さに満足する、ということはあるかもしれないが。

[注2] どのマシンでも同じソース記述で動く機能であれば、わざわざ妙なマシン上で開発する人は少ないだろう。

[注3] もちろん The design and implementation of the 4.[34]BSD Operating systemのこと。4.3BSDの方(黒本)は日本語版があるが訳の出来は今一つなので、多少なりとも英語が読めるのなら4.4BSD(赤本)の方がおすすめである。

[注4] もっとも、これらの解説は今日のFreeBSDやNetBSDのように「オープンソースですべてをユーザーが自由にいじることができる」ということは想定していなかったであろうから、それはある意味当然なのかもしれない。

[注5] 齊藤正伸; NetBSDの概要とハードウェアへの移植方法; インターフェース増刊 TECH-I Vol.5, 2000, CQ出版

[注6] 新規アーキテクチャ移植時の機種依存部の記述においてはNetBSDを最初にSH3に移植されたブレインズの堀内さんの解説記事も役立つであろう。
堀内岳人; NetBSD移植の実際; BSD magazine No.2,3,4 1998-1999, アスキー
また、つい最近発売されたインターフェース2002年8月号特集である「組み込み分野へのBSDの適用」の中にも齊藤さんを始めとした方々のカーネル開発の記事があり、こちらも必読である。


おことわり

FreeBSD Pressという本誌のタイトルからすると申し訳ないのだが、本記事が対象としているOSはNetBSDである[注7]。FreeBSDとNetBSDは、元が同じ386BSDをベースにしているとはいえ、特にカーネル内部のAPIについては今日では両者は大きく異なるものになっていると思う。筆者はNetBSDでしかカーネル開発は行っていないので、FreeBSDの現状についてはあまり把握していないが、デバイスドライバ記述の際に重要な部分を占めるデバイスコンフィギュレーションに関しては、NetBSDはかなり以前からnewconfigに移行し、かつさらなる進化を遂げているため、それらの記述についてはnewconfigを選択しなかったFreeBSDとは大きく異なっているであろう。また、 bus_spacebus_dma といったAPIはNetBSD由来のものがFreeBSDにも導入されているが、それらについても各OSで独自の拡張がされているため、その辺の相違については各自で解釈して読みかえていただきたい[注8]

また、えらそうなタイトルで記事を書いているものの、筆者は本業においても趣味においてもプログラマという立場にはない。正直な話、まともなCのアプリケーションなどほとんど書いたことがないし、アルゴリズム的な純粋なソフトウェア設計にはあまり興味がないので、その辺りは大抵ほかのカーネル内部のソースの真似するだけで済ませていたりする[注9]。そもそも基本になるCの文法や使い方に関しても、ほとんどNetBSDのカーネルソースでしか学んでない。したがって本記事の内容はあくまでも筆者が経験的に学んだことがほとんどであり、教科書的解釈からいくと間違った内容も含まれていると思われるがその辺は大目に見て欲しい。

一方、デバイスドライバ作成の前提となる仮想記憶機構などのOSの基礎や、CPU/メモリ/キャッシュなどのハードウェアアーキテクチャの基礎から説明を始めると、どれだけページがあっても足りないので、これらについては一通り知識があることを前提としている。これらについては各種の文献や実際のソースを参照して欲しい。


[注7] 本文中で参照しているNetBSDのソースは、NetBSD-1.6のブランチが切られる直前の-currentである、NetBSD-1.5ZCのものである。

[注8] NetBSDとFreeBSDとの比較を論じることができるほどFreeBSDの内部については理解していないが、一般的に移植性を重視しているNetBSDのほうが「デバイスを動かす」という作業対象としては向いているのは異論のないところであろう。

[注9] それだけでもある程度動かせてしまうのがNetBSDの偉大なところだと思う。


DC-390の概要とそのきっかけ

説明に入る前に、本稿で例として取り上げるDC-390と、それを最初のデバイスドライバ作成の対象として選んだ理由について少し説明しておく。

筆者がデバイスドライバを書いてみたいと考え始めたのは1997年から1998年頃の話である。当時の筆者は、メインマシンのi386でNetBSD-currentを追いかけていたほか、IRIの椿井さんによって移植が行われていたNetBSD/newsmipsのテストをしたり、同じく椿井さんによって始められていたNetBSD/macppcを何とか自分のMac互換機でブートさせられないかと四苦八苦[注10]したりという生活を送っていた。そうしてカーネルの動作テストとバグ追跡を繰り返しているうちにソースの構造がおぼろげながらもわかってくると、今度は他人様のコードを試すだけではなく、自分自身の書いたコードで何かを動かしてみたい、という思いが強く湧き上がってきたのである。そこで当時の自分のレベルでもなんとかなるような難易度のターゲットはなにかないか、ということで考えたのがTekram製PCI SCSIのDC-390のデバイスドライバ作成であった。

TekramのDC-390はPCIが普及し始めたわりと初期に発売されたバスマスタDMAのPCI SCSIカードで、SCSIのチップとしてAMDのAm53c974を使用していた[注11]。同時期に存在したAHA-2940やNCR53C810といった他のPCI SCSIカードと比較すると値段も手頃であったため、それなりにユーザーも多かったのではないかと思う。

しかし、当時筆者はDC-390を所有していたわけではなく、DC-390をターゲットとして選んだのは次のような理由からである。

つまり、SCSI制御部分はすでにあるソースを流用、PCIデバイス特有の部分はほかのPCIデバイスのソースから流用、各種バス依存部とNCR53C9xドライバの相関についてはISAやほかの機種のソースを参照、という安直な方法でいけるのではないかと踏んだのである。これならば既存のソースを読むばかりでロクにコードを書いたことのない自分でもなんとか動かすことができるのではないか……というわけでいっちょやってみようとDC-390の購入に踏み切ったのであった[注12]

ちなみに当時のFreeBSDにはTekram自身によって作成されたDC-390用のドライバがすでに存在していた[注13]。しかしこれらのコードは元々がLinux用ドライバがベースなのか、コードのスタイルやドライバ自身の構造がNetBSDのコードに慣れた自分にとっては読み進めるのがとてつもなく苦痛だった上、ぱっと目を通しただけでも outb() とか OutB() とか DC390_read() とか怪しげなI/Oポートアクセスらしい命令が散りばめられているのが目につき、これをNetBSDに移植するとなるととてつもなく苦労しそうだということを感じていた[注14]

しかし逆にそのFreeBSD用のドライバの存在は、自分でDC-390用ドライバをNetBSD流に作成しようという思いを後押ししてくれるものであった。というのも、DC-390はそれなりにメジャーなボードであるからサポートの需要はそれなりにあるだろうという皮算用する一方で、自分のような素人がドライバを動かせるようになるまでの間に別の有識者がサクッとドライバを書いてしまうのではないかという危惧も抱いていたのだが、FreeBSD用のドライバが中途半端に(失礼!)存在しているがために、FreeBSD用ドライバの移植をする人もいなければドライバを一から書き直そうという気合いのある人も出てこないのではないか、というある種ふざけた期待があったのである。


[注10] NetBSD/macppcが正式にマージされる半年ほど前の話で、当時はNetBSD/powermacと呼ばれていた。まだPowerMac9500などごく一部の機種でしかサポートされておらず、実際にカーネルを動作させていた人はわずかであった。

[注11] 原稿執筆時点でデータシートは http://www.amd.com/us-en/Networking/TechnicalResources/0,,50_2334_2496_2520,00.html から入手できる。(html化時点でリンク切れ)

[注12] いわゆる本末転倒とやつであるが、デバイスドライバを書くこと自体が目的なのだから構わないであろう。

[注13] 当時のコード(FreeBSD-2.2.xのころ?)は
http://www.FreeBSD.org/cgi/cvsweb.cgi/src/sys/pci/Attic/tek390.c
http://www.FreeBSD.org/cgi/cvsweb.cgi/src/sys/pci/Attic/scsiiom.c
などで参照できる。

[注14] 現在のFreeBSDのAm53c974のコードは
http://www.FreeBSD.org/cgi/cvsweb.cgi/src/sys/pci/amd.c
である。現在のこのドライバはFreeBSDのSCSI開発の中心であるJustin Gibbs氏によってかなり手が入れられており、以前のコード比べると整備されている。が、NetBSDのように他のすべてのNCR53c9x系のドライバを統合しようというような動きはないようである。


カーネルソースの追いかけ方

話がやや横道にそれてしまったが、ここからは本題であるカーネルソースの話に戻る。

どこから見ればよいのか

原稿執筆時点でのNetBSDのソースツリーを見てみると、カーネルのソースが含まれる /usr/src/sys 以下の容量は du(1) の表示で約110Mバイトあり、ファイルの数は13,000以上、ディレクトリの数だけでも1,000近くある。筆者は学生時代、386BSDをいじり始めた頃に先輩方から「manになければソースを読むべし」という格言を教えられていたが、さすがにシステムがここまで大きくなると、ソースを読もうにもどこから手を付けたらよいのかすら見当がつかないかもしれない。

しかし実際にはNetBSDのソースツリーはかなり整理されており、慣れればすぐに目的のファイルを見つけられるようになる[注15]。ここではNetBSDのソースツリーの構成と各ディレクトリに存在するソースの役割を説明し、ドライバを書くにあたりそれらの既存のファイルをどう流用すればよいかということを述べていきたい。

MIとMD

NetBSDのソースツリー構成の特徴としては「マシン非依存部(Machine Independent; MI部)と、マシン依存部(Machine Dependent; MD部)との分離」ということが挙げられる[注16]。具体的には、たとえばある複数のマシンにおいてそれらのCPUのアーキテクチャがまったく違っていたとしても、それらの双方にPCIやISAのバスがついていたとすれば、バスの先に取り付けられるデバイスはすべて共通であり、異なる部分は各マシンのCPU周辺とPCIないしISAバスとの接続部分だけである。また、前項でも少し触れたがNCR53C9x系のSCSIコントローラを使用するマシンやデバイスは、alphaやpmaxのTURBOchannel上のものやsparcのSbus上、macppcのオンボードSCSIやここでとりあげるPCIのAm53c974など多数存在するが、これらのデバイスはSCSI制御に関する部分はすべて共通であり、異なるのは各デバイスが接続されるバスだけである。

このように世の中には多数のマシンと多数のバス、多数のデバイスの組み合わせが存在するが、その組み合わせの数だけドライバを用意するのではなく、マシン⇔バス間、バス⇔デバイス間およびデバイス単体というように、それぞれの階層を分離して記述してやろう、というのがNetBSDにおけるMI、MD分離の考え方である。このような構成であれば、あるPCIのデバイスドライバを書けばそれはすべてのPCI付きのマシンで利用可能であるし、あるデバイスのドライバがすでに存在していれば別のバス上の同じデバイスのサポートはそのバスに対するインターフェースを記述するだけで済む。

以上のような構成を念頭においてソースツリーを眺めると、少なくともデバイスドライバ関連のファイルについては下表のとおりでどこに何があるかは一目瞭然である。

-------------------------------------------------------------------------------
個別のデバイスの制御のためのソース		sys/dev/ic/

各機種共通のバス固有の機能、			sys/dev/isa/
バス⇔デバイス間のインターフェース、		sys/dev/cardbus/
特定のバスのみに存在するデバイスのソース	sys/dev/isapnp/
						sys/dev/mca/
						sys/dev/pcmcia/
						などなど

各CPUアーキテクチャ依存部			sys/arch/*/ 以下
						各ディレクトリ

各CPUアーキテクチャ⇔バス間のインターフェース	sys/arch/i386/pci/
						sys/arch/i386/isa/
						sys/arch/alpha/pci/
						sys/arch/alpha/isa/
						などなど
-------------------------------------------------------------------------------

[注15] NetBSDとFreeBSDのソースのディレクトリ構成を比較すると、特に各アーキテクチャやデバイス関係についてはかなり異なった物になっている。これはお互いのポリシーを反映したものであろう。

[注16] The NetBSD Goalsのページにある http://www.NetBSD.org/Goals/portability.html も参照してもらいたい。


config(8)から追いかけてみる

どのディレクトリにどんなファイルがあるかがわかったところで、次に具体的にそれぞれのソースがどのデバイスあるいは機能に対応するソースであるのかを追う方法について考えてみよう。単純に適当なディレクトリに移動してひたすら grep(1) する、という方法でもそれなりに追うことはできるが、ここでは基本に立ちかえってカーネルのconfigファイルからたどっていく方法を考えてみる。

ユーザーが自分でカーネルを作成する場合には、 sys/arch/i386/conf といった各マシン別のconfigディレクトリ上で GENERIC といった標準configファイルをコピーし、各種デバイス等をそれぞれにあった構成に変更した上で config(8)コマンドを実行する。そうして "config MYKERNEL" といったコマンドを実行すると sys/arch/i386/compile/MYKERNEL/ というディレクトリが作られ、その中にカーネルコンパイル用の Makefile も作られるわけであるが、 Makefile が作られるということは config(8) が処理している内容を追っていけばconfigファイルに記載した内容に対応するソースファイルを追う方法もわかるという寸法である。NetBSDで用いられているnewconfig機構やその関連ファイルの書式の詳細についてはSRAの古田さんによる記事[注17]に詳しいが、ここではもっとゲリラ的に必要な部分だけをかいつまんで取り上げてみる。

config(8) の動作を調べる、となるとまず思いつくのが config(8) 自体のソースを読むことであるが、 config(8) のソースはいわゆるパーサーだとか構文解析だとか筆者のようにプログラマでない人間にとっては最も苦手な部類のソースである。知りたいのは config(8) が何をしているかではなくて、単に config(8) がどのファイルを参照しているかであるので、ここでは ktrace(1) を使って調べることにする[注18]。 深く考えずに sys/arch/i386/conf に移動して "ktrace config GENERIC" を実行したあと "kdump | grep NAMI | less" などとすると config(8) がオープンしようとしたファイルのパスがずらずらと表示される。

--------------------------------------------------------------------------------
[...前略...]

  2753 config   NAMI  "GENERIC"						←[1]
  2753 config   NAMI  "/etc/malloc.conf"
  2753 config   NAMI  "/tmp"
  2753 config   NAMI  "/tmp/config.tmp.02753a"
  2753 config   NAMI  "/tmp/config.tmp.02753a"
  2753 config   NAMI  "../compile/GENERIC"				←[2]
  2753 config   NAMI  "../compile/GENERIC"
  2753 config   NAMI  "../../../.."
  2753 config   NAMI  "../../../../arch/i386/conf/std.i386"		←[3]
  2753 config   NAMI  "/dev/null"
  2753 config   NAMI  "../../../../arch/i386/conf/files.i386"		←[4]
  2753 config   NAMI  "/dev/null"
  2753 config   NAMI  "../../../../conf/files"				←[5]
  2753 config   NAMI  "../../../../altq/files.altq"
  2753 config   NAMI  "../../../../dev/sysmon/files.sysmon"
  2753 config   NAMI  "../../../../dev/mii/files.mii"
  2753 config   NAMI  "../../../../dev/raidframe/files.raidframe"

[..中略..]

  2753 config   NAMI  "../../../../ufs/files.ufs"			←[6]
  2753 config   NAMI  "../../../../dev/scsipi/files.scsipi"		←[7]
  2753 config   NAMI  "../../../../dev/ata/files.ata"
  2753 config   NAMI  "../../../../dev/i2o/files.i2o"
  2753 config   NAMI  "../../../../dev/pci/files.pci"			←[8]
  2753 config   NAMI  "../../../../dev/pci/files.agp"
  2753 config   NAMI  "../../../../dev/isa/files.isa"			←[9]

[..中略..]

  2753 config   NAMI  "../../../../dev/cardbus/files.cardbus"		←[10]
  2753 config   NAMI  "../../../../dev/pcmcia/files.pcmcia"
  2753 config   NAMI  "../../../../dev/usb/files.usb"
  2753 config   NAMI  "../../../../dev/ieee1394/files.ieee1394"
  2753 config   NAMI  "../../../../arch/i386/pnpbios/files.pnpbios"
  2753 config   NAMI  "../../../../dev/acpi/files.acpi"
  2753 config   NAMI  "../../../../dev/acpi/acpica/files.acpica"	←[11]
  2753 config   NAMI  "../../../../arch/i386/conf/GENERIC.local"
  2753 config   NAMI  "config_file.h"
  2753 config   NAMI  "config_file.h"
  2753 config   NAMI  "machine"						←[12]
  2753 config   NAMI  "machine"
  2753 config   NAMI  "i386"
  2753 config   NAMI  "i386"
  2753 config   NAMI  "../../../../arch/i386/conf/Makefile.i386"
  2753 config   NAMI  "../../../../arch/i386/conf/Makefile.i386"
  2753 config   NAMI  "Makefile.tmp"
  2753 config   NAMI  "Makefile.tmp"
  2753 config   NAMI  "Makefile"
  2753 config   NAMI  "Makefile.tmp"
  2753 config   NAMI  "Makefile"

[...後略...]
--------------------------------------------------------------------------------

順に見ていくと、[1]で引数の GENERIC を読み込み、[2]でコンパイルディレクトリを作成(あるいは存在を確認して)いる。その後[3]で std.i386 を読み込んでいるが、これは GENERIC の先頭にある "include arch/i386/conf/std.i386" に対応している。そして[4][5]で arch/i386/conf/files.i386sys/conf/files と2つのファイルを読み込んでいるが、こちらは std.i386 中の "machine i386" の記述に対応した動作である。これ以降の files.* のファイルは[5]から[6]の部分は sys/conf/files から、[7]から[11]の部分は arch/i386/conf/files.i386 の中から include 文で指定されているファイルである。すなわち、どのデバイスがどのファイルを使用しているかを調べるにはこの2つの files および files.i386[注19]から手繰っていけばよいことになる。

これら files.* の中身を見てみると、各デバイスとソースファイル名の対応がなんとなくわかってくる。これらの中からNCR53C9x系のデバイス名として使われている "esp" で検索してみると各ファイルには次のような記述がされている。


sys/dev/isa/files.isa:
--------------------------------------------------
# Qlogic ESP406/FAS408 boards
# device declaration in sys/conf/files
attach	esp at isa with esp_isa: isadma
file	dev/isa/esp_isa.c		esp_isa
--------------------------------------------------

sys/arch/macppc/conf/files.macppc:
--------------------------------------------------
attach esp at obio
file arch/macppc/dev/esp.c		esp
--------------------------------------------------

sys/conf/files:
--------------------------------------------------
# NCR 53x9x or Emulex ESP SCSI Controller
#
define	ncr53c9x
device	esp: scsi, ncr53c9x
file	dev/ic/ncr53c9x.c		ncr53c9x
--------------------------------------------------

attachfiledefinedevice などの構文の正確な書式と意味については前述の古田さんの記事を参照して欲しいが、 files.isa 中の2行を見れば「esp というデバイスが isa に接続されてそのとき dev/isa/esp_isa.c というファイルが必要」というのはなんとなくわかるし、 sys/conf/files 中の3行も「esp デバイスには scsincr53c9x のファイルが必要で、 ncr53c9x には dev/ic/ncr53c9x.c が必要」という具合だというのはわかると思う。ちなみに "scsi" 関連のファイル定義は sys/dev/scsipi/files.scsipi の中にある。

ここまでわかったところで当初の目的であるPCIのAm53c974ドライバを追加する場合を考えると、 files.pci に対してだけデバイスの定義を追加してやればよいことがわかる。Am53c974のデバイス名が現状の "pcscp" となったのにはいくつか経緯がある[注20]のだが、ひとまず現在の files.pci の記述は次のようになっている。

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

# AMD Am53c974 PCscsi-PCI SCSI controllers
device	pcscp: scsi, ncr53c9x
attach	pcscp at pci
file	dev/pci/pcscp.c			pcscp

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

つまり新たに作成が必要なのは ncr53c9x⇔PCI間のインターフェースに相当する部分(アタッチメントとも呼ばれる)の sys/dev/pci/pcscp.c だけであり、あとは相手側の sys/dev/ic/ncr53c9x.c や関連ヘッダファイルに対しAm53c974を認識させるためのコードをいくつか追加させるだけでよいということになる。 ncr53c9x⇔各バス間のインターフェース仕様については、それぞれのバスのソースである sys/dev/isa/esp_isa.csys/arch/mac68k/dev/esp.csys/arch/macppc/dev/esp.c といったファイルを参照すればよいし、PCIのデバイスドライバ一般に必要な操作については sys/dev/pci/isp_pci.csys/dev/pci/bha_pci.c のような既存のSCSIドライバのPCI部のソースを参照すればだいたいの内容は見当がつく。 ncr53c9x⇔scsi間のインターフェースについては今回の対象とは別の階層であるため、変更する必要はまったくない。


[注17] 古田敦; NetBSDにおけるデバイスの自動コンフィギュレーション; BSD magazine No.2; 1998; アスキー

[注18] ktrace(1) ってなに? という説明を始めるとこれまた終わらないので割愛する。manやインターフェース2002年8月号の齊藤さんの記事のなかにある解説記事を参照して欲しい。

[注19] もちろんi386以外のマシンであれば対応するアーキテクチャ名のファイルになる。

[注20] ドライバ作成の作業を始めた当初は素直に esp というデバイス名にしようと考えていたのだが、当時は "device esp" の定義が sys/conf/files の中ではなく、 files.isafiles.macppc などに分散して存在していた。そのため files.pciesp の定義を足してしまうと files.pcifiles.isafiles.macppc と同時に読み込まれるため、 config(8) が名前の重複により受け付けてくれず、やむを得ずデバイス名を "amd" としてドライバを作成した。そしてそのドライバがNetBSDのツリーに取り込まれた際に作業をしたJason Thorpe氏が "amd" という名前では他のデバイス名と紛らわしいという理由でAm53c974の別名である "PCscsi-PCI" から "pcscp" という名前をつけたのである。今思えばAm53c974には一般のNCR53c9x系のチップとは違いPCIに特化したDMA機能も含まれているため、独自の名前を持たせたことについては結果的に問題なかったと考えている。


実際のデバイスドライバ作成の例

対象となるファイルが明らかになったので、次は実際に作成および修正すべきファイルの中身について考えていく。ここでもDC-390のドライバ作成時の例を中心に挙げる。

作成時の考え方

前節でMIとMDが分離された記述をするためにCPU⇔バス間、バス⇔デバイス間そしてデバイス制御部を分ける必要があると書いた。しかし実際にそのようなコードを実装するためにはそれぞれの階層を結びつけるためのインターフェース(API)を規定することが必要になる。具体的にはMI側のドライバが実際のハードウェアにアクセスしたり上位の階層の特定の機能を呼び出したりするような場合である。

真にMIなデバイスドライバを作成しようとすると、各種のAPIを適切に使用して様々なハードウェアに対応できるような記述を行う必要があり、設計にはかなりの労力を必要とする。しかし、逆にいうとMI側のドライバがすでに完成していれば、そのデバイスに対して新規のバスに対するアタッチメントを作成するには、それらのAPIさえ理解しておけばよいことになる。Am53c974の場合でいえば、たとえSCSIやPCIの詳細を知らなくてもそのデバイス自身のアクセス方法と上位のドライバを呼び出す手続きさえわかれば、あとは適当に見よう見まねでもドライバ(正確にはドライバの一部に過ぎないが)が書けてしまう。

ドライバアタッチメント部の構成

とりあえず他のソースの真似をするためにMI ncr53c9xドライバを用いる各種バスのアタッチメントを調べると、いずれのドライバも以下の3つの部分から構成されていることがわかる。

  1. probe関数
  2. attach関数
  3. glue関数群

1.のprobe関数は名前のとおり[注21]そのドライバが対象とするデバイスが実際に存在するのかどうかをテストする関数であり、これらの関数の実装はバス依存である。

ISAではデバイスが存在するかどうかのテスト方法をハードウェアが提供していないため、ドライバが実際にデバイスが存在する可能性のあるアドレスに対してアクセスを行い、その内容に応じてデバイスの存在を判定する[注22]

PCIではすべてのデバイスに対してVendor IDとProduct IDが割り当てられているため、probe関数は上位の関数から渡されるそれらのIDをチェックするだけでよい。デバイスがそのドライバに対応するものであれば1を返し、そうでなければ0を返す。

--------------------------------------------------------------------------------
int
pcscp_match(parent, match, aux)
	struct device *parent;
	struct cfdata *match;
	void *aux;
{
	struct pci_attach_args *pa = aux;
	if (PCI_VENDOR(pa->pa_id) != PCI_VENDOR_AMD)
		return 0;

	switch (PCI_PRODUCT(pa->pa_id)) {
	case PCI_PRODUCT_AMD_PCSCSI_PCI:
#if 0
	case PCI_PRODUCT_AMD_PCNETS_PCI:
#endif
		return 1;
	}
	return 0;
}
--------------------------------------------------------------------------------

2.のattach関数では、デバイスのメモリ空間への割り当て、デバイスの割り込みの設定そしてドライバ内で使われる各種パラメータを初期化する[注23]

メモリ空間割り当てと割り込みの設定の2つはバス依存の設定であり、PCIデバイスの場合はたいてい pci_mapreg_map()pci_conf_read()pci_intr_map()pci_intr_establish() といった関数がだいたいのドライバで使われており、詳細はわからなくてもどういう操作が必要なのかはだいたい見当がつく。3つ目のパラメータ設定は各MD側アタッチメント内のドライバ構造に依存するが、深く考えずこれから真似しようとするドライバと同じような構成にしておけばよいはずである。

3.のglue関数群であるが、"glue" とは「接着剤」というような意味である。つまりMI側のncr53c9x.c側から各MD側アタッチメントによって異なる処理が必要な機能を呼び出す場合に使用する関数群である。具体的には、NCR53c9xチップに対するレジスタアクセス処理[注24]と、DMA転送開始の際の初期設定、およびDMA転送完了時の割り込み処理が主な内容である。なお、これらのDMA処理などがすべてのバスのチップにおいて共通になっているデバイスにおいてはこれらのglue関数が存在しない場合もある。

DMA処理は完全にMD側で処理される内容であり、本来ならほかのアタッチメントの真似をして済ますということのできるものではないが、各アタッチメントを見てみると esp_isa.c のglue関数群ではDMA関連の処理はしておらず、実際にはPIO転送の処理を行なっているのがわかる。これは esp_isa_write_reg() の中でNCR53c9xコアにコマンドを書き込む際にDMAのビットを落としていることからもうかがえる。

--------------------------------------------------------------------------------
void
esp_isa_write_reg(sc, reg, val)
	struct ncr53c9x_softc *sc;
	int reg;
	u_char val;
{
	struct esp_isa_softc *esc = (struct esp_isa_softc *)sc;
	u_char v = val;

	if (reg == NCR_CMD && v == (NCRCMD_TRANS|NCRCMD_DMA)) {
		v = NCRCMD_TRANS;
	}

	ESP_REGS(("[esp_isa_write_reg CRS%c 0x%02x=0x%02x] ",
	    (bus_space_read_1(esc->sc_iot, esc->sc_ioh, NCR_CFG4) &
	    NCRCFG4_CRS1) ? '1' : '0', reg, v));

	bus_space_write_1(esc->sc_iot, esc->sc_ioh, reg, v);
}
--------------------------------------------------------------------------------

PIO転送しか行わないのであれば、アタッチメントがISAでもPCIでも処理は変わらないはずだろうということで、実際に作成したDC-390のアタッチメントのglue関数群では esp_isa.c とほぼ同じ構成とした。

MI側の sys/dev/ic/ncr53c9x.c についてはAm53c974特有のレジスタはいくつか存在するものの、PIO転送のみの場合にはほとんど影響ないと考えられたため、デバイスのアタッチメントが行われる際の画面表示を直すという意味で各NCR53c9xチップの名前の定義である ncr53c9x_variant_name[] と、各チップの識別を行うための ncr53c9xvar.h 中の NCR_VARIANT_* の定義のみを変更した。


[注21] 関数の名前が xxx_probe() ではなく xxx_match() となっている場合もある。

[注22] このような「あてずっぽう」なアクセスを行うと、そのアドレスに想定しているものと違うデバイスがついていたような場合に予想外の動作を引き起こすことがある。このような操作をせざるを得ないISAバスは「ダメな構造のバス」の典型とも言える。

[注23] デバイスのconfig的な定義を厳密に言うといろいろあると思うが、ここではあまり気にしないことにする。

[注24] 本来ならレジスタへのアクセスはこのような関数ではなくbus_space APIで処理させるべきであるが、MI ncr53c9xドライバ作成時にそのベースとなったsparcの esp.c が当時はまだbus_space化されていなかったことなどからこのような構成になっていると思われる。


softc 構造体について[注25]

デバイスドライバ内で静的に割り当てる必要のある変数は、attachの際に動的に割り当てられるsoftc構造体といわれる領域の中に確保する必要がある。これはドライバのソース内に静的に変数を割り当てたのでは同じデバイスが複数取り付けられた場合などに対応できないためである。 esp_isa.c で使われる esp_isa_softc 構造体は esp_isavar.h の中で次のように定義されている。

--------------------------------------------------------------------------------
struct esp_isa_softc {
	struct ncr53c9x_softc	sc_ncr53c9x;	/* glue to MI code */

	int		sc_active;		/* Pseudo-DMA state vars */
	int		sc_tc;
	int		sc_datain;
	size_t		sc_dmasize;
	size_t		sc_dmatrans;
	char		**sc_dmaaddr;
	size_t		*sc_pdmalen;

	bus_space_tag_t sc_iot;
	bus_space_handle_t sc_ioh;
	void *sc_ih;
	int sc_irq;
	int sc_drq;

#ifdef ESP_DEBUG
	int sc_debug;
#endif
};
--------------------------------------------------------------------------------

先頭の struct ncr5c9x_softc はMI側の ncr53c9x.c で使われる変数の構造体であり、 sys/dev/ic/ncr53c9xvar.h で定義されている。MD側の esp_isa.cesp_isa_softc の構造体のポインタをMI側の ncr53c9x.c に渡すと、MI側はその先頭の struct ncr53c9x_softc の部分のみを使ってMI側の処理を行う。 struct ncr53c9x_softc の構造は各種SCSI操作用の変数等が多く含まれており、かなり大きいので詳細は説明しないが、この中には前項で説明したglue関数群のポインタも含まれており、これらの関数はMI側では次のようなマクロを用いて呼び出される。

--------------------------------------------------------------------------------
struct ncr53c9x_glue {
	/* Mandatory entry points. */
	u_char	(*gl_read_reg)(struct ncr53c9x_softc *, int);
	void	(*gl_write_reg)(struct ncr53c9x_softc *, int, u_char);
	int	(*gl_dma_isintr)(struct ncr53c9x_softc *);
	void	(*gl_dma_reset)(struct ncr53c9x_softc *);
	int	(*gl_dma_intr)(struct ncr53c9x_softc *);
	int	(*gl_dma_setup)(struct ncr53c9x_softc *,
		    caddr_t *, size_t *, int, size_t *);
	void	(*gl_dma_go)(struct ncr53c9x_softc *);
	void	(*gl_dma_stop)(struct ncr53c9x_softc *);
	int	(*gl_dma_isactive)(struct ncr53c9x_softc *);

	/* Optional entry points. */
	void	(*gl_clear_latched_intr)(struct ncr53c9x_softc *);
};

[..略..]

/*
 * DMA macros for NCR53c9x
 */
#define	NCRDMA_ISINTR(sc)	(*(sc)->sc_glue->gl_dma_isintr)((sc))
#define	NCRDMA_RESET(sc)	(*(sc)->sc_glue->gl_dma_reset)((sc))
#define	NCRDMA_INTR(sc)		(*(sc)->sc_glue->gl_dma_intr)((sc))
#define	NCRDMA_SETUP(sc, addr, len, datain, dmasize)	\
     (*(sc)->sc_glue->gl_dma_setup)((sc), (addr), (len), (datain), (dmasize))
#define	NCRDMA_GO(sc)		(*(sc)->sc_glue->gl_dma_go)((sc))
#define	NCRDMA_ISACTIVE(sc)	(*(sc)->sc_glue->gl_dma_isactive)((sc))
--------------------------------------------------------------------------------

softc構造体のサイズは、デバイスドライバのアタッチメントの中でprobe関数とattach関数とともに struct cfaatach 構造体の中に定義される。

--------------------------------------------------------------------------------
int	esp_isa_match __P((struct device *, struct cfdata *, void *)); 
void	esp_isa_attach __P((struct device *, struct device *, void *));  

struct cfattach esp_isa_ca = {
	sizeof(struct esp_isa_softc), esp_isa_match, esp_isa_attach
};
--------------------------------------------------------------------------------

ここで使われている "esp_isa_ca" という名前の "esp_isa" の部分は config(8) が読み込む files.* の中で定義される名前である。


[注25] この節の記載内容は筆者が今までに読んだソースと経験だけに基づいた記述である。このあたりの記述に関してはもっと教科書的解説があると思うのだが、仮に見当違いの記述が混じっていたとしてもご容赦願いたい。


いざコンパイルと動作テスト

まずはコンパイル

ずいぶんと説明が冗長になってしまったが、これで実際に書くべき内容はすべてそろったことになる。とは言っても、実際にはどこにどういうソースがあってそれぞれがどのように実装されているかを確認したに過ぎず、一から作成すべきコードはどこにもなかったことに注意してもらいたい。当時実際に行った作業はprobe関数を適当なPCIのドライバからコピーして変更し、attach関数をいくつかのncr53c9xのアタッチメントとPCIのアタッチメントから切り貼りし、そしてglue関数群はほぼ esp_isa.c からコピーしただけである。

ただ、そうは言ってもデバイスドライバ以前にプログラムすらろくに書いたことのない身ではあちこちにバグ以前のミスばかりでカーネルのコンパイルを通すだけでも丸一日かかってしまい、そうして何度もソースを修正しながら無事に(?)カーネルのコンパイルが通ったところで当然すぐに動くわけもなかった。

動作テストの結果……

それで一日目はそれであっさりあきらめてソースを有識者の方々に送って見ていただいたのだが、そこで前出の古田さんから「glue関数の中の read_regwrite_regesp_isa.c そのままではダメなのではないか」と指摘をいただいた。たしかにマニュアルを見るとAm53c974の場合は各レジスタは4バイト毎に配置されていたため、 "bus_space_read_1(esc->sc_iot, esc->sc_ioh, reg);" 等のoffsetの部分を (reg * 4) と修正して試したところ、なんとそれだけでSCSIデバイスをプローブするところまで動いてしまった。

これには書いた本人が一番驚いたが「MI/MDを分離した実装をするといかに良いことがあるかという実例だろう」とのコメントもいただいた。この時は書き込みにはまだ問題があり、かつ読み込みの動作速度も200kbyte/secとかなり遅いという状態であったものの、それでもリードオンリーであれば dd(1)mount(8)fsck(8) もそのまま動いたのである。ソースの中身は何も理解せずに書いていたため、書き込みの問題点はしばらくわからないままであったが、ある日何の気なしにmac68kの arch/mac68k/obio/esp.c を見てみたところ、 esp_dma_go() の中身が esp_isa.c と違い書き込み時のみ最初の1バイトのデータを先に書いてやるような記述になっていた。

sys/dev/isa/esp_isa.c:
--------------------------------------------------------------------------------
void
esp_isa_dma_go(sc)
	struct ncr53c9x_softc *sc;
{
	struct esp_isa_softc *esc = (struct esp_isa_softc *)sc;

	ESP_TRACE(("[esp_isa_dma_go] "));

	esc->sc_active = 1;
}
--------------------------------------------------------------------------------

sys/arch/mac68k/obio/esp.c:
--------------------------------------------------------------------------------
void
esp_dma_go(sc)
	struct ncr53c9x_softc *sc;
{
	struct esp_softc *esc = (struct esp_softc *)sc;

	if (esc->sc_datain == 0) {
		esc->sc_reg[NCR_FIFO * 16] = **esc->sc_dmaaddr;
		(*esc->sc_dmalen)--;
		(*esc->sc_dmaaddr)++;
	}
	esc->sc_active = 1;
}
--------------------------------------------------------------------------------

そこで dmg_go() の関数をmac68kと同じように書き換えてやると、これまたあっさりと書き込みも動くようになってしまった。結局、当初の目標であったDC-390のPIOでの動作はソースの切り貼りだけで達成できてしまったのである。

次はDMA動作?

PIO転送はあっさり動いてしまったDC-390ドライバであったが、さすがにPCI SCSIで転送速度200kbyte/secではお話にならない。PIOにしてももう少しスピードは出ないものかとも思ったが、やはり根本的な解決はDMA動作しかないのだろうということで bus_dma(9) のmanやbus_dmaを使った各種デバイスドライバのソース、そしてAm53c974のマニュアルを眺める日々が続いた[注26]


[注26] 今にして考えると、既存のOSの実装や先入観などをまったくない状態でbus_dma APIを学んだことが結局は正しい理解につながったように思う。


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

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