はじめに
前回の記事では、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 の情報も返せます。
こちらで最初に取得すべきと考えたのは、主に次の項目です。
NSZENCAPNUSE
この 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 Controller や Identify 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 を中心に読むことで、容量確認に必要な範囲から進められています。
次回は SMART や Error Log にどう広げていったかを書いていきます。