はじめに
MIPS系のルータのFreeBSD化をいろいろ試しているが、コンソールが使える、rootfsがマウントできてログインできる、この次にはやはりnetworkが使えるようにしたいと思いFreeBSDのEthernetドライバの事を調べてみた。ネットワークが使えないFreeBSDなんて、、、
Ethernetドライバとは
Ethernetドライバは基本的にはカーネルコードから送られてくるデータを送信し、ハードウエアから送られてくるデータをカーネルコードに引き渡す事になる。FreeBSDではifnetというフレームワークがこのカーネルコードとのデータのやり取りをサポートしている。
Ethernetドライバも一般のドライバと同じように作られていてprobe,attachというコードにより処理が始まります。Ethernetドライバではattachの時にifnetへ登録をおこないます。
ifp = sc->tulip_ifp = if_alloc(IFT_ETHER);
/* XXX: driver name/unit should be set some other way */
if_initname(ifp, "de", sc->tulip_unit);
ifp->if_softc = sc;
ifp->if_flags = IFF_BROADCAST|IFF_SIMPLEX|IFF_MULTICAST;
ifp->if_ioctl = tulip_ifioctl;
ifp->if_start = tulip_start;
ifp->if_init = tulip_init;
IFQ_SET_MAXLEN(&ifp->if_snd, ifqmaxlen);
ifp->if_snd.ifq_drv_maxlen = ifqmaxlen;
IFQ_SET_READY(&ifp->if_snd);
ifmedia_init(&sc->tulip_ifmedia, 0,
tulip_ifmedia_change,
tulip_ifmedia_status);
ether_ifattach(sc->tulip_ifp, sc->tulip_enaddr);
送信はカーネルから登録した関数(上のコードではtulip_start)にmbufが渡されるので、それをハードウエアに送り込みます。これはLinuxでのnet_device構造体のメンバーのhard_start_xmitに相当します。受信はハードウエアの割り込みやポーリングでデータが入ってきた事がわかるので、それをif_inputでカーネルに引き渡します。if_inputはLinuxでのskb_putにあたります。
送信のmbufは送信が終わったら解放して、受信のmbufはkernelに上げたmbufの代わりを貼り付けて次を待ちます。
Tulip
Tulipとは1990年代DECが作っていた古いPCI Ethernetチップのコードネーム(?)です。このチップのドライバコードはsys/dev/deにあります。このコードはいろいろなチップに対応するためにちょっと見通しが悪いです。おそらくi386 BSDのころからあったのかもしれません。devの下のdc,vr(VIA Rhine),wb(Winbond)もTulip互換系のようです。DECの半導体部門は1998年にIntelに買収されたため21143はIntelからもリリースされています。
Tulipの21041などのDECのpdfドキュメントはネットで入手可能です。初期の物はpsフォーマットだったりして時代を感じます。
Ethernetの話ではないですが話ですが、DECのAlpha部門は2000年代に入ってからIntelに売却されているのですが、ところがAlphaテクノロジでIA32を64Bit化されたチップがAMDから発売されたのはなんとも皮肉な話です。
どのような経緯かは定かではないのですが、TulipはSiByteやAtherosやIDTのSOCでも採用されたようです。RT1310もTulipのようです。
SOCで採用されたTulipはPCIではなくメモリマップで提供されています。
TulipのレジスタはCSRと呼ばれて、0から8までが元々の仕様で、それ以外は拡張されたものになります。Ethernetアドレスを拡張されたCSRに設定するパターンと、セットアップフレームとして送信して設定するパターンがあります。CSRのバウンダリは4バイトや8バイトだったりします。
拡張は既存のCSRの空いているビットを使ったり、CSRを増やしたりなど、CSRのチップによって様々で注意が必要です。
このチップにはdmaが入っていて送信/受信ともdmaで処理する事ができます。dmaはDescriptorという16バイトの構造により処理をおこないます。Descriptorにはステータス、コントロールとサイズ、バッファへのポインタが2つ入っています。それぞれ32bitで4バイト×4となります。
Descriptorは連続したリスト形式(Implicit Chain)か、二つ目のバッファポインタに次のDescriptorへのポインタを入れるリンク形式(Explicit Chain)で構成されます。ドライバの実装はこのどちらかを使う事になります。
Descriptor自体は固定で持つ事ができますが、バッファのポインターの実体のmbufは動的に指定する必要があります。
受信用のDescriptorのバッファはmbufを事前に用意しておいて、割り込みが上がってデータが入ったmbufをifに渡して、新しいmbufをDescriptorに貼付けます。
DMAに渡すDescriptor自体のアドレレスやバッファのアドレスは物理アドレスである必要があります。これはDMAはmmuを介せずメモリアクセスするためです。
送信用のDescriptorにはifから引き渡されたmbufを貼付けて送信を開始します。この処理は大きく分けると空のリストに追加する場合と、既にリストにエントリーがあるところに追加する場合があります。空のリストに追加する場合はデータを登録してstartとendのフラグをセットします。既に登録されている場合は既存のデータのendのフラグクリアして新たにデータを追加して、endのフラグを設定します。
Kernel APIのbus_dma_tag_create() -> bus_dmamem_alloc()を呼ぶとカーネルからアクセスするためのKVAなアドレスが返ってきます。次にbus_dmamap_load()するとコールバックにdma に使用する物理アドレスが入ったbus_dma_segment_tが渡ってきますので、Tulipのレジスタ(CSR)に渡します。
送信時のDescriptorのbufferはbus_dmamap_load_mbuf_sgでmbufの物理アドレスを受け取りバッファのポインタとして利用します。
注意が必要なのはキャッシュが効いて、実メモリに書かれていないデータをdmaしてしまわない事です。このためにbus_dmamap_sync()などを適宜呼ぶ必要があります。
送信については全てDMAされますが、受信についてはMACに設定されたアドレスに送られてきたものと、EthernetのブロードキャストとマルチキャストパケットがDMAされてドライバに渡されます。マルチキャストパケットは限定して受け取る設定ができるチップもあるようです。
通常のEthernetチップはMACとPHYという組み合わせで構成されています。PHYの設定もMACのレジスタ(CSR)でおこなう事ができます。
MACとPHYはMIIという仕様で接続されていている。FreeBSDでは古くはMIIは各ドライバで実装していたが3.0くらいからNetBSD由来のmiibusが持ち込まれたようだ。deなどの古いドライバはmiibusは実装されていません。
ルータなどでEthernetスイッチ機能がある場合はPHYの代わりにSwitchチップを接続しています。
dev/etherswitchでは、miiをmdioという仕組みに差し替えて、MACからの接続をmidoで代理するmiiproxyというインターフェースを利用しています。またmdioは実質的にはmiiなのですが、スイッチに付いているPHYをコントロールする機能も提供しています。
promiscuousモード
大抵のEthernetチップはpromiscuousモードという物をもっている。これは通常Ehernetチップは自分のMacアドレスのユニキャストと指定されたマルチキャスト、それとブロードキャストのEthernetパケットを時受信してホストに上げるのだが、すべてのパケットをホストに上げるモードになる。これはtcpdumpなどで利用されるモードになる。
インターフェースがこのモードになってもインターフェースの先にSwitchがついていると確認が面倒だったりする。
もし二つのインターフェースがある場合はbridgeを試す事でこのモードの動作確認が出来る。
ちなみにpromiscuousとはお下品なスラングのようです。
テストケース
送信はカーネルから渡されたmbufをDMAに突っ込むだけなので、ブロードキャスト・マルチキャスト・ユニキャストの種類は関係ないのですが、受信についてはデバイスが処理するのでそれぞれのタイプの確認が必要です。
- 内から外へping - ブロードキャスト(ARP)と1対1パケットの確認
- 外から内へping - ブロードキャスト(ARP)と1対1パケットの確認
- iperf3での双方向のテスト
- 外から送信されたマルチキャストを受信
- tcpdumpで自分以外のパケットの確認
- などなど
参考資料
FreeBSD_kernel_networking.pdf
Lecture 23: More devices - UARTとTulipのドライバの解説があります。
FreeBSDでインタフェース名につけられる名前の根拠他
NIC MANIA - 古いですがいろいろなNICのレビューがあります。
ルータのEthernet
FreeBSDのカーネルデバッグ
FreeBSD device driver で DMA
後日談
これを調べていたのはAR2315のEthernetドライバを作ろうと思ったのが、きっかけでした。当初deのコードをコピーしてとりあえず動かしてみようと思い、試していたのですがうまくいかず一回あきらめました。で、この記事を書いた後にもう一度mips/idt/if_kr.cをベースに試したところうまく動くようになりました。if_krもTulipと同じ16バイトのDescriptorを使っていますが、構成が微妙に違っています。その辺をNetBSDのコードを参考に修正してみました。そもそもdeのコードはいろいろなサポートコードがはいって、複雑になっている上にmiibusバス導入以前のコードでした。ただAR2315を使ったターゲットはPHYではなくSWITCHが接続されているのでPHYのコードは実装せず、RedBootが初期化した状態をそのまま使うコードになっています。ターゲットによっては動かなくなるので、ほんとはよくないんですかね。
cとhファイルをコピーして"kr_"と"KR"を置換しました。krは割り込みが4つありますがAR2315は1つなので書き換えました。
AR2315ではTulip由来のCSRが0x1000からあり、0x0000からはAR2315固有の設定のCSRがあります。
元々のPCIカードのTulipはMACアドレスをボードに持っていたですが、SOCに入っているTulip互換デバイスは、MACアドレスをコードで設定するためにCSRが拡張されています。
probe,attachやバッファの管理はkrのコードベースで、CSRアクセスや割り込み処理はNetBSDのコードをコピーしました。
外からのpingはすぐに動いたのですが、中から外へのpingがすべてlossしてしまう状態でした。いろいろ調べたところ、1296行のキャッシュの処理が悪さをして、mbufのデータを壊していました。if_krってちゃんと動くのかな。。。
とはいえgonzoさんのコードは見やすくてよいです。
are_fixup_rx()でTCPヘッダーを4バイトバウンダリに合わせているのですが、元のコードはmbufの全体をコピーしていましたが、packetのサイズだけコピーするようにしています。あとm_adjは8バイトの必要なく4バイトで十分なので変更してあります。このことはfreebsd-mipsのmlにも報告してあります。
上の図ではm_getclで2Kの受信用のバッファを用意してm_adjで先頭に4バイトの空きを作りDescriptorに登録します。DMAでパケットをメモリにコピーされたら、2バイト前にcopyしてTCPヘッダーを4バイトアライメントに合わせます。
当初うまくいかなかった原因として、Endianに関する設定がAHBに一つとデバイス側に三つあり混乱してしまった事があります。AHBで変換してデバイスでも変換すると元に戻ってしまいます。^ ^;
RT1310のドライバも作ってみたのだが、AR2315ではCSRで自己のMACアドレスを設定していたが、RT1310ではsetup frameを使っていたのでdev/dcを参考に実装してみた。あと違いはエンディアンと、CSRが4バイトではなく8バイトバウンダリくらいだった。
と思ったのだが、実はすぐに落ちる状態であった。確認したところencap()のEFBIGでpanicしているようだったので、この部分の処理を他のコードからコピーしたところとりあえず落ちなくはなった。まだ20世紀の頃、鮎川誠がJSを使っているって話を聞かれて、「良さそうなページからコピーしている、ロックの基本はコピーです。」というような話をしていたな。
テストしてみようと思い、ネットで探したところiperf3というオープンソースがあり、ZRouterのtargetにつっこんでイメージを焼いてみた確認してみた。
テストしてみるとすぐにネットワークが落ちてしまう。。。ARの方は正常に動作します。
でいろいろ試してみて、encapの前半の処理をvrからコピーしてみたところ、EFBIGのエラーは出なくなったのだが、まだ落ちる。
モリモリデバッグライトを入れて、調べたところTxのDMAでUnderflow Errorが起きているようだ。どうもDMAのディスクリプタのOwn,Start,Endのフラグがまずいようだったので、ごにょごにょしたら、うまく動くようになった。継ぎ足しのときにEndフラグを引っこ抜いて、Startを付けないようにしました。
このようにある程度のレジスタ互換はあってもDMAの挙動はチップ毎に微妙に違っているようです。
とりあえずこれで落ちたりしなくはなりましたが、いろいろいじっている間にiperf3でRオプションで時々データが流れなくなっていましたが、とりあえず動いているのでそのうちデバッグしたいと思います。
久しぶりにちょっといじってみてbridgeを試してみたのだが、bridgeで必要なpromiscuousモードの実装が抜けていたので、追加してみた。
これでとりあえず動くようになったのだが、負荷をかけると落ちる状態で調べてみた。問題は二つあって、bridgeしなくなる問題と受信パケットが壊れる問題であった。
bridgeしなくなる問題は、初期化のパラメータ(IFQ_SET_MAXLENとif_snd.ifq_maxlen)に問題があった。
パケットが壊れる問題はなぜか負荷をかけると、受信データが4バイト連ずれてバッファにDMAされている。。。なぜかすべてではないのだが、CRCエラーも発生している。
iperf3で調べてみると、Switchの方につながれたfv0では問題が起きるが、Phy相当とつながっているfv1では問題が起きません。これからfvの問題ではなくSwitchの問題である事が特定できました。このターゲットはip175cが使われていてetherswitchのドライバを利用しています。ためしにmidoのphyアドレス29以降の書き込みを無効にしたところ問題が起きなくなりました。dev/etherswitch/ip17x/ip175c.cの設定が原因のようです。
実装されているSwitchチップはIP175CなのだがIP175C/IP175CH enable register(29:31)が0x175aになっていて、ここに0x175cを書き込むとダメになるようだ。このターゲットは2ポートEtherでVLANの必要もないので、このターゲットでのetherswitchの利用はあきらめる事にした。
iperf3での確認
SOC | TYPE | IF | normal | revers |
---|---|---|---|---|
AR5312 | MIPS 4K | are | 17.7/17.7 Mbits/sec | 11.6/11.5 Mbits/sec |
AR2313 | MIPS 4K | are | 30.6/30.6 Mbits/sec | 27.1/27.1 Mbits/sec |
AR5315 | MIPS 4K | are | 18.2/18.2 Mbits/sec | 16.3/16.2 Mbits/sec |
AR2317 | MIPS 4K | are | 16.3/16.3 Mbits/sec | 15.0/14.9 Mbits/sec |
AR2318 | MIPS 4K | are | 18.1/18.1 Mbits/sec | 16.4/16.4 Mbits/sec |
RT2880 | MIPS 4K | rt | 9.29/9.16 Mbits/sec | 9.50/9.50 Mbits/sec |
RT2880+RTL8366SR | MIPS 4K | rt | 8.49/8.43 Mbits/sec | 35.0/34.9 Mbits/sec |
RT3050 | MIPS 24K | rt | 16.6/16.5 Mbits/sec | 74.3/74.2 Mbits/sec |
MT7620 | MIPS 24K | rt | 70.4/70.4 Mbits/sec | 88.8/88.8 Mbits/sec |
AR7240 | MIPS 24K | arge | 45.9/45.8 Mbits/sec | 87.9/87.8 Mbits/sec |
AR9132 | MIPS 24K | arge | 117/117 Mbits/sec | 125/125 Mbits/sec |
AR9341 | MIPS 74K | arge | 58.9/58.8 Mbits/sec | 91.6/91.6 Mbits/sec |
RT1310 | ARM V5T | fv | 13.7/13.7 Mbits/sec | 42.7/42.5 Mbits/sec |
対向はMicroserverでGigaのスイッチを挟んで同じ環境で確認しています。
fv/areも最初そうでしたが、iperf3で落ちるドライバもたまにあります。上記のようにスイッチが落ちるケースものあるので、ドライバの問題とも言い切れないのですが。
2018/10 古いドライバを消すって話が出てます。
mgeについて
MarvellのSOCで使われているmgeを調べてみました。
RXのディスクリプタはlongが4つです。
これを16バイトアラインで並べるのですが、リングを作るときにこんなふうにしてました。
desc_paddr = 0;
for (i = size - 1; i >= 0; i--) {
省略
/* Chain descriptors */
dw->mge_desc->next_desc = desc_paddr;
desc_paddr = dw->mge_desc_paddr;
}
tab[size - 1].mge_desc->next_desc = desc_paddr;
下から作って最後に下と上を引っ付けるってコードです。一瞬理解できなかったのですが、あまり分かりやすいコードではないですね。