NVMeドライバをLinux上でuioを用いて開発してみたが間に合わなかった

はじめに

いいですか。「開発してみました」というのは、「完成しました」ではないのです…。

「黙って現在の進捗を差し出せ!」という方はこちらをご参照ください。

github.com

この記事はなに

自作OSアドベントカレンダーの7日目として書かれた記事です。

17日を書こうと思ったら、すでに登録されている方がいらっしゃいましたので、2番目に好きな素数を選びました。

話は6か月前にさかのぼる…

6ヶ月ほど前でしたでしょうか。Liva師からこんな話が降って来たのは。

Liva師「hikaliumさん、うちの自作OS(Raphine)向けにNVMeのドライバとか書いてみたくない?」

hikalium「いいですね〜」

いいですねーと言ってしまったものの、私は実は本当のドライバというものを書いたことがありませんでした。

これでは、自作OS開発者失格ですね。

というわけで、とりあえずnvmeの仕様をざっと読んでみることにしました。

NVMeってなに?

Non-Volatile Memory Expressの略で、不揮発性メモリ(SSDとか)を高速に読み書きできるインターフェイスを提供するよ!というものです。やっぱり速いのは正義ですね。 最近はこの、NVMeのインターフェイスで通信するSSDもだいぶ増えてきていて、ノートPCにも内蔵されていたりします。 これは自作OSとしてはサポートするしかありませんね!

どれくらいすごいインターフェイスなのかは、検索するなりWikipediaでも眺めておいてください。 私たちが知りたいのはそれよりも下の、どうやって制御すればいいのか、というところですので。

というわけで、まずは仕様を読んでみました。

仕様はどんな感じ?

NVMeの仕組み自体は、そこまで難しいものではありません。

まずは、データ構造の話から。仕様書の4章をみてください。

Submission Queue / Completion Queue

NVMeのコントローラとコマンドやデータをやり取りするためには、Submission Queue / Completion Queueという、リングバッファのような構造を用います。この解説が、4.1に書いてあります。

Submission Queue(略してSQ)は、コントローラに対してコマンドを送るために使用します。たとえば、読み書きを行ったり、設定情報を読み書きしたりするときには、このSQに私たちがコマンドを書き込んであげるわけです。

Completion Queue(略してCQ)は、逆に、コントローラから「コマンドが完了したよー」もしくは「エラーがおきた!」ということを私たちに通知するために使用されます。

NVMeのすごいところは、これらのQueueのセットを、複数作成することができる、という点です。 これにより、複数のCPUコアに、Queueのセットを1つずつ持たせることなども可能になり、コア間でQueueのロックを取り合って性能が低下することを避けることができます。

さて、これらのQueueは、あくまでもコントローラーに対する「命令」と、その「結果」をやり取りするためのものです。 そのため、これらのQueueのエントリの大きさは、固定されています。では、例えばSSDに対して読み書きなどを行いたい場合、どのようにコントローラとの間でデータをやり取りするのでしょうか?

Physical Region Page Entry / List

ということで、コントローラーとの間で大きなデータをやり取りする際に用いられるのが、Physical Region Page(PRP)エントリおよびリストです。詳細は、仕様書の4.3を確認してください。

PRPエントリは、物理メモリの場所を指し示すポインタです。そのため、64bitの幅を持っています。 これが指すメモリ領域の大きさはコントローラのレジスタCC.MPSで定義します。つまり、PRPエントリひとつあたりが指すメモリ領域は固定です。最低でも、4KBの大きさがあります。(ページングみたいですね。)

コントローラに対して送るコマンドの構造体には、このPRPエントリを書ける場所が2箇所ありますが、4*2=8KBしか一度に転送できない、とかいうわけではありません。もっと大きなメモリ空間を指定したい場合に用いられる構造が、PRP Listです。

PRP List は、PRPエントリが指し示すメモリ領域に、PRPエントリを敷き詰めることで構成されます。 コマンドのPRPフィールドに、PRPエントリを指定するのか、PRPリストを指定するのかは、コマンドによって異なるようです。

ちなみに、PRP List / Entry以外にも、Scatter Gather Listとかいうかっこいい構造を使うこともできるのですが、今回の実装では使用していないので省略します。(知りたい人は4.4を読んでください。)

Namespace

NVMeは、他のストレージインターフェイスと異なり、ストレージ空間をNamespaceというパーティションのようなものに分割することができます。仕様書の1.4 Theory of Operationには、図解入りでその説明が書いてありますが、パーティションのようなものだと思っておいていただければOKです。 この、Namespaceを区別するIDを、Namespace ID (NSID)と呼びます。16bit幅の値をもち、0は常にInvalidで、0xffffffffはBroadcast Valueといい、全Namespaceを表すIDとして使われます。

話はいいからさっさとドライバを書け

こんな感じで、いろいろと情報をまとめたのはよかったのですが、セキュリティキャンプでCPUの作り方を教えたり、大学の学園祭の準備をしていたり、CODEBLUEでNOCをしたりしていたら、いつのまにか冬になってしまいました。

私は、Liva師が非常に進捗を重視する方であると知っていたので、きっと進捗を出していない私はもう見向きもされないのだろうと思い、ひっそりと生きていこうと思っていたのですが、そんなときにLiva師から神の手が差し伸べられたのです。

Liva師「NVMeのドライバ、まだやる気があるなら、ちょっとおもしろいことをしてみようと思うので、いかがでしょうか。」

Liva師「uioというものがあってね…。それを使うと、Linuxでドライバが簡単に書けちゃうんだ。」

なんですって!これは書くしかありませんね。

uioはすごいぞ

uioというのは、Userspace IOの略で、なんとユーザー空間からIOを制御できるというスグレモノのカーネルモジュールです。

ドライバを書いたことがある人ならわかると思いますが、ユーザーランドでドライバを書けるということは本当にすごいことです(技術的にではなく心理的に)。 だって、

  • バグってもカーネルパニックになりにくい(たいていSEGVで済む)
  • いろいろ面倒をみてくれる
  • printfができる!!!
  • gdbがつかえる!!!

こんなことがドライバの開発でできるんですよ!! 画面の色やキーボードのLEDを変化させたり、がんばってシリアルポートを制御したりして苦労してデバッグをする時代は終わったんです! これからは、いくらでもprintfしていいんです!gdbであの変数の値を見ちゃってもいいんです。最高でしょ?

というわけで、まずはuioの使い方を知るところから実装は始まりました。

uioの使用例 〜Liva師のXHCIドライバ〜

心優しき、かのLiva師は、uioを用いてXHCIを実装してくださいました。

github.com

これと、上のドキュメントを読むのがもっとも正確ですが、要点をかいつまんで説明しましょう。

まずは、コマンドでPCIバイスの制御をuioに移します。

  • sudo modprobe uio_pci_genericを実行して、uio_pci_genericカーネルモジュールをロードする。
  • 制御したいデバイスのベンダIDとデバイスIDをスペース区切りで/sys/bus/pci/drivers/uio_pci_generic/new_idに書き込む。
  • 制御したいデバイスを、デフォルトのドライバからunbindする(/sys/bus/pci/drivers/$(TARGET_KERNEL_DRIVER)/unbindにバス番号を書き込む)
  • 制御したいデバイスを、uio_pci_genericドライバにbindする(/sys/bus/pci/drivers/uio_pci_generic/bindにバス番号を書き込む)

これが終わると、なんとプログラムからPCIレジスタ空間がファイル(/sys/class/uio/uio0など。最後の数字は複数attachしている場合は違う。)として見えるようになります。

使い方の簡単な例がドキュメントにありますが、簡単にいうと

  • /dev/uio0をreadすると、デバイスからの割り込みがかかるまでブロックされる。
  • /sys/class/uio/uio0/device/configmmapすれば、PCIレジスタ空間がメモリにマップされて触れるようになる
  • /sys/class/uio/uio0/device/resource0mmapすれば、BAR0のレジスタ空間を触れる(resource1にすればBAR1も読める)

これだけです。ここまで触れれば、PCIバイスのドライバは簡単に書けるはずです。 どうですかuio! みなさんも使ってみたくなりましたか?快適なドライバ開発ライフを私たちと一緒に送りませんか?

で、進捗どうですか

さて、ただいま12/7の23:40です。

私がドライバを本格的に書き始めたのは、12/6の朝なのですが...。 さすがに、NVMeのSSDに対する読み書きを実行するまでは間に合いませんでした。ごめんなさい。 年内には完成させますので、お楽しみに!

f:id:hikalium:20171207234823p:plain

ちなみに現在は、Identifyというコマンドをコントローラに送信して、その返答の割り込みを処理するところまで書きました。しかも実機で動きます。というか逆にVirtualboxで動きません。原因究明中です…。

途中でuio_pci_genericがうまく動作しなくなったり、Virtualboxでは割り込みが再現しない(多分私のコードのミスだけど)といった問題に悩まされましたが、約1日でNVMeさんとお話をできるレベルにはなったわけです。実機だけorカーネルランドで開発していたら、1日ではここまで来れなかったでしょう。やはりuioはすごいですね!

ちなみに、uioで書かれたドライバを、Raphineという自作OSに容易に移植できるよう、クラスなどを整備する予定ですので、今みなさんがuioでドライバを書けば、Raphineに取り込まれるかもしれませんよ!ぜひやってみてください!

まとめ

  • NVMeのドライバは未完成
  • uioは便利だからみんな使おう!!!というかドライバ書こう!
  • やっぱり低レイヤは最高だね!