概要
Linuxで動くe1000eドライバを開発しようとしており、ドライバと紐付けたNICのstateがupになるところまで進めました。
個人的に忙しくなった都合で一旦ここでプロジェクトを止めるため、備忘録としてここまでの作業内容を簡単に書き記し、詰まった点についても文章として残しておこうと思います。
前半部では、NICドライバ実装のために必要な処理のうちパケット送受信以外に関する部分について書いています。
後半部では、ドライバ実装中にLinuxカーネル関連で詰まった箇所とその解決の様子を書いています。
おことわり
実はまだパケットを送受信する部分を作りこめていません。 そのこともあって、ドライバがパケットを送受信する仕組みなどについてはこの記事では一切触れないことにしました。
知りたい方は別の何かを読んでください、すみません。
自分は下記のような記事を読んで理解することができましたので、ご興味のある方はぜひ見てみてください。(すごくわかりやすいです)
MikanOS に NIC ドライバを実装する - 準備編 #自作OS - Qiita
Intel NIC ドライバにおけるパケット送信について - かーねるさんとか
流れ
以前xv6へe1000eドライバを実装し、無事送受信ができることを確認できたので(ソースコード)、次はそのドライバをLinuxで動かそうと思いました。
Linuxではドライバをカーネルモジュールとして組み込むことができるので、カーネルと一緒にビルドする必要がありません。
自分もこの機能を使って自作ドライバを動かす方針にしました。
またLinuxではカーネル側が提供するAPI関数を使うことで、自作のNICドライバとカーネルのネットワークスタックを連携させることもできます。
[前半]:NICドライバ実装に必要な作業
デバイスドライバの実装してカーネルモジュールとして動かす方法は、こちらの記事と書籍を参考にさせていただきました。
組み込みLinuxデバイスドライバの作り方 (1) #Linux - Qiita
Linuxデバイスドライバプログラミング | SBクリエイティブ
insmod/rmmod 時のエントリポイントとなる関数を用意
https://github.com/yushoyamaguchi/yama_driver/blob/debug1/yama_e1000e/netdev.c#L305
https://github.com/yushoyamaguchi/yama_driver/blob/debug1/yama_e1000e/netdev.c#L318
pci_diriver構造体を定義して、ドライバを登録
ドライバを登録する方法は、デバイスの種類によって異なる。
PCIデバイスに関しては、insmod時のエントリポイントとなる関数で、pci_divice登録用の関数を呼び出すことで登録できる。
引数となるのはpci_driver構造体で、ここにこのドライバが取り扱うデバイスのリストや、デバイスの状態変化時に呼び出される関数を定義する。
こうすることで、今実装しているドライバとデバイスを紐づけることができる。
static struct pci_driver yama_e1000_driver = { .name = yama_e1000e_driver_name, //name .id_table = yama_e1000_pci_tbl, //the list which specify the PCI devices supported by the driver. .probe = yama_e1000_probe, //function that is called when the kernel discovers an appropriate PCI device .remove = yama_e1000_remove // function that is called when a matching PCI device is removed from the system };
今回実装したのはこれだけだが、きちんとしたドライバを作るには他にも実装すべき項目はある。
net_deviceとして登録
probe関数内で、デバイスをnet_deviceとして登録する。
struct net_device *netdev = alloc_etherdev(sizeof(struct yama_e1000e_adapter)); if(!netdev){ return -ENOMEM; } netdev->netdev_ops=&yama_e1000e_netdev_ops; // some ops ret=register_netdev(netdev);
alloc_etherdev関数の引数となっているyama_e1000e_adapterという構造体は、ドライバ実装において必要な変数を含むものを自分で定義したものである。
こうすることで、任意の必要な値をprivateデータとしてnet_device構造体の末尾にくっつけて取り扱うことができる。
現時点で自分が定義しているデータは、以下である。
https://github.com/yushoyamaguchi/yama_driver/blob/debug1/yama_e1000e/include/yama_e1000e.h#L33
net_deviceハンドラ関数を用意してハンドラテーブルに登録
static const struct net_device_ops yama_e1000e_netdev_ops = { .ndo_open = yama_e1000e_netdev_open, .ndo_stop = yama_e1000e_netdev_close, .ndo_start_xmit = yama_e1000e_start_xmit, .ndo_set_mac_address = yama_e1000e_set_mac_addr, .ndo_get_stats = yama_e1000e_get_stats, };
下記のような仕様に沿った形で、net_deviceとしての様々な役割をこなす関数を作成し、テーブルに登録する。
https://elixir.bootlin.com/linux/latest/source/include/linux/netdevice.h#L1400
全てを実装することはできないので、とりあえず必要そうな関数だけを登録した。(それでもその中身はまだ完成なのですが...)
パケット送信時に呼び出される関数は、ndo_start_xmit のところに登録する。
割り込みの定義(未動作確認)
受信はパケットが入ってきた時に、割り込み処理として実行されるようにしたい。
そのため以下のように、受信時に呼び出したい関数 yama_e1000e_irq_handler を登録した。
ret=request_irq(adapter->pdev->irq,yama_e1000e_irq_handler,IRQF_SHARED,yama_e1000e_driver_name,adapter);
この関数の第一引数には、該当するirq番号を入れる。
PCIデバイスの場合は、スロットごとにIRQ番号が自動で決められており、OSから教えてもらうことができる。
MMIOレジスタを適切に初期化して、実際にパケットを処理する部分を書いていく(未実装)
上記のように、パケット送受信部の処理を実装する。
ハンドラとスケジューリング
割り込み
割り込みの後半部、Softirq、Tasklet、Work Queue
上記の記事のように、割り込みハンドラ自体の処理を短くすることで、割り込み禁止の時間をを短くする必要がある。
時間的制約のある処理だけをハンドラで行い、残りの処理はよしなにスケジューリングする。
[後半]:Linuxカーネル関連で詰まったところ
insmod時のカーネルクラッシュ
はじめ、insmod時にカーネルがクラッシュする症状が出ていた。
クラッシュ時のメッセージを読むために、
-serial file:path_to/serial.log
上記のオプションをつけて起動したQEMU上でUbuntuを動かして、
ゲストUbuntu上で /etc/default/grub を編集してコンソールの内容をシリアル出力するように変えた上で、ドライバを動かした。
[ 646.160007] genirq: Flags mismatch irq 11. 00000000 (yama_e1000e) vs. 00000080 (uhci_hcd:usb1)
クラッシュ時にこのようにirq番号に関するエラー出力を観測できたので、request_irq関数の第三引数をIRQF_SHAREDに変えることで、カーネルクラッシュが起こらないようにした。
インターフェースのstateがUPにならない問題
動作しているインターフェースを iproute2を利用して見た際は通常、下記の写真のようにstateがUPになっている。

しかし自分が実装したドライバが紐づいているインターフェースをiproute2を使って見たところ、stateがUNKNOWNになっていた。
原因を探るべく、iproute2のnetlinkメッセージを読み取る部分にデバッグプリントを入れて実行してみたところ、netlinkメッセージのoperstateという部分が原因であるように思えた。

そこで今度は、Linuxカーネル内のどこの部分がnetlinkメッセージのこの部分に値を設定しているのかを調べた。
https://elixir.bootlin.com/linux/v6.6.6/source/net/core/rtnetlink.c#L1807
すると、net_device構造体のoperstateという変数が参照されているような記述が見つかったので、ドライバでその値を適切に設定するように変えた。
その結果、無事に自作ドライバと紐づくインターフェースのstateがUPという風に表示されるようになった。

処理を全て追いかけたわけではないが、この rtnl_fill_ifinfo という関数は、
- RTM_GETLINKメッセージを受け取った時のハンドラ関数
- net_deviceに関するイベント(register・open・close・chage state・異なるnamespaceへの移動など)が起きた際にnetlinkメッセージを作成する関数
から呼び出されていた。
さいごに
怪しい記述や間違っている箇所を見つけてくださった方がいれば、教えていただけると嬉しいです...