Linuxの不揮発メモリの使い方について



# はじめに
2017年は私にとって忙しくも充実した年となりました。今年はLinuxの不揮発メモリ開発のメンテナーの方とお会いしたり、弊社のOSS開発者を育てるために[OSS Gate](https://oss-gate.github.io/)を[弊社社内で行って記事を書いた](https://oss-gate.github.io/report/workshop/2017/10/02/workshop-report-in-fujitsu.html)り、色々なことをやった気がします。

さて、今年の[Fujitsu Advent Calender part2](https://qiita.com/advent-calendar/2017/fujitsu_part2) 24日目のの記事として、昨年[Linux Advent Calender](https://qiita.com/YasunoriGoto1/items/177c7a5b22a02d087ebf)に記載した不揮発メモリの続編を書きたいと思います。昨年の記事をまだ参照されてない方は、そちらを先に見たほうが良いでしょう。

本記事ではpart1としてまず不揮発メモリ(NVDIMM)の設定や使い方を紹介したいと思います。2017年のACPIやコミュニティでの動向もこの記事に一緒に記載したかったのですが、長くなるので[part2](https://qiita.com/YasunoriGoto1/items/367e1d3214fe951d0493)として別の記事に記載します。

### お断り（お約束）

- 本記事の内容は私個人の見解であり、所属する組織として意見を代表したり保証したりするものではありません。
- 内容については誤りを含む可能性もありますのでご注意ください。(誤りについてご指摘していただけると助かります。)

# NVDIMMのアクセス方法
NVDIMMのLinuxでのアクセス方法について、改めて使い方について記載します。昨年は「DAX」や「メモリアクセス(?)」と記載していましたが、それから情報更新したものが後ろ２つです。

1. 通常のblock device/ファイルシステムとして使う  
NVDIMMを従来のストレージと同じように扱うことができます。メモリというよりは単なる高速なストレージとして扱う方法です。この方法であれば、従来のアプリもそのまま不揮発メモリ上で動作させることができます。しかし、HDDなどこれまでの遅いストレージを想定した設計をそのままNVDIMMにも引きずるので、NVDIMM本来の性能は発揮できません。
2. File System DAX(Direct Access Mode)  
昨年は単にDAXと書いていました。File System 経由で不揮発メモリにアクセスする方法ですが、ファイルをread()/write()する時には不揮発メモリでは無駄となるであろうpage cacheをスキップするほか、その中のファイルをmmap()したときには、当該のNVDIMMのblockを直接アプリに割りあててくれるので、本当に不揮発メモリとしてアクセスできます。従来のファイルシステムのシステムコールも使えるので、比較的使いやすいといえるでしょう。/dev/pmemX.Xとなっているデバイスに対して、XFSまたはext4のファイルシステムを構築し以下のようにmount時にオプションで-o daxと指定することでDAX Filesystemが使えるようになります。
3. Device DAX  
  昨年は「メモリインタフェース(？)」と書いていたアクセス方法です。
  使い方はシンプルで、/dev/daxX.Xというデバイスをopen()して、mmap()することで使います。
  最速のアクセス方法ですが、一方でopen(), mmap(), close()以外のシステムコールは使えません。また、/dev/daxの領域の管理も壊れた時の復旧も、互換性などもすべてがMWやアプリケーション責任です。kernel/driver側では何もしません。（[PMDK(旧名NVML)](http://pmem.io/pmdk/)のライブラリがある程度やってくれるかもしれませんが、私個人はまだちゃんと評価できていません。）

私見ですが、３つのアクセス方法を表にしてまとめると、このような感じになります。

|  |通常のblock deviceアクセス|File System DAX|Device DAX|
|:--|:--|:--|:--|
|性能|×：  無駄が多いため遅い|△～○：  従来アクセスでもpage cacheをスキップするなど高速性があがる。本当に不揮発メモリとして直接アクセスする方法もある |◎：  最速|
|アプリから見た従来との互換性|あり|Filesystem DAX向けにソフトの書き換えが必要|互換性は全くない|
|使いやすいさ|従来のストレージと同等|領域の管理はファイルシステムがやってくれるので、Device DAXよりは楽|面倒。領域内の管理はカーネル・ドライバは一切関知しないので、アプリ側で何とかする必要がある。（あるいは[PMDK](http://pmem.io/pmdk/)を使う）|
|対応ファイルシステム|すべて|xfs, ext4|なし|
|CPU cacheから媒体へのデータ反映|sync()/fsync()/msync()など|今のところ、原則左に同じ(理由はpart2のほうに別途記述)。将来的にはDevice DAXと同じような反映方法が期待されている|cpuのcache flush命令(clflush, clflushopt,or clwb)とメモリバリア(sfence)で行う|

# Linuxでの不揮発メモリの使い方

まずは、使ってみるのが一番わかりやすいと思います。このため、kernelが持っているNVDIMMのエミュレーション機能を使って、それぞれどんな感じになるのか試してみましょう。
環境はFedora27です。

### エミュレーションの設定
kernelのboot optionで、memmap=XX!YY と指定すると、物理アドレスYYのメモリを先頭にXXバイトのRAMをNVDIMMとしてエミュレーションすることができます。この環境のメモリは12Gなので、RAMとNVDIMMを半分ずつの6Gで割り当てます。

```shell-session

# cat /etc/default/grub
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
#GRUB_TERMINAL_OUTPUT="console"
GRUB_TERMINAL="console serial"
GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200 memmap=6G!6G"
                                                      ^^^^^^^^^^^^
                                                      追加
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop==
1"
GRUB_DISABLE_RECOVERY="true"

# grub2-mkconfig -o /boot/grub2/grub.cfg
```

これで再起動すると、dmesgの出力から本来はRAMの領域が不揮発メモリとして扱われていることが分かります。
BIOS-e820の行は、ファームから与えられた本来のメモリマップ、userとなっている行は、memmap=6G!6Gのboot optionの指定によって変更された後のマッピングです。最後のほうにpersistent (type 12)と出力されています。

```
[    0.000000] e820: BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000bffdefff] usable
[    0.000000] BIOS-e820: [mem 0x00000000bffdf000-0x00000000bfffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000feffc000-0x00000000feffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000033fffffff] usable　　<---　もともとは全部普通のRAMの領域
[    0.000000] NX (Execute Disable) protection: active
[    0.000000] e820: user-defined physical RAM map:
[    0.000000] user: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] user: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[    0.000000] user: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] user: [mem 0x0000000000100000-0x00000000bffdefff] usable
[    0.000000] user: [mem 0x00000000bffdf000-0x00000000bfffffff] reserved
[    0.000000] user: [mem 0x00000000feffc000-0x00000000feffffff] reserved
[    0.000000] user: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[    0.000000] user: [mem 0x0000000100000000-0x000000017fffffff] usable
[    0.000000] user: [mem 0x0000000180000000-0x00000002ffffffff] persistent (type 12) <--------ここが不揮発メモリと扱われるようになった領域です。
[    0.000000] user: [mem 0x0000000300000000-0x000000033fffffff] usable

```

### 管理コマンド(ndctl)のインストール

Fedora27では、ndctlコマンドもdnfコマンドで簡単にインストールできます。では、インストールしてみましょう。

```shell-session

$ sudo dnf install ndctl

$ sudo ndctl --version
58.4
```

さて、NVDIMMのnamespaceのリストを表示させてみます。ndctlの出力はJSON形式で出力されます。ここでは、namespace0.0という領域ができていることが分かります。
以降は実験として、この領域のnamespaceの設定を変更していきます。

```shell-session

$ sudo ndctl list
{
  "dev":"namespace0.0",
  "mode":"memory",
  "size":6442450944,
  "blockdev":"pmem0",
  "numa_node":0
}
```

### 高速なストレージとして使ってみる

まずはNVDIMMを高速なストレージとして使ってみましょう。namespaceの設定はndctl create-namespaceコマンドで行いますが、この時のオプションに-m sectorと指定すると、当該namespaceは従来のストレージと同じようにブロック単位でデータを更新する領域になります。またこの際、-lオプションでブロックサイズも指定する必要があります。この値は512byteとか4096byteとかで良いはずですが、今回は4096byteにしています。

```shell-session

$ sudo ndctl create-namespace -e "namespace0.0" -m sector -l 4096 -f
{
  "dev":"namespace0.0",
  "mode":"sector",
  "size":"5.99 GiB (6.44 GB)",
  "uuid":"202373f0-1b85-429a-8c16-6615dd28cb50",
  "sector_size":4096,
  "blockdev":"pmem0s",
  "numa_node":0
}
```

これによって/dev/pmem0sというデバイスができるので、後は普通にパーティションの設定やファイルシステムのフォーマットをするだけです。今回はxfsを使ってみましょう。mountも今までのストレージと同じです。

```shell-session

$ ls /dev/pmem*
/dev/pmem0s

$ sudo parted /dev/pmem0s
(parted) mkpart
パーティションの名前?  []? nvdimm
ファイルシステムの種類?  [ext2]? xfs
開始? 1M
終了? 6G

$ ls /dev/pmem*
/dev/pmem0s  /dev/pmem0s1


$ sudo mkfs.xfs /dev/pmem0s1 -f
meta-data=/dev/pmem0s1           isize=512    agcount=4, agsize=392640 blks
         =                       sectsz=4096  attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=0, rmapbt=0, reflink=0
data     =                       bsize=4096   blocks=1570560, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
log      =internal log           bsize=4096   blocks=2560, version=2
         =                       sectsz=4096  sunit=1 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0

$ sudo mkdir /mnt/pmem

$ sudo mount /dev/pmem0s1 /mnt/pmem
```

これで、NVDIMMは/mnt/pmem上の普通のxfsファイルシステムとして使うことができるようになります。
### Filesystem DAXを使う(mmap編)

では、こんどはFilesystem DAXを使ってみましょう。Filesystem DAXとして使うためには、まず先ほどのnamespaceの設定を変更する必要があります。先ほどの/mnt/pmemをumountした後、ndctlのcreate-namespaceコマンドのオプション-m memoryを使って再設定しましょう。今度はbyte単位でアクセスできるモードですから、ブロックサイズを指定する必要はありません。

```shell-session

$ sudo ndctl create-namespace -e "namespace0.0" -m memory -f
{
  "dev":"namespace0.0",
  "mode":"memory",
  "size":"5.90 GiB (6.34 GB)",
  "uuid":"dc47d0d7-7d8f-473e-9db4-1c2e473dbc8f",
  "blockdev":"pmem0",
  "numa_node":0
}
```

今度は/dev/pmem0というデバイス名になります。先ほどは/dev/pmem0sでしたからsがあるか無いかの違いで見分けることができますね。パーティションやフォーマットは通常のストレージと同じですが、Filesystem DAXを有効にするためにはmountのオプションで-o daxをつける必要があります。

```shell-session

$ sudo parted /dev/pmem0

GNU Parted 3.2
/dev/pmem0 を使用
GNU Parted へようこそ！ コマンド一覧を見るには 'help' と入力してください。
(parted) mklabel gpt
(parted) mkpart
パーティションの名前?  []? nvdimm
ファイルシステムの種類?  [ext2]? xfs
開始? 1M
終了? 6G

$ ls /dev/pmem*
/dev/pmem0  /dev/pmem0p1

$ sudo mkfs.xfs /dev/pmem0p1
meta-data=/dev/pmem0p1           isize=512    agcount=4, agsize=386816 blks
         =                       sectsz=4096  attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=0, rmapbt=0, reflink=0
data     =                       bsize=4096   blocks=1547264, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=1
log      =internal log           bsize=4096   blocks=2560, version=2
         =                       sectsz=4096  sunit=1 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0

$ sudo mount -o dax /dev/pmem0p1 /mnt/pmem
              ^^^^^
              Filesystem DAXの指定
```

なお、Filesystem DAXはまだ"Experimental"、つまりまだ実験的な未完成の機能として扱われているので、mountするとsyslogに警告が出ます。

```
Dec 23 12:29:20 localhost kernel: XFS (pmem0p1): DAX enabled. Warning: EXPERIMENTAL, use at your own risk
Dec 23 12:29:20 localhost kernel: XFS (pmem0p1): Mounting V5 Filesystem
Dec 23 12:29:20 localhost kernel: XFS (pmem0p1): Ending clean mount
```
さて、FilesystemDAXでは、NVDIMM内の領域のファイルをmmap()すると、不揮発メモリの領域が直接割り当てられます。それが本当かどうか実験してみましょう。

以下のような、簡単なプログラムを用意します。

```c

#include <stdio.h>
#include <sys/mman.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define MMAP_SIZE ((size_t)1024*1024*1024)

int main(int argc, char *argv[])
{
        int fd;
        void *p;

        if (argc != 2) {
                printf("dax_read_loop <file or device name>\n");
                return -1;
        }

        fd = open(argv[1], O_RDWR);
        if (fd == -1) {
                perror("open");
                return -1;
        }

        p = mmap(NULL, MMAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        if (p == MAP_FAILED) {
                perror("mmap");
                return -1;
        }

        printf("mmapped p=%p\n", p);

        memcpy(p, "hogehoge\n", sizeof("hogehoge\n"));
        msync(p, 4096, MS_SYNC);
        for(;;);

}
```

コンパイルして実行します。プロセスに割り当てられた仮想アドレスが表示されます。

```shell-session

$ cc dax_read_loop.c -o dax_read_loop
$ sudo touch /mnt/pmem/hogehoge
$ sudo ./dax_read_loop /mnt/pmem/hogehoge
mmapped p=0x7fd0df600000
```

上記仮想アドレスに対して、対応する物理アドレスを知るにはcrashコマンドを使ってkernelの中を覗く方法を使ってみます。
注）crashでこのようなlive dumpの機能を使うには、あらかじめ以下のような準備が必要なのですが、本題から外れてくるので手順は省略します。興味のある人は別途調べてみてください。

- kernel の debuginfoをダウンロード・インストール
- 上記に対応するkernelにupdate（debuginfoは最新版のkernelにしか用意されません）
- crashが古くてlive dumpを開くことができない場合は、最新の[crash](https://people.redhat.com/anderson/)をビルド

では、見てみましょう。

```shell-session

$ sudo ./crash /usr/lib/debug/lib/modules/4.14.7-300.fc27.x86_64/vmlinux /proc/kcore

crash 7.2.0
Copyright (C) 2002-2017  Red Hat, Inc.
　:
  :

crash> ps |grep dax <----実行したプロセスを探す
> 25956    948   4  ffff9b5b6511ddc0  RU   0.0 1128512   1360  dax_read_loop


crash> set 25956　<----ターゲットをこのプロセスに設定
    PID: 25956
COMMAND: "dax_read_loop"
   TASK: ffff9b5b6511ddc0  [THREAD_INFO: ffff9b5b6511ddc0]
    CPU: 4
  STATE: TASK_RUNNING (ACTIVE)

crash> vtop 0x7fd0df600000 <----プロセスが表示した仮想アドレスから物理アドレスを求める
VIRTUAL     PHYSICAL
7fd0df600000  186315000 <----割り当てられた物理アドレス

   PML: 12e3247f8 => 13f363067
   PUD: 13f363a18 => 12aa66067
   PMD: 12aa667d8 => 12b264067
   PTE: 12b264000 => 8400000186315a25
  PAGE: 186315000

      PTE         PHYSICAL   FLAGS
8400000186315a25  186315000  (PRESENT|USER|ACCESSED|NX)

      VMA           START       END     FLAGS FILE
ffff9b5b6529fed8 7fd0df600000 7fd123fb4000 380000fb /root/mnt/pmem/hogehoge <---開いているファイル

      PAGE        PHYSICAL      MAPPING       INDEX CNT FLAGS
ffffe70a4618c540 186315000                0        0  0 0

```

先ほどdmesgで表示されたアドレスと比べると、先ほどの物理メモリが不揮発メモリの領域の範囲内であることが分かります。
このように、Filesystem DAX上のファイルをmmap()すると、不揮発メモリが直接割り当てられるようになるのです。

```
[    0.000000] user: [mem 0x0000000180000000-0x00000002ffffffff] persistent (type 12) 
                                   ^^^^^^^^^          ^^^^^^^^^
```

### Filesystem DAXを使う(read()/write()編)

Filesystem DAXでは通常のread()/write()システムコールも普通に使うことができます。この場合、page cacheすなわちファイルのキャッシュをスキップするようになります。これを確認してみましょう。

通常のファイルシステムでは、システムにFreeMemoryがまだいっぱい残っている状態の時にtarのアーカイブを展開すると、カーネルはそれらのファイルを一旦page cacheに残そうとします。これは /proc/meminfo の Inactive(file) の値が増えるかどうかで確認することができます。大きなtarballほど効果が分かりやすいので、kernelのソースを使ってまずは通常のファイルシステム上に展開してみましょう。

```shell-session

$ cat /proc/meminfo
MemTotal:        6109588 kB
MemFree:         5202244 kB  <---空きメモリが大量にある
MemAvailable:    5699356 kB
Buffers:            4620 kB
Cached:           684080 kB
SwapCached:            0 kB
Active:           502456 kB
Inactive:         254672 kB
Active(anon):      69056 kB
Inactive(anon):      304 kB
Active(file):     433400 kB
Inactive(file):   254368 kB　<---　tarを展開する前はこれぐらい

  :
  :

$ sudo tar xvf /home/goto/linux-4.14.tar.gz

$ cat /proc/meminfo
MemTotal:        6109588 kB
MemFree:         4048288 kB  <---空きメモリが使われた
MemAvailable:    5504288 kB
Buffers:            4620 kB
Cached:          1585188 kB
SwapCached:            0 kB
Active:           506596 kB
Inactive:        1152844 kB
Active(anon):      70292 kB
Inactive(anon):      308 kB
Active(file):     436304 kB
Inactive(file):  1152536 kB  <--- tarを展開するとpage cacheが使われるので、この値が増える
　 :
   :
```

このtarを展開したディレクトリを削除すると、page cacheはもう必要なくなるため、Inactive(file)の値が減ることになります。

```shell-session

$ sudo rm -rf linux-4.14/

MemTotal:        6109588 kB
MemFree:         5189832 kB　<---増えた
MemAvailable:    5693512 kB
Buffers:            4620 kB
Cached:           689852 kB
SwapCached:            0 kB
Active:           508196 kB
Inactive:         255280 kB
Active(anon):      69548 kB
Inactive(anon):      308 kB
Active(file):     438648 kB
Inactive(file):   254972 kB　<---page cacheが不要になったので、MemFreeに返却された
```

では、Filesystem DAX上で展開するとどうなるでしょうか？比較してみましょう。

```shell-session

MemTotal:        6109588 kB
MemFree:         5177504 kB
MemAvailable:    5694032 kB
Buffers:            4620 kB
Cached:           702828 kB
SwapCached:            0 kB
Active:           522180 kB
Inactive:         254088 kB
Active(anon):      69456 kB
Inactive(anon):      304 kB
Active(file):     452724 kB
Inactive(file):   253784 kB　<-----もとの量
  :
  :

$ cd /mnt/pmem
$ sudo tar xvf ~goto/linux-4.14.tar.gz

MemTotal:        6109588 kB
MemFree:         4971340 kB
MemAvailable:    5526536 kB
Buffers:            4620 kB
Cached:           704212 kB
SwapCached:            0 kB
Active:           523636 kB
Inactive:         254116 kB
Active(anon):      69604 kB
Inactive(anon):      304 kB
Active(file):     454032 kB
Inactive(file):   253812 kB  <---ほとんど増えてない
  :
  :
```

先ほどと比べてほとんど増えていません。このように、Filesystem DAXには、page cacheを使わずにデータをread()/write()するというメリットもあります。
### Device DAXとして使う
では、最後にDevice DAXとして使ってみましょう。/mnt/pmem を umount したら、もう一度ndctl create-namespaceコマンドを使って、今度はDevice DAXモードにしてみましょう。

```shell-session

$ sudo ndctl create-namespace -e "namespace0.0" -m dax -f
{
  "dev":"namespace0.0",
  "mode":"dax",
  "size":"5.90 GiB (6.34 GB)",
  "uuid":"1d516f0a-b2c7-49e8-a90a-ee9551ef6d5e",
  "daxregion":{
    "id":0,
    "size":"5.90 GiB (6.34 GB)",
    "align":2097152,
    "devices":[
      {
        "chardev":"dax0.0",
        "size":"5.90 GiB (6.34 GB)"
      }
    ]
  },
  "numa_node":0
}
```
このモードでは、/dev/daxというデバイスができます。

```shell-session

$ ls /dev/dax*
/dev/dax0.0
```

使い方は先ほどのmmap()のプログラムを使って/dev/dax0.0を開くだけです。（ただし、msync()はエラーで戻ってくるので注意が必要です。）

```shell-session

$ sudo ./dax_read_loop /dev/dax0.0
mmapped p=0x7fc4aa200000
```
このデバイスに対してできることは、open(), close(), mmap()だけです。
/dev/dax0.0の領域の管理は完全にアプリレイヤに任されているので、既存のソフトウェアからDevice DAXを使うというのは無理そうです。[PMDK](http://pmem.io/pmdk/)のライブラリを使ったほうが良さそうな予感がしますね。

# まとめ

ざっとNVDIMMの３種類の使い方を紹介しました。Fedora27のカーネルがNVDIMMのエミュレーションをデフォルトで対応するようになったので、自分でカーネルのコンパイルオプションを指定してビルドする必要もなくなり、昨年よりずいぶん楽に、そして身近になってきています。ぜひ試してみてください。

では、part2に続きます。