FreeBSDでNVMeディスクを低レベルフォーマットする

#動機
他の用途に使っていたNVMeなSSDを初期化したい。

#前提
* FreeBSD 11.1-RELEASE-p10

#できること
* データ消去
* 性能回復（いわゆるSecure Erase）
* セクタサイズ変更

#予備知識
##namespace
NVMeデバイスは中身を特性の異なる複数の領域「namespace（名前空間）」に区分することができる（できないものもあるがその場合でもnamespaceが1つある）。感覚的にはディスクをパーティションに区分するのに近いのだが、規格書を斜め読みするともっと複雑なことを実現する枠組みらしい。正直よくわからないのだけど、少なくとも1つのデバイス中に512バイト/セクタな領域と4Kバイト/セクタな領域を作ったりできるようだ。だがここでは単に、NVMeデバイスには必ずnamespaceというものがあって、それが**HDDとか旧来のSSDとかの「ディスク」に対応している**、というだけで済ませる。

##デバイスドライバ
FreeBSDの場合、NVMeデバイスに関係するデバイスドライバが**[nvme(4)][]と[nvd(4)][]の2種類ある**。大雑把に区別すると、NVMeデバイスに固有の管理操作の対象がnvme(4)、一般的なストレージとしての操作の対象がnvd(4)となる。たとえば低レベルフォーマットはnvme(4)、ファイルシステム作成はnvd(4)に対して行う。またnamespaceにはnvme(4)のサブデバイスが対応しており、それぞれのnamespaceに対応するnvd(4)が1つずつ存在する。

[nvme(4)]:https://www.freebsd.org/cgi/man.cgi?query=nvme&sektion=4
[nvd(4)]:https://www.freebsd.org/cgi/man.cgi?query=nvd&sektion=4

|デバイス|ドライバ|デバイスノードの例|
|:------|:------|:---------------|
|NVMeデバイス|nvme(4)|/dev/nvme0|
|namespace||/dev/nvme0ns1|
|ディスク|nvd(4)|/dev/nvd0|
|パーティション||/dev/nvd0p1|

なお-CURRENTではSCSIやATAと同様に[CAM(4)][]フレームワークの元でnda(4)を通してアクセスするよう[開発が進められている](https://svnweb.freebsd.org/base/head/sys/cam/nvme/)。

[CAM(4)]:https://www.freebsd.org/cgi/man.cgi?query=CAM&sektion=4

##低レベルフォーマット
物理フォーマットと呼んだりもする。HDDではさほど出番がなかったが、昔はSCSIドライブに対して低レベルフォーマットを実施した記憶がある。NVMeデバイスの場合、規格上はセクタサイズを変更することもできるため、フロッピーディスク時代を彷彿とさせるものがある。

#nvmecontrol(8)
FreeBSDでnvme(4)に対する管理操作を行うのが[nvmecontrol(8)][]というユーティリティーである。システムに認識されているNVMeデバイスやnamespaceの一覧や、その状態を表示したり、ファームウェアを書き換えたりと、要するに**NVMeの規格に定められたコマンドを発行するツール**である。意外なところではカーネルレベルでデバイスに対する読み書きの性能テストを実施する機能がある（これは規格にはない）。

[nvmecontrol(8)]:https://www.freebsd.org/cgi/man.cgi?query=nvmecontrol&sektion=8

低レベルフォーマットもNVMeの規格にあるので、もちろんnvmecontrol(8)を使って実施できる。と思いきや、現行のFreeBSDに含まれているnvmecontrol(8)には実装されていない。開発レポジトリに対応版が存在するのだが、おそらくもうじきリリースされる11.2-RELEASEには含まれず、11.3以降で対応ということになるはず。どうせコマンド発行するだけだから、自分でコンパイルすればそのまま使えるに違いない。

なお-CURRENTでは[CAM(4)][]の元でnda(4)を通してアクセスするよう開発が進められているため、ここで説明するやり方はいずれ[camcontrol(8)][]を利用するようになるのかもしれない。

[camcontrol(8)]:https://www.freebsd.org/cgi/man.cgi?query=camcontrol&sektion=8

##コンパイル
SVNから11-STABLEの先頭を持ってきて、それに-CURRENTのコードをマージする。nvmecontrol(8)のソースは[sbin/nvmecontrol](https://svnweb.freebsd.org/base/head/sbin/nvmecontrol)にあるが、カーネルソースに含まれるヘッダファイルやサブルーチンも必要なので[sys/dev/nvme](https://svnweb.freebsd.org/base/head/sys/dev/nvme)も一緒にチェックアウト＆マージする。コミットの履歴やコードを参照して単純なマージで問題なさそうなことを確認しているけれど、一応自己責任ということで。

```shell-session
> mkdir ~/nvme
> cd ~/nvme
> svnlite checkout https://svn.freebsd.org/base/stable/11/sys/dev/nvme devnvme
> cd devnvme
> svnlite merge -r 329819:330802 https://svn.freebsd.org/base/head/sys/dev/nvme
> sudo mv /usr/include/dev/nvme /usr/include/dev/nvme.orig
> sudo ln -s ~/nvme/devnvme /usr/include/dev/nvme
> cd ~/nvme
> svnlite checkout https://svn.freebsd.org/base/stable/11/sbin/nvmecontrol nvmecontrol
> cd nvmecontrol
> svnlite merge -r 326276:330826 https://svn.freebsd.org/base/head/sbin/nvmecontrol
> sed -i.bak -e '/^\.PATH/s#\$.*#../devnvme#' Makefile
> make
```

新しいmanページを読みたければ、

```shell-session
> ln -s . man8
> man -M . nvmecontrol
```

##動作確認
コンパイルできたら動作確認。11.1-RELEASEのnvmecontrol(8)だと、2行目の`Format NVM Attributes`という行が表示されない。

```shell-session
> sudo /sbin/nvmecontrol identify nvme0 | grep Format
Format NVM:                  Supported
> sudo ./nvmecontrol identify nvme0 | grep Format
Format NVM:                  Supported
Format NVM Attributes:       Per-NS Erase, Per-NS Format
```

#フォーマット実行

フォーマットは`nvmecontrol format <namespace>`の様に実行すればいい。特に何の警告もなく瞬時に実行され、元あったデータにはアクセスできなくなる。

```shell-session
> sudo dd if=/dev/random of=/dev/nvd0 bs=1m count=1
1+0 records in
1+0 records out
1048576 bytes transferred in 0.022508 secs (46586596 bytes/sec)
> sudo dd if=/dev/nvd0 of=/dev/stdout bs=4096 count=1|hd |head
1+0 records in
1+0 records out
4096 bytes transferred in 0.000968 secs (4229251 bytes/sec)
00000000  42 68 66 4c 9b 0c e4 4e  4f 39 a5 f6 df 19 e4 b6  |BhfL...NO9......|
00000010  3c b7 e3 9a 2a ad 19 a5  3c 3f ec 68 62 3b c1 bc  |<...*...<?.hb;..|
00000020  af 4a 1f d9 86 5b 1c 52  6f bf 13 7d 45 97 54 a3  |.J...[.Ro..}E.T.|
00000030  34 7e ab a2 71 52 49 1a  9d 99 bc 1a 20 08 0e 2d  |4~..qRI..... ..-|
00000040  a5 83 e8 68 80 c0 99 0c  34 f2 d1 9d 71 d2 e8 ec  |...h....4...q...|
00000050  f9 e6 08 42 96 da e9 3d  bc 7f d1 ef 54 6f 9f 92  |...B...=....To..|
00000060  e8 6b 1d 4e 21 8b 20 46  53 12 89 88 dc eb cb 13  |.k.N!. FS.......|
00000070  c7 ba ff a9 49 c0 74 de  83 37 78 b8 24 51 68 6f  |....I.t..7x.$Qho|
00000080  f5 7e 56 bb f3 db b2 a0  22 56 fe c1 a6 32 f1 66  |.~V....."V...2.f|
00000090  b4 91 c7 6f d7 69 b2 ef  98 d7 07 38 f1 4f 08 24  |...o.i.....8.O.$|
> sudo ./nvmecontrol format nvme0ns1
> sudo dd if=/dev/nvd0 of=/dev/stdout bs=4096 count=1|hd
1+0 records in
1+0 records out
4096 bytes transferred in 0.000910 secs (4499072 bytes/sec)
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001000
```

ただしこれはメモリを未使用とマークするだけでチップ上にはデータが残っている可能性がある。すべて未使用とマークされるので、SSDを使い込んだことで劣化した性能は回復すると予想される。

##データ消去
より確実にチップ上のデータを消去するためには、-Eオプションを指定してUser Data Eraseの実行を指示する必要がある。手元のデバイスでは15秒ほど時間がかかった。これはSATA接続のSSDでいうところのSecure Eraseに相当する操作である。ただSSDの性能回復のために、通常のフォーマットだけで足りるのか、User Data Eraseが必要なのかは不明であり、デバイス依存である可能性がある。

```shell-session
>date; sudo ./nvmecontrol format -E nvme0ns1; date
Wed May 16 13:27:56 JST 2018
Wed May 16 13:28:11 JST 2018
```

チップ内で暗号化している場合には、User Data Eraseの代わりに暗号化の鍵だけを消去するCryptographic Eraseという操作を選択することができる（-Cオプション）。またさらに厳重に消去するSanitizeという操作も規格上定められているが、nvmecontrolは現状では対応していない。

##セクタサイズ変更
今回使ったNVMeデバイス（[WD Black PCIe SSD 256GB](http://products.wdc.com/library/SpecSheet/ENG/2879-800086.pdf)）はセクタ長を512バイトと4096バイトで切り替え可能であるらしい。LBA Format #00というのがセクタ長512バイト、#01というのがセクタ長4096バイトで、現在は512バイトになっていることがわかる。セクタ数が約5億で、総容量は238GBである。


```shell-session
> sudo ./nvmecontrol identify nvme0ns1 | grep 'in LBAs'
Size (in LBAs):              500118192 (476M)
Capacity (in LBAs):          500118192 (476M)
Utilization (in LBAs):       500118192 (476M)
> sudo ./nvmecontrol identify nvme0ns1 | grep 'LBA Format'
Number of LBA Formats:       2
Current LBA Format:          LBA Format #00
LBA Format #00: Data Size:   512  Metadata Size:     0  Performance: Best
LBA Format #01: Data Size:  4096  Metadata Size:     0  Performance: Best
```

セクタサイズの変更は`nvmecontrol format`に対して`-f <format>`を付ければ良い。このディスクの場合4096バイトセクタは#01なので`-f 1`となる。

```shell-session
> sudo ./nvmecontrol format -f 1 nvme0ns1
> sudo ./nvmecontrol identify nvme0ns1 | grep 'in LBAs'
Size (in LBAs):              62514774 (59M)
Capacity (in LBAs):          62514774 (59M)
Utilization (in LBAs):       62514774 (59M)
> sudo ./nvmecontrol identify nvme0ns1 | grep 'LBA Format'
Number of LBA Formats:       2
Current LBA Format:          LBA Format #01
LBA Format #00: Data Size:   512  Metadata Size:     0  Performance: Best
LBA Format #01: Data Size:  4096  Metadata Size:     0  Performance: Best
```

4096バイトセクタに変わっており、それに応じてセクタ数がちょうど1/8の約6250万となった。なおこの状態ではカーネルがセクタサイズの変更に気付いておらず、カーネルパニックを起こしたりするので早めに再起動したほうがよい。

```shell-session
> diskinfo -v nvd0
nvd0
        512             # sectorsize
        256060514304    # mediasize in bytes (238G)
        500118192       # mediasize in sectors
        0               # stripesize
        0               # stripeoffset
        18********89    # Disk ident.

```