">

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

LinuxNVMe操作アプリケーションを作ってみた-6-DeviceClass設計

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

はじめに

ここまでで、

  • Linux から情報を拾う list / show
  • ioctl で NVMe command を打つ入口
  • Identify
  • SMART Log
  • Error Information Log

まで実装してきました。

機能が増えてくると、次に気になってくるのが「どこに何を書けばいいのか」です。

特に責務の分割について、途中で何度も考え直す羽目になりました。

今回は、v0 を作りながら最終的にどう分けたかを整理します。

最初は「とりあえず動けばいい」で始まる

実装の最初の段階では、

  • command を呼ぶ
  • device を探す
  • 必要な情報を取る
  • そのまま表示する

という流れを 1 箇所にまとめがちでした。

これは最初の 1 本を動かすだけなら自然です。
ただ、Identify の次に SMART、さらに Error Log へ進んでいくと、同じような処理が何度も出てきます。

例えば、

  • serial で対象 device を探す
  • mock / real を切り替える
  • access 権限を確認する
  • IdentifyGet Log Page を呼ぶ
  • 結果を表示する

のような流れです。

この段階になると、実装側が増えてきます。そのため、なるべく各動作を共通化したくなりますし、その処理はどこでまとめるか、を考えたくなってきます。

v0での最終的な構成

v0では以下の構成になりました。

classDiagram
    class main_cpp {
        +main(argc, argv)
        +DescribeError(err)
        +ToExitCode(err)
    }

    class cli_cpp {
        +run_app(argc, argv)
        -ParseOptions(argc, argv)
        -ParseSourceValue(...)
        -ParseCountvalue(...)
        -PrintNoValueError(cmd, name)
        -PrintInvalidArgument(cmd, ex_args)
        -PrintCommandUsage(cmd)
    }

    class identify_cpp {
        +RunIdentify(serial, src)
    }

    class get_log_page_cpp {
        +RunGetErrorLog(serial, src, err_count)
        +RunGetSmart(serial, src)
    }

    class NvmeDevice {
        <<interface>>
        +Info() const
        +IdentifyController(out) const
        +IdentifyNamespace(out) const
        +GetErrorLog(...)
        +GetSmart(...)
    }

    class MockNvmeDevice {
        +Info() const
        +IdentifyController(out) const
        +IdentifyNamespace(out) const
        +GetErrorLog(...)
        +GetSmart(...)
    }

    class LinuxNvmeDevice {
        +Info() const
        +IdentifyController(out) const
        +IdentifyNamespace(out) const
        +GetErrorLog(...)
        +GetSmart(...)
    }

    class DeviceInfo {
        +serial
        +is_allowed
        +block_dev_path
        +char_dev_path
    }

    class IdentifyControllerInfo
    class IdentifyNamespaceInfo
    class LogErrorInfo
    class LogSmart

    main_cpp --> cli_cpp : call
    cli_cpp --> identify_cpp : identify
    cli_cpp --> get_log_page_cpp : errinfo / smart

    identify_cpp --> NvmeDevice : use
    get_log_page_cpp --> NvmeDevice : use

    NvmeDevice <|-- MockNvmeDevice
    NvmeDevice <|-- LinuxNvmeDevice
    NvmeDevice --> DeviceInfo : returns

    identify_cpp --> IdentifyControllerInfo : output
    identify_cpp --> IdentifyNamespaceInfo : output
    get_log_page_cpp --> LogErrorInfo : output
    get_log_page_cpp --> LogSmart : output

それぞれのクラスの概要について、概要だけですが解説していきます。

NvmeDevice は device access interface に寄せた

最終的に、NvmeDeviceNVMe device に対する操作 interface として扱う方針にしました。

つまり、NvmeDevice に持たせるのは、

  • IdentifyController(...)
  • IdentifyNamespace(...)
  • GetLogPage_Smart(...)
  • GetLogPage_ErrorInfo(...)

のような、device に対する操作そのものです。

逆に、ここへは入れないようにしたものもあります。

  • command line 引数の解釈
  • serial で対象を探す処理
  • どの情報を画面にどう表示するか

このようにしておくと、NvmeDevice は「device に触る人」であり、command の都合までは持たない形になります。

RunXXX() は orchestration に寄せた

では、serial で対象 device を探したり、表示したりする処理はどこに置くかというと、RunXXX() 側です。

RunIdentify()RunGetSmart() のような関数には、

  • LoadNvmeDevices() で device 一覧を取る
  • serial で対象を探す
  • access 可能かを確認する
  • NvmeDevice のメソッドを呼ぶ
  • 取得結果を表示する

という流れを持たせています。

ここでは device access そのものではなく、command を実行する流れ全体を組み立てること を責務としています。

この分け方にすると、

  • NvmeDevice
    device access
  • RunXXX()
    command orchestration

という役割の違いがかなりはっきりします。

mock / real の差分は LoadNvmeDevices() の下に閉じ込めた

もうひとつ大きかったのが、mock と real をどこで吸収するかです。

ここは最終的に、

  • LoadNvmeDevices(source)

std::unique_ptr<NvmeDevice> の配列を返す形にしました。

つまり、command 側から見ると、

  • 返ってくるのは NvmeDevice
  • それが mock なのか real なのかは知らなくていい

という状態です。

これにより、RunXXX() 側は

  • source に応じて読み込み方法を切り替える

のではなく、

  • source に応じて作られた NvmeDevice を使う

だけで済むようになりました。

結果データは *_info.h に寄せた

途中でかなり迷ったのが、取得結果の置き場です。

最終的には、

  • IdentifyControllerInfo
  • IdentifyNamespaceInfo
  • LogSmart
  • LogErrorInfo

のような結果型を *_info.h に寄せました。

これは、「device access interface」と「結果データ」に責務自体を分けたかったからです。

NvmeDevice 自体が大量の field を持つのではなく、

  • NvmeDevice は取ってくる
  • Info は結果を保持する

という形にした方が、後で SMARTError Log を増やした時にも整理しやすくなりました。

また、IdentifySMART では raw bytes の decode も Info::LoadFromBytes(...) に寄せています。

これは、

  • spec の byte layout を解釈する責務

を 1 箇所に寄せるためです。

表示は PrintXxxInfo(...) 側に寄せた

かなり悩んで、結局これだという解が見いだせなかったのが、結果型自身に表示メソッドを持たせるかどうかでした。

最終的には、

  • PrintIdentifyControllerInfo(...)
  • PrintIdentifyNamespaceInfo(...)
  • PrintSmart(...)
  • PrintErrorLog(...)

のような free function に寄せています。

今回は、表示は device access でも decode でもなく、command 側の責務だと考えて、この形になりました。

例えば SMART では、

  • 温度を KC で出す
  • % を付ける
  • Data Units に補助表示を付ける
  • Error Count == 0 の entry を見せない

のように、表示の判断がかなり多く入ります。

これを data 型の中に入れるより、表示関数として外に出した方が後で直しやすい形になりました。

振り返り: クラス図を書くと次の分割候補も見えてくる

今回クラス図を書いてみて、v0 としては十分整理できた一方で、次に分けたくなる責務も見えてきました。

特に気になったのは次の 2 点です。

  • DeviceInfoNvmeDevice interface からさらに独立してよいのではないか
  • serial で対象 device を引く処理と、RunXXX() で command を実行する処理は分けてもよいのではないか

今の v0 では、NvmeDevice::Info() を通して serialpath を返しています。
ただ、NvmeDevice 自体は本来 device access interface なので、

  • IdentifyController(...)
  • IdentifyNamespace(...)
  • GetLogPage_Smart(...)
  • GetLogPage_ErrorInfo(...)

のような操作だけを持っていても成立しそうです。

同じように、RunXXX() も今は

  • device 一覧の取得
  • serial での対象探索
  • access check
  • command 実行
  • 表示

までまとめて持っていますが、今後さらに command が増えるなら「対象 device を解決する層」を別に置いた方がすっきりしそうです。

v0 ではここまで踏み込まず、まずは動く構成を整理するところまでに留めました。
ただ、v1 に向けて設計を見直すなら、こうした分割は最初に考えておきたい論点だと感じています。

これに関しては、表示メソッドもデータ型の責務として持たせたほうがいいのか、いまだに解を見いだせないです。

open -> ioctl -> close は executor に切り出した

さらに実装が進んでから整理したのが、admin command を投げる骨格です。

IdentifySMARTError Log も、

  • char device を開く
  • nvme_admin_cmd を埋める
  • ioctl を呼ぶ
  • 閉じる

という骨格自体は同じです。

そこで、この共通部分は NvmeAdminExecutor に切り出しました。

一方で、

  • opcode
  • nsid
  • cdw10-15
  • buffer size

のような command 固有の意味は、それぞれの実装側に残しています。

つまり、

  • executor は「どう送るか」
  • feature 側は「何を送るか」

という分け方です。

v0 ではここまでに留めた

ここまで整理した一方で、v0 の段階ではあえて踏み込まなかったこともあります。

  • CLI error の全面整理
  • error code の細分化
  • 全 command を Identify 前提にする初期シーケンス
  • lower layer の異常系テスト

このあたりは、必要性は見えていても、v0 でやると影響範囲が大きすぎると判断しました。

そのため、v0 では

  • 実際に困った責務分割だけ解く
  • 大きな再設計は Followups.md に逃がす

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

まとめ

今回の v0 で落ち着いた責務分割をまとめると、次のようになります。

  • NvmeDevice
    device access interface
  • RunXXX()
    command orchestration
  • LoadNvmeDevices()
    mock / real の差分吸収
  • *_info.h
    取得結果の保持と decode
  • PrintXxxInfo(...)
    表示
  • NvmeAdminExecutor
    open -> ioctl -> close の共通骨格

最初からこの形を決めていたわけではありません。
実際には IdentifySMARTError Log を増やしながら、何度も責務を見直してこの形に落ち着きました。

個人的には、設計は最初に全部決め切るものというより、機能を増やした時に同じつらさが何度も出る場所を切り出していくものだと実感しています。