">

ほそぼそストレージ研究所

LinuxNVMe操作アプリケーションを作ってみた-2-ioctl

投稿日 2026.04.13 カテゴリ nvme レベル 中級者
nvme linux implementation

はじめに

前回の記事では、listshow が 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() です。
流れとしては、

  1. /dev/nvme0 のような char device を開く
  2. command 情報を構造体に詰める
  3. ioctl() で device に渡す
  4. 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_cmd
  • NVME_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 側の構造体に写している」と捉えると見やすくなります。

今回の実装でよく触ったのは、主に次のフィールドです。

  • opcode
  • nsid
  • addr
  • data_len
  • cdw10
  • cdw11
  • cdw12
  • cdw13
  • cdw14
  • cdw15
  • timeout_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 = Identify
  • nsid = 0 あるいは対象 NSID
  • addr = 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=0x06CNS

のように、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 を始めたのかを書いてみたいです。