">

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

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

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

はじめに

前回の記事では、Linux から ioctl() を使って NVMe admin command を直接打つ入口を整理しました。

次は、その command の返り値をどう読むかです。

NVMe操作でまずすることといえばIdentify、そう言ってもいいほど最初にやるべきことでしょう。
今回はIdentifyのうち、Identify Controller, Identify Namespaceを取得します。

まずは何を識別できれば十分か

Identify Controller の data structure は 4096 byte あります。
ただ、4096 byteすべてを解釈するのはあまりにも大変ですし、ほとんど今は不要な情報です。

以下はIdentify Controllerの先頭部分の抜粋です。
今回はそのうち、先頭[0:71]Byte分のみ取得することにします。
必要に応じてすぐに追加できるようにはしておきます。

[ 01: 00] : M : PCI Vendor ID (VID)
[ 03: 02] : M : PCI Subsystem Vendor ID (SSVID)
[ 23: 04] : M : Serial Number (SN)
[ 63: 24] : M : Model Number (MN)
[ 71: 64] : M : Firmware Revision (FR)
[ 72]     : M : Recommended Arbitration Burst (RAB)
[ 75: 73] : M : IEEE OUI Identifier (IEEE)
[ 76]     : O : Controller Multi-Path IO and Namespace Sharing Capabilities (CMIC)
[ 77]     : M : Maximum Data Transfer Size (MDTS)
[ 79: 78] : M : Controller ID (CNTLID)
[ 83: 80] : M : Version (VER)
[ 87: 84] : M : RTD3 Resume Latency (RTD3R)
[ 91: 88] : M : RTD3 Entry Latency (RTD3E)
[ 95: 92] : M : Optional Asynchronous Events Supported (OAES)
...

ちなみに、上記は私が下調べのときにまとめたものです。真ん中にあるM/OはMandatory (実装必須) / Optional (実装任意)の意味です。

これらが取れるだけでも、

  • いま見ている device が本当に意図したものか
  • Linux 側で見えていた情報と、device から返ってきた情報が対応しているか

を確認しやすくなります。

今後必要になる情報は増えていくと思いますが、ひとまずはFWまで取得できていれば十分でしょう。

Namespace 側で最初に必要だった情報

Identify では Controller だけでなく Namespace の情報も返せます。
こちらで最初に取得すべきと考えたのは、主に次の項目です。

  • NSZE
  • NCAP
  • NUSE

この 3 つが取れると、

  • namespace の総サイズ
  • 利用可能容量
  • 実使用量

をまず把握できます。

Controller 側が「どの device か」を見る入口だとすると、Namespace 側は「その device をどんな容量のストレージとして扱うか」を見る入口です。
ちなみに、NamespaceはM.2側で定義されているもので、Consumer向けSSDでは大体1しかないです。

まずは offset を決めて decode する

データの取り方自体は前回説明しましたので、今回は実際にIdentifyを取得してからどう管理をするかの実装部分を見てみます。

まずは、実装部分を見てみます。

void IdentifyControllerInfo::LoadFromBytes(std::span<const std::uint8_t, 512> data) {
    data_.  vid    = utility::byte_to_le_u16(&data[0]);
    data_.ssvid    = utility::byte_to_le_u16(&data[2]);
    data_.serial   = std::string(data.begin() +  4, data.begin() + 24);
    data_.model    = std::string(data.begin() + 24, data.begin() + 63);
    data_.firmware = std::string(data.begin() + 64, data.begin() + 72);
}

この実装に際して瞑想したのですが(詳細は別の記事で)、最終的にはクラスにLoadメソッドをもたせ、そこでデータを切り取る、という方向になりました。
IdentifyControllerInfoというクラスは以下の構成をしています。

struct IdentifyControllerInfoData {
    std::uint16_t vid = 0;    // [001:000]
    std::uint16_t ssvid = 0;  // [003:002]
    std::string   serial;     // [023:004]
    std::string   model;      // [063:024]
    std::string   firmware;   // [071:064]
};

class IdentifyControllerInfo {
private:
    IdentifyControllerInfoData data_;
public:
    void LoadFromBytes(std::span<const std::uint8_t, 512> data);
    void LoadFromData(const IdentifyControllerInfoData& data) {this->data_ = data;}
    std::uint16_t Get_Vid     () const {return data_.vid     ;}
    std::uint16_t Get_Ssvid   () const {return data_.ssvid   ;}
    std::string   Get_Serial  () const {return data_.serial  ;}
    std::string   Get_Model   () const {return data_.model   ;}
    std::string   Get_Firmware() const {return data_.firmware;}
};

データ自体はクラス内部で保持するように考えていましたが、構造体はどこでも使えるようにしたい(mockを作るときに便利なため)と考えた結果、データ構造体だけ外に出すような形になりました。
情報を追加する際は、基本的にはLoadFromBytes()と、IdentifyControllerDataクラス、そしてGet()メソッドを追加する、という3手間が発生します。

uint16_tなどはそのままだと長ったらしいbit演算を行わなければならないので、utility namespaceを作成し、そこで用意しています。

結果の確認はPrintIdentifyControllerInfo()関数を別で作っています。そちらは今回は割愛します。

Namespaceも同様に設定します。
namespaceはnsze/ncap/nuseだけでいまは十分ですが、あまりにも寂しいので、少々情報量を追加しています。

data_.nsze   = utility::byte_to_le_u64(&data[ 0]);
data_.ncap   = utility::byte_to_le_u64(&data[ 8]);
data_.nuse   = utility::byte_to_le_u64(&data[16]);
data_.nsfeat = data[24];
data_.nlbaf  = data[25];
data_.flbas  = data[26];
data_.mc     = data[27];
data_.dpc    = data[28];
data_.dps    = data[29];

実装: 最初から全部読まなかった理由

Identify ControllerIdentify Namespace には、この他にもたくさんの field があります。

ただ、最初から全部 struct に載せて全部 decode しようとすると、

  • spec を読む量が一気に増える
  • 今まだ使わない項目まで抱えることになる
  • どこまで読めていて、どこから未着手なのかが曖昧になる

という問題がありました。

そのため今回は、

  • まず必要な項目だけ追加する
  • 意味が理解できたものだけ decode する
  • 後で必要になったら項目を増やす

という進め方にしています。

まとめ

Identify で返ってくる 4096 byte の buffer は、一見すると大きくて身構えます。

ただ、最初の段階で重要だったのは 4096 byte 全体を読むことではなく、

  • まずどの項目を読むか決める
  • offset と型を対応づける
  • little endian の整数と文字列を分けて扱う

という整理でした。

今回の実装では、まず VID / SSVID / serial / model / firmware から始めることで、
Identify Controller の decode を小さく進めることができました。

また、Namespace 側も nsze / ncap / nuse を中心に読むことで、容量確認に必要な範囲から進められています。

次回は SMARTError Log にどう広げていったかを書いていきます。