はじめに
前回の記事では、NVMe 学習用 CLI の v0 全体像をまとめました。
今回はその中でも、最初に作った list と show に絞って書いてみます。
この2つは、後で作った identify や smart、errinfo とはかなり性質が違います。
identify 以降は NVMe command を自分で投げて情報を取りに行きますが、list と show はそうではありません。
list と show がやっているのは、Linux がすでに持っている NVMe デバイス情報を拾って整形することです。
実装を進める中で、入口としての list/show は他の command とはかなり別物として扱う必要があると分かってきました。
なぜ最初に list/show から作ったのか
今回の v0 は、最初から identify や smart に入ったわけではなく、まず list と show から作りました。
理由はかなり単純で、
- 対象NVMeを見つけるために、まずは Linux から見えているデバイスを把握しておく必要がある
- 対象の serial や model を確認する入口があると便利
- いきなり低レイヤーの NVMe command に入る前に、CLI の流れを作りたかった
- boot / system デバイスを触るので、最初はできるだけ安全側に寄せたかった
からです。
今回の環境ではM.2 SSDをシステム用以外に挿すことができなかったので、boot / system deviceを触らざるを得ないです。
そのため、NVMeを直接操作するpassthroughやioctlに進むのはリスクが高いと考えました。
また、複数台用意できる環境でも、どのデバイスが存在して自分が触るデバイスがなにかを確認するのは大事なことです。
list/show は NVMe command を投げていない
list と show は、NVMe の Identify や Get Log Page などのコマンドを投げているわけではありません。
Linux がすでに認識している block device と、その周辺情報を集めて表示しています。
つまり、ここで見ているのは「NVMe device が何を返すか」ではなく、Linux が NVMe device をどう見ているか です。
Linux system上のNVMeデバイスの仕組み
今回の list/show 実装では、主に次の情報源を見ています。
/sys/class/block//sys/class/block/<device>/device//proc/self/mounts/dev/nvme*
流れとしてはかなり素直で、まず /sys/class/block/ を走査し、nvme で始まる block device を探しています。実際に見てみましょう
$ ls /sys/class/block | grep nvme
nvme0n1
nvme0n1p1
nvme0n1p2
$ ls /sys/class/block/nvme0n1
alignment_offset capability dev discard_alignment eui events_async ext_range holders integrity mq nsid nvme0n1p1 partscan power range ro slaves subsystem uevent wwid
bdi csi device diskseq events events_poll_msecs hidden inflight metadata_bytes nguid nuse nvme0n1p2 passthru_err_log_enabled queue removable size stat trace uuid
$ ls /sys/class/block/nvme0n1/device
address cntlid cntrltype dctype dev device firmware_rev hwmon1 kato model ng0n1 numa_node nvme0n1 passthru_err_log_enabled power queue_count rescan_controller reset_controller serial sqsize state subsysnqn subsystem transport uevent
sizedevice/modeldevice/serialdevice/firmware_rev
のような情報を読んで、DeviceInfo に詰めています。
ファイルは以下のような情報が入っています。情報自体はシンプルですね。
$ cat /sys/class/block/nvme0n1/size
1000215216
$ cat /sys/class/block/nvme0n1/device/model
WPBSNM8-512GTP
*) 補足
このmodelはWodposit社製の512GB PCIe Gen3.0 x 4 NVMeデバイスです。
Gen3.0 x 4としては性能は優秀な方のようです。
Wodpositなんて会社初めて聞きました。中華製のデバイスですね。Geekomと提携していて、今回のMiniPCももれなくそれに該当します。
実際のコードでは
以下のように実装しています。
std::string block_dir="/sys/class/block/";
for (const auto& entry : std::filesystem::directory_iterator(block_dir))
if (std::filesystem::exists(entry.path() / "partition")) continue;
const auto name = entry.path().filename().string();
if (name.rfind("nvme", 0) != 0) continue;
DeviceInfo info;
std::string tgt_dir = block_dir + name + "/";
info.char_dev_path = "/dev/"+NormalizeNvmeController(name);
info.block_dev_path = "/dev/"+name;
info.capacity_bytes = std::stoull(ReadTextFile(tgt_dir + "size")) * 512;
tgt_dir += "device/";
info.model = TrimRightAsciiWhitespace(ReadTextFile(tgt_dir + "model"));
info.serial = TrimRightAsciiWhitespace(ReadTextFile(tgt_dir + "serial"));
info.firmware = TrimRightAsciiWhitespace(ReadTextFile(tgt_dir + "firmware_rev"));
info.bus_type = "NVMe";
}
ここでは、C++のSTLのfilesystemを使って、/sys/class/block下のディレクトリを順々に見ています。
いろいろなデバイスがあるので、以下の要素で絞り込みます。
- ディレクトリ名に
nvmeがある - 該当ディレクトリ下に
partitionファイルが存在しない。
後述しますが、partitionファイルがあるblockは今回の操作の対象外です。
さらに /proc/self/mounts を見て、
- そのデバイスが
/を支えているか /bootや/boot/efiに使われているか
を確認し、is_system_disk や is_boot_disk の判定に使っています。
私の環境では以下のようになっています。
nvme0n1p1がboot向け、nvme0n1p2がシステム向けの領域です。
$ cat /proc/self/mounts | grep nvme
/dev/nvme0n1p2 / ext4 rw,relatime 0 0
/dev/nvme0n1p1 /boot/efi vfat rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro 0 0
私の環境では1枚しかnvmeを挿せないので、何も考えずに同じデバイスを使っていますが、当初の予定ではここで使われているデバイスはis_allowed = falseというフラグを立てて、アクセス禁止にする、という予定でした。
今回の実装ではこのように書いています。
std::ifstream ifs("/proc/self/mounts");
std::string line;
while(std::getline(ifs, line)) {
std::istringstream iss(line);
std::string source, mount_point, fstype;
if (!(iss >> source >> mount_point >> fstype)) {
continue;
}
if (mount_point == "/") {
if (NormalizeNvmeParent(source) == info.block_dev_path) {
info.is_system_disk = true;
info.flags.push_back("system");
}
}
if (mount_point == "/boot" || mount_point == "/boot/efi") {
if (NormalizeNvmeParent(source) == info.block_dev_path) {
info.is_boot_disk = true;
info.flags.push_back("boot");
}
}
}
/proc/self/mountsの中身はたくさんあってややこしいのですが、/, /boot/efiを探して、その時のデバイスを特定するだけであれば、さほど難しくないです。
nvme0 と nvme0n1 と nvme0n1p1
Linux 上では、似たような名前のデバイスがいくつも見えます。
nvme0nvme0n1nvme0n1p1
今回の整理としては、ざっくり次のように考えています。
nvme0- controller 側の char device
- 今回のコードでは
char_dev_path nvme0n1- namespace に対応する block device
- 今回のコードでは
block_dev_path nvme0n1p1- その block device 上の partition
list/show で主に見たいのは、「評価対象としてのストレージ本体はどれか」です。
そのため、partition そのものを一覧に混ぜず、走査時には partition ファイルがあるものをスキップしています。
これにより、
- 何を評価対象にすべきか
/bootや/がどの NVMe block device にぶら下がっているか
を見やすくしています。
partition を親デバイスへ戻す
/proc/self/mounts を見ると、/ や /boot にマウントされている実体は nvme0n1p1 のような partition 名で出てくることがあります。
ただ、show で知りたいのは partition 単位ではなく、「その partition が属している NVMe block device は何か」です。
そのため、実装では
nvme0n1p1→nvme0n1
のように親 block device へ正規化する処理を入れています。
これによって、
/bootがnvme0n1p1/がnvme0n1p2
のようなケースでも、
- 元の NVMe block device は
nvme0n1
と判断できるようになります。
list と show の役割の違い
list と show は、同じ情報源を見ていても役割が少し違います。
list
list は候補デバイスをざっと眺める command です。
主に見るのは、
- device path
- model
- serial
だけで、あまり情報を出しすぎないようにしています。
ここでやりたいのは、
どの NVMe device を次に詳しく見るべきか決めること
です。
show
show は、list で見つけた serial を頼りに 1 台を詳しく見る command です。
こちらではさらに、
- firmware
- capacity
- bus type
- is system disk
- is boot disk
- is allowed
- flags
- reason denied
まで表示します。
ここでの is_allowed や flags は、単なる飾りではなく「このデバイスを次にどう扱うか」を判断するための補助情報です。
is_system_diskis_boot_diskflags
のような情報を先に見ておくことで、対象が boot / system に関わるデバイスかどうかを確認できます。
今回の v0 では write 系 command を入れていないので、is_allowed は厳密な実行制御というより、今後の安全判定に向けたフックの意味合いが強いです。
それでも、show の時点で安全に関わる情報をまとめて見られるようにしておくことには意味があると考えています。
つまり show は、
- 本当に意図したデバイスか
- 触ってよさそうか
- 次に
identifyへ進んでよいか
を確認するための command です。
list/show の次に、NVMe command の世界へ進む
list/show を実装していると、この層で扱っているのは Linux がすでに知っている情報を整理して見せること だと分かってきます。
たとえば、
- SMART Log の中身
- Error Information Log
- Identify Controller の各 field
のような情報は、Linux がそのまま見せてくれるわけではありません。
そこから先は、こちらが NVMe command を直接投げて、自分で buffer を読み、spec に従って解釈する段階に入っていきます。
つまり、
list/showでは Linux が持っている情報を確認するidentify以降では NVMe device に直接 command を打って情報を取りに行く
という流れになります。
この境界が見えるようになると、
list/showは Linux の観察identify/smart/errinfoは NVMe protocol の観察
という整理がしやすくなります。
まとめ
list と show は、NVMe 学習用 CLI の入口としてかなり良い command でした。
理由は、
- まず Linux が見ているデバイス情報を整理できる
- 安全に read-only で確認できる
char device/block device/partitionの違いを理解できる- 次に
identifyやsmartへ進む前の足場になる
からです。
この段階を踏んだからこそ、後で Identify や Get Log Page に進んだ時も、
- 何を Linux から拾っているのか
- 何を NVMe command で取りにいっているのか
を混同せずに済んでいます。
次はこの続きとして、identify 以降でついに NVMe device に直接 command を打ち、何を受け取っているのかを別記事でまとめたいです。