はじめに
前回の記事では、list と show が Linux の system 上にすでに見えている情報を拾い、整理して表示していることを書きました。
その次に進むと、いよいよ NVMe device に対して command を直接打っていくことになります。
NVMeに対して実際に操作を行う場合、最初に理解するべき話は
- Linux からどうやって NVMe command を送るのか
というかなり低いレイヤーの話です。
今回は、Linux から NVMe admin command を直接打つ入口 に絞って整理してみます。
Linux から NVMe command を打つ時の見取り図
list/show では、Linux がすでに持っている情報を sysfs や /proc/self/mounts から拾っていました。
一方で identify 以降では、こちらから NVMe device に command を送り、その返り値を自分で受け取って解釈する必要があります。
この時にまず必要になるのが、Linux 側の file descriptor と ioctl() です。
流れとしては、
/dev/nvme0のような char device を開く- command 情報を構造体に詰める
ioctl()で device に渡す- file descriptor を閉じる
という形になります。
つまり、ここから先は「Linux が持っている情報を読む」のではなく、Linux の file descriptor を通して NVMe device を制御する 段階に入っていきます。
最初に見た include
最初に必要になったのは、NVMe command を Linux から投げるための header です。
今回の実装で主に見たのは次のあたりです。
<linux/nvme_ioctl.h><sys/ioctl.h><fcntl.h><unistd.h>
役割はだいたい次の通りです。
<linux/nvme_ioctl.h>nvme_admin_cmdNVME_IOCTL_ADMIN_CMD<sys/ioctl.h>ioctl()<fcntl.h>open()<unistd.h>close()
nvme_admin_cmd に何を入れるのか
nvme_admin_cmd を最初に見た時は、cdw10 から cdw15 まで並んでいて少し身構えました。
ただ、実際に使い始めると「NVMe spec の command dword をそのまま Linux 側の構造体に写している」と捉えると見やすくなります。
今回の実装でよく触ったのは、主に次のフィールドです。
opcodensidaddrdata_lencdw10cdw11cdw12cdw13cdw14cdw15timeout_ms
ざっくり役割を書くと、
opcode- 何の command か
nsid- 対象 namespace
addr- 受け渡し buffer のアドレス
data_len- buffer サイズ
cdw10-15- command ごとの追加パラメータ
timeout_ms- timeout
という対応になります。
実装: file descriptor を開いて、制御して、閉じる
理論として整理するとシンプルですが、実装でもやっていることはかなりそのままです。
- char device を
open() nvme_admin_cmd相当の情報を埋めるioctl(fd, NVME_IOCTL_ADMIN_CMD, &cmd)を呼ぶ- 終わったら
close()
この「開いて、制御して、閉じる」という流れは、NVMe に限らず Linux の file descriptor ベースの I/O を考える時の基本形でもあります。
今回の実装でも、まず /dev/nvme0 のような char device を open() し、その file descriptor を ioctl() に渡しています。
ioctl() の第3引数には nvme_admin_cmd を渡し、device 側に command の内容と data buffer をまとめて渡す形になります。
ここで大事なのは、開いているのが /dev/nvme0 のような controller 側の char device だという点です。
list/show でよく見ていた /dev/nvme0n1 は namespace に対応する block device ですが、NVMe admin command を直接投げる時に使う入口はそちらではありません。
つまり、ここで扱っている file descriptor は
- filesystem 用の block device を読むためのもの
ではなく、
- NVMe controller に対して command を渡すための char device を操作するもの
ということになります。
最小限の形だけ抜き出すと、Linux 側の流れはだいたい次のようになります。
std::array<std::uint8_t, 4096> buffer{};
nvme_admin_cmd cmd{};
cmd.opcode = 0x06; // Identify
cmd.nsid = 0;
cmd.addr = reinterpret_cast<__u64>(buffer.data());
cmd.data_len = buffer.size();
cmd.cdw10 = 0x01; // CNS = Identify Controller
int fd = open("/dev/nvme0", O_RDONLY);
ioctl(fd, NVME_IOCTL_ADMIN_CMD, &cmd);
close(fd);
この O_RDONLY は、「今は read-only 系の command を扱っているので、まずは読み取り専用で開いている」という意味です。
この段階では block device の中身を read しているわけではなく、あくまで char device に対して command を渡す入口を開いています。
もちろん実際にはエラー処理が必要ですが、まずは
- buffer を用意する
nvme_admin_cmdを埋める- file descriptor を通して
ioctl()する
という骨格が見えていれば十分です。
実際の command と構造体の対応
この構造体が分かりにくい理由の一つは、field 名だけ見ても意味が分からないことです。
ただ、まず Identify を実装しながら見ると、
opcodeは command 固有cdw10-15は spec の command-specific dword
という対応がかなり素直に見えてきます。
たとえば Identify なら、
opcode = Identifynsid = 0あるいは対象 NSIDaddr = buffer.data()data_len = buffer.size()cdw10 = CNS
という形になります。
たとえば、かなり短く書くとこうなります。
// Identify Controller
nvme_admin_cmd identify{};
identify.opcode = 0x06;
identify.nsid = 0;
identify.addr = reinterpret_cast<__u64>(buffer.data());
identify.data_len = buffer.size();
identify.cdw10 = 0x01; // CNS = Controller
ここで見たいのは細かいビット操作そのものより、
Identifyではopcode=0x06とCNS
のように、command ごとに意味を持つ field が変わる という点です。
つまり、nvme_admin_cmd は Linux 独自の難しい仕組みというより、
NVMe spec で見ている command 情報を Linux から渡すための器
として見ると理解しやすいです。
感想
最初は ioctl という言葉に少し身構えましたが、流れだけ見ると意外と単純です。
難しかったのは Linux API そのものより、
- どの command に何を入れるか
cdw10-15の意味をどう読むか- 返ってきた buffer をどこまで読むか
の方です。
つまり、入口として重要だったのは ioctl() という関数名そのものよりも、Linux の file descriptor と NVMe spec の command 定義が、nvme_admin_cmd でどうつながるかを理解することでしょう。
まとめ
list/show の次に進んで最初に向き合ったのは、Identify の field よりも、むしろ
- Linux からどうやって NVMe command を送るのか
nvme_admin_cmdに何を入れるのか
という入口の部分でした。
ここが見えてくると、Linux が持っている情報を読む段階から、file descriptor を通して NVMe device に直接 command を打つ段階へ進んだことがかなり実感しやすくなります。
次はこの続きとして、実際に Identify を打って何を受け取り、どこから decode を始めたのかを書いてみたいです。