[1.はじめに] 4.4BSD UNIXのネットワークドライバの構造について概説する。この資料では BSD/OS 3.1を例として用いる。以下とくに断りがない限りBSDという名称は BSD/OSを指すものとする。またネットワークドライバと言った場合、データリ ンク層のドライバを指すものとする。 BSDのネットワークドライバはデバイス依存部とデバイス非依存部に分けるこ とができる。前者はne2000や3c509など、ハードウェアごとに存在する。後者 はEthernetならEthernetで共有される。したがって、新しいEthernetボード用 のドライバを追加する場合は前者のみを実装すれば良い。また、あらたに (IEEE1394のように)プロトコルごと実装する場合は、両者を実装する必要があ る。この資料では、ne2000のドライバを参考にする。関係するファイルは以下 のものである。 デバイス依存部: /sys/i386/isa/if_ne.c (ne2000の場合) /sys/i386/isa/if_neconf.c (同上) /sys/i386/isa/if_nereg.h (同上) デバイス非依存部: /sys/net/if_ethersubr.c (Ethernetの場合) なおこのドキュメントはBSD/OS 3.1(M310-020まで)のソースを元にしている。 [2.概要] デバイスドライバとは、ハードウェアデバイスとのやりとりを行う処理をまと めたものである。ハードウェアに依存する部分を集約したことにより、同じ機 能を持つ異なるハードウェアの間で、変更するべき点を最小に押えることがで きる。デバイスドライバはつねに動作しているのではなく、要求があった時の み必要な動作を行う。要求とは、OSやアプリケーションからの読み書き要求で あったり、ハードウェアからの割込み要求であったりする。このためにデバイ スドライバは、基本的に要求駆動型(イベントドリブン型)の構造になっている。 この基本的な構造は、ハードディスクドライバなどと同じである。BSDにおい て、ネットワークドライバと他のデバイスドライバの違いは、後者はデバイス ファイルに対応づけられていることである。対してネットワークドライバはデ バイスファイルへの対応づけがなされていない。 ネットワークドライバとは、ここではデータリンク層の処理を行うプログラム のことを指す。ハードウェアのデバイスドライバの上位に、リンク層プロトコル のドライバが存在する。例えばEthernetやFDDI, PPPなどである。 [3.デバイス依存部] ne2000ドライバを例にとる。BSDではネットワークドライバにそれぞれ名前が つけられており、これはインタフェースの名前に対応する。例えばne2000では "ne"という名前が付けられている。また、同じボードが複数枚装着される可能 性もあるので、0から順に番号をつけている。1枚目のne2000に割り当てられる インタフェース名は"ne0"になる。 neドライバのデバイス依存部はif_ne.cに格納されている。他にPCIのID情報が if_neconf.cに格納されており、レジスタマップなどがif_nereg.hに格納され ている。 if_ne.cは、ドライバに関する構造体を宣言する部分と、実際のデバイスへの 操作を行う一連の関数が記述されている。ドライバに関する構造体はstruct ne_softcとstruct cfdriverがあり、デバイスドライバ用のワークエリアや、 関数へのポインタが格納される。これはC++などのOOP言語に例えると分かりや すい。struct ne_softc とstruct cfdriver の宣言の箇所で、いわゆるクラス の定義を行っている。このクラスのインスタンスがOSによって生成され、 neprobe()とneattach()の二つの関数で初期化される。この二つがコンストラ クタの役割をすると考えても良いだろう。仮にボードが2枚あったらインスタ ンスは二つ生成されることになる。OSからの書き込み要求や、ハードウェアか らの割込みは、対応するメソッド呼び出しに対応する。 struct ne_softcと、struct cfdriverはそれぞれ次のような構造をしている。 (PCMCIAドライバなど余計なものは取り除いてある) --------------------------------------------------------------------------- struct ne_softc { struct device ne_dev; /* base device (must be first) */ struct isadev ne_id; /* ISA device */ struct intrhand ne_ih; /* interrupt vectoring */ struct arpcom ns_ac; /* Ethernet common part */ struct ifmedia ns_media; /* Media options */ struct atshutdown ns_ats; /* shutdown routine */ int ns_base; /* the board base address */ int ns_ba; /* byte addr in buffer ram of inc pkt */ int ns_cur; /* current page being filled */ int ns_bdry; int ns_ifoldflags; /* previous if_flags */ u_char ns_ne1000; /* true if the board is NE1000 */ u_char ns_tbuf; /* start of TX buffer (pages) */ u_char ns_rbuf; /* begin of RX ring (pages) */ u_char ns_rbufend; /* end of RX ring (pages) */ struct prhdr ns_ph; /* hardware header of incoming packet*/ struct ether_header ns_eh; /* header of incoming packet */ char ns_pb[2048 /*ETHERMTU+sizeof(long)*/]; }; struct cfdriver necd = { NULL, "ne", neprobe, neattach, DV_NET, sizeof(struct ne_softc) }; --------------------------------------------------------------------------- struct cfdriverは/sys/sys/device.hで宣言されており、次の構造をしている。 --------------------------------------------------------------------------- struct cfdriver { void **cd_devs; /* devices found */ char *cd_name; /* device name */ cfmatch_t cd_match; /* returns a match level */ void (*cd_attach) __P((struct device *, struct device *, void *)); enum devclass cd_class; /* device classification */ size_t cd_devsize; /* size of dev data (for malloc) */ void *cd_aux; /* additional driver, if any */ int cd_ndevs; /* size of cd_devs array */ }; --------------------------------------------------------------------------- if_ne.c中のstruct cfdriverの定義によって、デバイス名が"ne"、初期化用の 関数がneprobe,neattach、デバイス用ワークエリアの大きさがsizeof(struct ne_softc)であると分かる。neprobeとneattachの違いは、前者がデバイスの検 出(probe)を行い、後者がデバイスの有効化を行うことにある。 最初に、OSが起動しデバイスが認識、初期化されるまでの流れについて説明す る。最初のデバイスの検出を行う。この処理はneprobe()(853行目)で行われる。 neprobe()はデバイスが発見されたら1を返し、見つからなかったら0を返す。 実際の検出処理は、ISAデバイスからPCIデバイスかによって分岐している。こ こではPCIデバイスの場合を見ていくことにする。PCIデバイスの検出は ne_pci_probe()(982行目)で行われる。この関数では、まずPCIデバイスのIDを よみとり、ne2000が存在するかを調べる(pci_scan())。見つかったら、PCIデ バイスの情報を読み取る(pci_getres())。この時点で、そのPCIデバイスに割 り当てられているI/Oの領域などが判明する。次に、このデバイスが本当に ne2000かどうか調べるために、適当なI/O処理を行う(998-1053行目)。予期さ れる通りの操作ができたらne2000が見つかったものとして1を返す。 次にneattach()(1158行目)が呼びだされる。この関数の中ではデバイスと ne_sotfc構造体の初期化を行う。まずハードウェアの初期化と行い、neprobe から渡されたデバイスの情報に基づきne_softc構造体の初期化を行う。次に EthrenetBoardのROMから、MACアドレスを読み取る(1206-1216行目)。次に interface構造体を初期化し、デバイス非依存部の初期化関数を呼び出す(1237 行目)。最後にこのデバイスが有効になったことをOSに宣言し(1255行目)、割 込みハンドラを登録する(1277行目)。interface構造体の初期化の際に、 neinit,nestart,neioctlの各関数を登録している。ここで、OSからの読み書き やパラメータの変更要求に対しての動作ができるようになる。また、割込みハ ンドラとしてneintrを登録したため、ハードウェア割込みに対しての動作がで きるようになる。初期化が終了したあとは、ここで登録した各関数が、必要に 応じて呼ばれることになる。 例としてパケットを受信したときの処理を見ていくことにする。パケットを受 信するとハードウェア割込みが発生する。割込み処理はneintr() (1577行目) で行われる。ハードウェア割込みが発生する要因は、パケットの受信以外にあ るので、まず何が原因で割込みが発生したのかを調べる必要がある。パケット を受信した場合は1632行目からの処理が行われる。実際の受信処理はnerecv() (1744行目)で行われる。nerecv()ではパケットを受信バッファに取り込んだ後、 neread()(1875行目)に処理を渡す。neread()ではBSD Trailerとbpfの処理をし たのち(ここではあまり気にしなくて良い)、mbufを確保してパケットバッファ の内容をコピーする(neget() (1974行目)で行う)。最後にmbufの内容を デバイス非依存の関数に渡す(1957行目)。 [4. デバイス非依存部] デバイス非依存部の処理は、簡単に言うとデバイスドライバとネットワーク層 (IP)の処理との中継をするものである。Ethernetのデバイス非依存ドライバは /sys/net/if_ethersubr.cに格納されている。このドライバは大きく3つの処理 に分けられる。初期化処理、パケット受信処理、パケット送信処理である。 初期化処理はether_attach() (554行目)で行われる。この関数はデバイスドラ イバのattach処理から呼び出される。この関数ではifnet構造体の初期化を 行い、リンク層の情報(MACアドレス長や、出力関数のハンドラなど)を登録する。 読み込み処理はether_input()(316行目)で行われる。この関数はデバイスドラ イバのパケット受信処理(ne2000の場合はneread())から呼び出される。引数と してifnet構造体、Ethernetヘッダへのポインタ、データ本体が格納されてい るmbufが渡される。ether_input()では、まず受信したパケットがマルチキャ ストまたはブロードキャスト宛のものであったらフラグを立てる。次にプロト コルの種類ごとに、パケットを対応したキューに追加する(493行目)。プロト コルの種類がIPであれば(343行目)ipintrqになる(345行目)。 書き込み処理はether_output() (99行目)で行われる。上位層から渡られた struct rtentryがNULLでなければ、以下の処理を行う。rtentryがルーティン グテーブルのための構造体であるが、BSDではARPテーブルもルーティングテー ブルに含まれている。したがって、rtentry構造体が、ルーティングテーブル として用いられる場合と、arpテーブルとして用いられる場合とがあることに 注意する必要がある。 最初に上位層から与えられたルーティング(rt0)が有効であるかを調べる。無 効であったら、再度ルーティングテーブルを検索する(119行目)。見つからな かったらエラーを返す。新たに見つかったルーティングで指示されているイン タフェース(rt->rt_ifp)が、現在処理の対象になっているインタフェース (ifp)と異なったら、新たに見つかったインタフェースの送信関数に処理を移 す(122行目)。次にrt(上位層から与えられたルーティングまたは上記で検索し たルーティング)のRTF_GATEWAYフラグが立っている場合以下の処理を行う。 RTF_GATEWAYフラグが立っている場合は、目的のIPアドレスに送信するために はルータを経由する必要があることを意味する。中継点に関するルーティング が記述されていなかったら、新たにルーティングテーブルを検索する(132行目)。 中継点に到達するために、さらにルータを経由する必要があったり(136行目) か、中継点に送信するためのインタフェースが現在のものと違った場合(137行 目)はエラーを返す。ここまでが、rt0が有効であった場合の処理である。 次にプロトコルごとに分岐する。IPの場合は150行目からの処理になる。151行 目ではarpの解決を行う。解決できなかった場合(返答待ち状態)は、そのまま 終了する。154-155行目の処理はブロードキャストの場合、自分自身にも転送 するためにコピーしている。実際の転送は264-265行目で行っている。最後に 送信キューにパケットを追加し(295行目)、デバイス依存の送信関数を呼び出 す(297行目)。 [5.新しいデバイスの追加] 新しいネットワークデバイスを追加するために必要な手順は以下の通りである。 ・デバイスの名前を決める。仮に"new"とする ・if_new.c, if_newregs.hなどのファイルを作成する。 PCIのみのデバイスなら/sys/i386/pci/に置くのが適当。 ・if_new.cの中で諸々の関数を実装する。 ・if_new.c内でstruct new_softc を宣言し、struct cfdriverを定義する。 e.g. --------------------------------------------------------------------------- struct cfdriver newcd = { NULL, "new", newprobe, newattach, DV_NET, sizeof(struct new_softc) }; --------------------------------------------------------------------------- ・/sys/i386/conf/files.i386 で関連するファイルを記述する。 e.g. --------------------------------------------------------------------------- device new at pci: ifnet, ether, pcisubr file i386/pci/if_new.c ne device-driver --------------------------------------------------------------------------- ・kernelのconfigファイル(/sys/i386/conf/GENERICなど)にnewドライバに ついての記述を追加する e.g. --------------------------------------------------------------------------- new* at pci? --------------------------------------------------------------------------- ・kernelを作り直す。 e.g. (configファイルをNEWCONFIGとする) --------------------------------------------------------------------------- % cd /sys/i386/conf/ % config NEWCONFIG % cd ../../compile/NEWCONFIG % make depend % make --------------------------------------------------------------------------- ・出来たカーネルを置き換える <補足> [A. ソースリストについて] ソースを読む時の注意点としてPCMCIAドライバの存在があげられる。BSD/OSが 採用しているPCMCIAドライバはWildboarといってWIDEプロジェクトのメンバー の手によって開発されたものである。BSD/OSでは動的にデバイスドライバを組 み込む機能(LKM: Loadable Kernel Module)がないために、特殊な構造をして いる。PCMCIAデバイスに対応したドライバは、probeとattachの関数を二つ持 つ。*_cc_propbe, *_cc_attachという名称の関数がそれである。PCMCIAデバイ スでは通常のpropbe, attachは正常に働いたものとして返り値を返す。このた め、PCMCIAデバイスはつねに起動時に認識されたものとして扱われる。実際は、 カードが挿入されたときに、各デバイスドライバに処理が渡され、そこで *_cc_probeが呼ばれる。挿入されたカードが、そのデバイスドライバの処理の 対象であれば、*_cc_attachが呼ばれてカードが認識されるのである。今回カー ネルのソースを見る場合には、むしろPCMCIAドライバは邪魔になるので、ない ものと思って良い。 [B.mbufについて] mbufはBSDがカーネル内で用いるデータ構造で、主にネットワークパケットの やりとりに用いられる。ネットワークパケットは、カーネル内でヘッダの削除 や追加が頻繁におこなわれる。この時に毎回メモリ確保と転送をしていては、 速度の点で問題がある。そこで、必要に応じて小さなメモリ領域を線形リスト で連結させて(mbuf chainと呼ばれる)、メモリ内でのブロック転送を最小限に 押えられるようにmbufが導入された。mbufの構造については、あまり深いりし なくても良いと思われるが、mbufを操作するための関数およびマクロ群につい ては知っておくとよい。mbufについてはTCP/IP Illustrated vol.2 に詳しく 書いてある。