btrfsでswapfile + hibernation

Linux kernel 5.0リリース記念の記事です

# はじめに
Linux kenel5.0からbtrfsにおいてswapfileがサポートされました[^a]。
ただしいくつかの注意点があるのでその使い方についてまとめます。
(以下の動作はノートPC上のopenSUSE Tumbleweed + kernel 5.0で確認していますが、他の環境でも違いはないと思います)

[^a]: これまでもループバックデバイス上にswapfileを作るという裏技はありましたが

# 使用条件
まずswapfileを使用するためにはいくつかの条件があります。kenel 5.0の時点での使用条件は以下のようになります:

* シングルデバイス[^g]上のみに作成可能
* swapfileはnocowでなければならない (よってチェックサムなし/透過圧縮不可)
* swapfileが含まれるsubvolumeはスナップショット不可　
* swapfile使用中はbalanceおよびデバイスの追加/削除/交換/リサイズ不可
* balanceおよびデバイスの追加/削除/交換/リサイズ中はswapfileのアクティベーション不可

[^g]: 正確に言うとシングルデバイスかつデータ部がシングルプロファイルでなければなりません(つまりデータ部にdup不可)。mkfs.btrfsのデフォルトを使用した場合、シングルデバイスならばデータ部はシングルプロファイルになるので問題ありません

大雑把に言うとswapfileに対してbtrfsのcopy on writeの機能を禁止し、またデータの共有や再配置が行われないようにする必要があります。
これは使用中のswapfileにおいてファイルのアドレスが変更されたり、データが共有されることがあってはならないためです。

# swapfileの作り方
では実際にswapfileの作り方を見ていきます。

基本的には他のファイルシステム上で作るときと同じです。一点違うのはswapfileにnocow[^1]設定を行うことですが、nocow設定はファイルサイズが0のときに行わなければ効果がありません。よって

```
truncate -s 0 /swapfile
chattr +C /swapfile # nocow設定
fallocate -l 32G /swapfile
```

とするか、

```
mkdir /swap
chattr +C /swap
fallocate -l 32G /swap/swapfile # 他のattrと同様に親ディレクトリの設定が引き継がれる
```
などとします[^2]。
後の作業は他のファイルシステムと同じです:

```
# $ chown root /swapfile # ownerがrootでないならrootにする
$ chmod 600 /swapfile
$ mkswap /swapfile
$ swapon /swapfile
$ swapon -s
Filename                     Type            Size            Used    Priority
/swapfile                    file            33554428        0       -3
```

nocow設定等に失敗している場合はswaopnでエラーになるのでdmesgで原因を確認してください。
起動時に自動でswapfileをアクティベートするには以下の記述を/etc/fstabに加えます:

```
/swapfile                    swap   swap    defaults   0   0 
```

[^1]: nocowと言っていますが厳密にはnodatacowです。nocow設定をしたファイルではデータの更新の際にcopy on writeが行われず直接上書きされますが(よってクラッシュ時にpartial writeの可能性あり)、メタデータの更新に関しては依然としてcopy on writeが行われます
[^2]: 他には"-o nodatacow"マウントオプションをつけてマウントしている場合は作成する全てのファイルがnocowになりますが、普通は使用しないと思います

# hibernation
swapfileが作成できたので、せっかくなのでhibernationも試してみます。

結論を言うとswapfileを使用したhibernationも他のファイルシステムと同様に問題なく動作するのですが、現時点では次のbugzillaにあるように簡単には設定できません:
https://bugzilla.kernel.org/show_bug.cgi?id=202803

以下簡単に説明します。

## swapfileを利用したhibernationの設定方法
まず一般にswapfileを使用したhibernationの仕方について説明します。

これは単純で、GRUBのコマンドラインに"resume="および、"resume_offset="の項目を付け加えるだけで設定できます。ここで"resume="にはswapfileのあるデバイス名、"resume_offset"にはswapfileのデバイス上の開始物理アドレスを指定します[^q]。

[^q]: ブート時にはスワップファイルの場所が分からないため、デバイスから読み出す位置を直接指定します

ファイルの物理アドレスはfilefragコマンド(FIEMAP ioctlを使用しています)を用いて取得できます。例えば

```
$ filefrag -v /swapfile
Filesystem type is: 9123683e
File size of /swapfile is 34359738368 (8388608 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..       0:    8789476..   8789476:      1:            
 <略>
```

という出力が得られた場合、このファイルの開始物理アドレスは8789476(ページ単位)であることがわかります。
これをGRUBのコマンドラインに記述すれば良いので、/etc/default/grubに必要な記述を追加し:

```
GRUB_CMD_LINUX="... resume=/dev/nvme0n1p6 resume_offset=8789476"
```
grubを更新して再起動します:

````
$ grub2-mkconfig -o /boot/grub2/grub.cfg
$ reboot
```

この後は普通にhibernationできます。systemd経由で行う場合は:

```
$ systemctl hibernate
```

とします。

## 現状のbtrfsでの問題と解決策
問題は2つあります。

### 問題1. filefragコマンドでデバイス上の物理アドレスが取得できない
```filefrag -v```の出力に現れる"physical offset"の値は、btrfsでは実はデバイス上の物理アドレスではありません[^w]。このアドレスをさらに一度変換したものが物理アドレスになります。これはbtrfsがマルチデバイスに対応するためであり、例えばRAID1を使用する場合はこのアドレスが2つの物理アドレスにマッピングされます。またシングルデバイス上でもbtrfsのアロケーションの関係上、物理アドレスと一致するとは限りません。よって、別の方法で物理アドレスを取得する必要があります。

残念ながらbtrfs-progsには今のところファイルの物理アドレスを取得するコマンドがないため自分で計算する必要があります。
上のbugzillaにはファイルの物理アドレスを求めるプログラムが貼られているので、btrfs-progsが更新されるまで待てない人はそれを使用すると良いと思います。

また自力で計算したい(?)という人は以下の方法でできます（完全に余談なので興味のない人は飛ばしてください）:
まずbtrfsの論理アドレス → デバイス上の物理アドレスへの変換はchunkと呼ばれる単位で行われます。
btrfsでは名前の由来の通り全てのメタデータが複数のbtree(正確にはリーフにしかデータがないのでb+ tree)で管理されており、chunkの情報はchunk treeと呼ばれるツリーに含まれています。btrfs-progsにはメタデータツリーの情報を全て取得できるinspect-internal dump-treeというコマンドがあるので(当然ながら要root権限)、

```
btrfs inspect-internal dump-tree -t chunk <デバイスへのパス>
```

とするとそのファイルシステム上のchunk情報がすべて取得できます。例えばシングルデバイス上で上のコマンドを実行すると、

```
item 1 key (FIRST_CHUNK_TREE CHUNK_ITEM 13631488) itemoff 16105 itemsize 80
                length 8388608 owner 2 stripe_len 65536 type DATA
                io_align 65536 io_width 65536 sector_size 4096
                num_stripes 1 sub_stripes 0
                        stripe 0 devid 1 offset 13631488
                        dev_uuid ea2e29d3-02eb-4a0d-860f-f2d4135798a5
```

というようなデータが並んで出力されます。これが１つのchunkに対応するのですが、

* ```(FIRST_CHUNK_TREE CHUNK_ITEM 13631488)```の"13631488"がこのchunkの開始論理アドレス
* ```length 8388608```の8388608がこのchunkのサイズ
* ```stripe 0 devid 1 offset 8645009120```がこのchunkがデバイス1の物理アドレス8645009120にマッピングされていることを表す

といった情報が取得できます(すべてbyte単位です)。
よって、

1. filefrag -vでbtrfs上の論理アドレスを取得
2. 1.の値はページ単位なのでバイト単位に変換
3. chunk treeの一覧から2.のアドレスが含まれるchunkを探す
4. chunkの物理開始アドレスにオフセットを足してファイルの物理アドレスを計算

といったことを行えば自分でファイルの物理アドレスが計算できます。

[^w]: ややこしいことにbtrfsではここに現れるアドレスのことをlogical addressと呼んでいます

### 問題2. systemd経由でhibernationを実行すると正しくパラメータがカーネルに渡されない
```systemctl hibernate```を使用してhibernationを行った場合、systemd側で改めてFIEMAP ioctlで取得した値を物理アドレスとして設定し直しているため、hibernation処理が開始できずにエラーになります。これを回避するために直接:

```
$ echo disk > /sys/power/state
```

を指定してhibernationを実行すると問題なく動きます。

# おわりに
冒頭に述べたような条件はあるものの、kernel 5.0以降ではbtrfs上にswapfileを簡単に作成して使用することできます。
またswapfileを利用したhiberanationに関しては現時点で設定に注意が必要ですが、問題に対してすでに議論が進んでいるため、近いうちにより使いやすくなるような修正が入るのではないかと思います。