BSD/OSのネットワークドライバ概説

Copyright (C) 1998 by Masahiko KIMOTO <kimoto@ohnolab.org>
第一版:1998年7月21日

[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 に詳しく
書いてある。

Masahiko Kimoto <kimoto@ohnolab.org>
Last modified: Wed Jul 22 10:12:03 1998