TenForward

技術ブログ。はてなダイアリーから移転しました

OverlayFSの下層側にあるディレクトリーをリネームしたときの動き

OverlayFSは、複数のディレクトリーを重ね合わせて 1 つに見せてマウントするファイルシステムです。みなさん、コンテナを使っているならば、ほぼその裏側では OverlayFS が動いていることでしょう。

LXC/Incus 推しの私の環境では OverlayFS が動くことはほとんどないですが。:-p

重ね合わせには「下層側」ディレクトリーと「上層側」ディレクトリーを指定し、それを重ねます。下層側はいくつでも指定できます。このあたりの詳細は、ちょっと古いのですが、私の連載「LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術」の第 18 回をご覧ください。

gihyo.jp

この記事は少し古いので、無駄な部分(今と異なる部分)が多いので、もっと効率的に勉強したい!という方は、おや、ちょうど良い本が…😂

techbookfest.org

技術書典 16 にて、"Linux Container Book" 第 4 弾リリースです。中身は大体 OverlayFS の話です!! ぜひお買い求めください!!

OverlayFS における変更の記録

OverlayFS のファイルやディレクトリーなどのオブジェクトに変更を加えると、通常は上層側に記録されます。下層側にしか存在しないオブジェクトの場合でも、上層側に変更が加えられます。

その際、下層側にしかないオブジェクトは上層側に作成されたり、コピーされたりします。

詳しい動きは連載か本でご覧ください。連載でも十分にこのあたりは理解できます。

下層側に存在するディレクトリーのリネーム(移動)

さて、下層側にだけ存在するディレクトリーをリネームする場合、OverlayFS は次の 2 つの方法で処理できます。これはカーネル付属文書に書かれています。

  1. return EXDEV error: this error is returned by rename(2) when trying to move a file or directory across filesystem boundaries. Hence applications are usually prepared to handle this error (mv(1) for example recursively copies the directory tree). This is the default behavior.
  2. If the “redirect_dir” feature is enabled, then the directory will be copied up (but not the contents). Then the “trusted.overlay.redirect” extended attribute is set to the path of the original location from the root of the overlay. Finally the directory is moved to the new location.

1 がデフォルトの動きです。下層側に存在するディレクトリーをリネームすると、EXDEV というエラーが返ります。これは、ファイルシステムでリネームする場合にも一般的に返るエラーなんだと思います(しらんけど)。

一般的な ext4 や XFS といったファイルシステムがマウントされているシステムで、リネーム元とリネーム先が同じファイルシステムに存在しない場合、メタデータを変更して済ますわけにはいきませんので、実際のオブジェクトの作成やコピーなどが必要になります。

このエラーが返ったときは、呼び出し元のコマンドがよしなに処理します。つまり mv コマンドの場合でも、mv コマンドが EXDEV を受け取って必要な処理を行います。

OverlayFS は、既存のコマンドでも使われているこの仕組みをうまく利用しています。

OverlayFS において、下層側のオブジェクトに変更が加わると、下層側→上層側間のオブジェクトのコピーが発生します。コピーしたあとに、上層側で変更を加えます。

ディレクトリーをリネームした場合も、EXDEV が返ると、mv コマンド側で必要な処理がされますので、そのあたりを追ってみました。

  1. 下層側にあるディレクトリーを mv でリネーム
  2. OverlayFS が EXDEV を返す
  3. mv が上層にリネーム先のディレクトリーを作成する
  4. mv がリネーム元のディレクトリーに存在するファイルをオープン
  5. mv がリネーム先のディレクトリーに 4 と同じ名前でファイル作成
  6. mv がリネーム元のディレクトリー内ファイルのデータを読み、リネーム先のファイルに書き込み
  7. mv が下層側のリネーム元のオブジェクトの削除を行う
  8. OverlayFS が削除に対応して whiteout を作成

OverlayFS としては、EXDEV を返すだけで、あとは呼び出し元のコマンド mv が必要な処理を全部やってくれて、OverlayFS はそれに応じた処理を淡々とこなすだけで、ちゃんと OverlayFS として矛盾のない動きが得られています。しかも、ユーザー側のプログラム(ここでは mv)の変更は不要です。これはなかなか良くできてますね。

実際の内部的な処理を追う

先に紹介した動きを実際に確認していきましょう。

まずは、OverlayFS 環境の準備です。

$ mkdir lower upper overlay work
$ mkdir -p lower/lowerdir upper/upperdir
$ touch lower/lowerdir/testfile

今回は上層側のオブジェクトは不要ですが、一応準備してあります。これでマウントします。

$ sudo mount -t overlay -o lowerdir=lower,upperdir=upper,workdir=work,redirect_dir=off overlay overlay
$ tree 
.
├── lower
│   └── lowerdir
│       └── testfile
├── overlay
│   ├── lowerdir
│   │   └── testfile
│   └── upperdir
├── upper
│   └── upperdir
└── work
    └── work  [error opening dir]

9 directories, 2 files

下層側 lower と上層側 upper を重ね合わせて overlay にマウントしています。

これで、overlay 以下に移動して、下層側にあるディレクトリーを mv でリネームします。このときの内部的な処理を、みなさんもお好きな strace コマンドで追っかけます。

$ cd overlay
$ strace mv lowerdir upperdir

まず、リネーム処理がされているところを探します。rename をキーワードにすると見つかりました。ここからは strace の出力の一部だけをお見せしますが、上から下に時系列で並んでいると思ってください。

execve("/usr/bin/mv", ["mv", "lowerdir", "newerdir"], 0x7ffdca0c8470 /* 24 vars */) = 0
  :(snip)
renameat2(AT_FDCWD, "lowerdir", AT_FDCWD, "newerdir", RENAME_NOREPLACE) = -1 EXDEV (Invalid cross-device link)

最初に execvemv コマンドを実行しただいぶ後に renameat2 が呼ばれているのがわかります。そして、たしかにエラーとなっていて、EXDEV が返っているのがわかります。

次に関係しそうなのが、上層側に必要なオブジェクトを作成することですね。まずはディレクトリーの作成でしょう。

mkdir("newerdir", 0700)                 = 0

mkdirnewerdir が作成されていることがわかります。変更は上層側に行われます。

次に、readdirlowerdir 内のオブジェクトを取得しているのでしょうか。

openat(AT_FDCWD, "lowerdir", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
  :(snip)
getdents64(3, 0x559d7c4a9e40 /* 3 entries */, 32768) = 80
getdents64(3, 0x559d7c4a9e40 /* 0 entries */, 32768) = 0
close(3)                                = 0

そしてディレクトリー内のファイルの処理を行います。

openat(AT_FDCWD, "lowerdir/testfile", O_RDONLY|O_NOFOLLOW) = 3
newfstatat(3, "", {st_mode=S_IFREG|0664, st_size=0, ...}, AT_EMPTY_PATH) = 0
openat(AT_FDCWD, "newerdir/testfile", O_WRONLY|O_CREAT|O_EXCL, 0600) = 4
newfstatat(4, "", {st_mode=S_IFREG|0600, st_size=0, ...}, AT_EMPTY_PATH) = 0

次は、リネーム元からリネーム先にデータをコピーしなければならないので、元のファイルを openat でオープンしていますね。そして、移動先のファイルを openat で作成しています。

read(3, "", 131072)                     = 0
  :(snip)
close(4)                                = 0
close(3)                                = 0

リネーム元のファイルからデータを読んでますね。書き込んでるところは見つからんけどw close(4) の前にはファイルの属性を色々設定しています。

unlinkat(4, "testfile", 0)              = 0
  :(snip)
unlinkat(AT_FDCWD, "lowerdir", AT_REMOVEDIR) = 0

その後、下層側ディレクトリー内のファイルとディレクトリーを削除しています(たぶん)。

削除を行うと、OverlayFS は、上層側に下層側のオブジェクトをマスクするための "whiteout" という特殊なデバイスファイル(番号0:0のキャラクターデバイス)を作成します。なので、上記の処理で whiteout が作成されます。


普通は、同じデバイス上のリネームでは返さない EXDEV というエラーを返すようにしただけで、ユーザー側の変更は不要で、ユーザー側の実装含めて、ちゃんと矛盾なくよしなに動くようになるという、なかなかうまくやってるなあ、と思ったのでブログにしました。

ちなみに、カーネル付属文書の 2 は、OverlayFS でオプション機能を有効にすると、上記のような単純なファイルのコピーが起こらなくなるというものです。詳しくは本を買ってくださいw

なんかイマイチツッコミが足りない記事ですが、技術書典の本をよろしくお願いいたします。