Edited at

Linuxのfsfreezeを調査する

More than 1 year has passed since last update.


はじめに

Linuxにはfsfreeze(8)というFilesystemへのアクセスを一時凍結させる機能がある。Filesystemのスナップショットを取るなどの目的で使われるようだ。このfsfreezeがどのように動いているのかについて調査してみた。

なお、動作確認はUbuntu16.04(Linux-4.4.0-83-generic)くらいで行いつつ、ソースコードはLinux-4.12くらいとutil-linux-2.30.1くらいを見ています。


試してみる


fsfreezeの実行

システムに影響を与えるところで実験するのは怖いので、テスト用のFilesystemを用意してテストする。

[rarul@tina ~]$ mkdir ~/test

[rarul@tina test]$ cd ~/test
[rarul@tina test]$ dd if=/dev/zero of=hoge.dat bs=1M count=64
64+0 records in
64+0 records out
67108864 bytes (67 MB, 64 MiB) copied, 0.0563057 s, 1.2 GB/s
[rarul@tina test]$ mkfs.ext4 hoge.dat
mke2fs 1.42.13 (17-May-2015)
Discarding device blocks: done
Creating filesystem with 65536 1k blocks and 16384 inodes
Filesystem UUID: f06fffa4-752e-484c-a54d-c6fa8c3834df
Superblock backups stored on blocks:
8193, 24577, 40961, 57345

Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done

[rarul@tina test]$ mkdir mnt
[rarul@tina test]$ su
[root@tina test]# mount -t ext4 -o loop hoge.dat mnt
[root@tina test]# fsfreeze -f mnt
[root@tina test]# touch mnt/test.log

freezeさせた後に最初に書き込みをしようとしたところでプログラムが返ってこなくなる。


freeze中の状態の確認

ここで別のTerminalを開き、touchプログラムがどういう状態で待たされているのかを確認する。

(ps uなどで該当のtouchのPIDを確認した上で)

[root@tina ~]# cd /proc/3015
[root@tina 3015]# cat wchan
call_rwsem_down_read_failed
[root@tina 3015]# cat stack
[<ffffffff81407a54>] call_rwsem_down_read_failed+0x14/0x30
[<ffffffff81211f4c>] __sb_start_write+0x2c/0x40
[<ffffffff81230704>] mnt_want_write+0x24/0x50
[<ffffffff8121e59a>] path_openat+0xd5a/0x1330
[<ffffffff8121fd61>] do_filp_open+0x91/0x100
[<ffffffff8120e1f8>] do_sys_open+0x138/0x2a0
[<ffffffff8120e37e>] SyS_open+0x1e/0x20
[<ffffffff81840b72>] entry_SYSCALL_64_fastpath+0x16/0x71
[<ffffffffffffffff>] 0xffffffffffffffff

書き込み用のopenシステムコールで返ってこなくなっている様子がわかる。またこの時プロセスはUNINTERRUPTIBLE待ちとなる。なので、Ctrl+CでSIGINTで止めようとしても、外からSIGKILLしても、動かすことも殺すこともできない。復帰させるにはunfreeze(fsthaw)するしかない。

[root@tina ~]# cd ~rarul/test

[root@tina test]# fsfreeze -f mnt

これでもとのTerminalで実行したtouchも続きを実行してくれるので、後処理をしておこう。

[root@tina test]# umount mnt

[root@tina test]# exit
[rarul@tina test]# rm hoge.dat


コードを確認する


fsfreezeのコード

fsfreeze(8)はutil-linuxに含まれる。といっても1ファイル152行しかないので実物(util-linux/sys-utils/fsfreeze.c)見てもらったほうが早い。


fsfreeze.c

    switch (action) {

case FREEZE:
if (ioctl(fd, FIFREEZE, 0)) {
warn(_("%s: freeze failed"), path);
goto done;
}
break;
case UNFREEZE:
if (ioctl(fd, FITHAW, 0)) {
warn(_("%s: unfreeze failed"), path);
goto done;
}
break;

freezeさせたいファイルシステム内のファイルを適当に開いて、FIFREEZE, FITHAWのioctlをしていることがわかる。


kernelのコード(freeze)

FIFREEZEのioctlが呼ばれると、sys_ioctl()->do_vfs_ioctl()->ioctl_fsfreeze()と来る。kernel/fs/ioctl.c:547より、


ioctl.c

static int ioctl_fsfreeze(struct file *filp)

{
struct super_block *sb = file_inode(filp)->i_sb;

if (!capable(CAP_SYS_ADMIN))
return -EPERM;

/* If filesystem doesn't support freeze feature, return. */
if (sb->s_op->freeze_fs == NULL && sb->s_op->freeze_super == NULL)
return -EOPNOTSUPP;

/* Freeze */
if (sb->s_op->freeze_super)
return sb->s_op->freeze_super(sb);
return freeze_super(sb);
}


CAP_SYS_ADMIN権限が必要なことがわかる。また、.freeze_fs もしくは .freeze_super の実装がないFilesystemではfreezeできない。

次は .freeze_super の実装があるかどうかで分岐するが、.freeze_super があるのは現時点でgfs2のみなので、共通関数のfreeze_super()の方を見る。kernel/fs/super.c:1394より、


super.c

/**

* freeze_super - lock the filesystem and force it into a consistent state
* @sb: the super to lock
*
* Syncs the super to make sure the filesystem is consistent and calls the fs's
* freeze_fs. Subsequent calls to this without first thawing the fs will return
* -EBUSY.
*
* During this function, sb->s_writers.frozen goes through these values:
*
* SB_UNFROZEN: File system is normal, all writes progress as usual.
*
* SB_FREEZE_WRITE: The file system is in the process of being frozen. New
* writes should be blocked, though page faults are still allowed. We wait for
* all writes to complete and then proceed to the next stage.
*
* SB_FREEZE_PAGEFAULT: Freezing continues. Now also page faults are blocked
* but internal fs threads can still modify the filesystem (although they
* should not dirty new pages or inodes), writeback can run etc. After waiting
* for all running page faults we sync the filesystem which will clean all
* dirty pages and inodes (no new dirty pages or inodes can be created when
* sync is running).
*
* SB_FREEZE_FS: The file system is frozen. Now all internal sources of fs
* modification are blocked (e.g. XFS preallocation truncation on inode
* reclaim). This is usually implemented by blocking new transactions for
* filesystems that have them and need this additional guard. After all
* internal writers are finished we call ->freeze_fs() to finish filesystem
* freezing. Then we transition to SB_FREEZE_COMPLETE state. This state is
* mostly auxiliary for filesystems to verify they do not modify frozen fs.
*
* sb->s_writers.frozen is protected by sb->s_umount.
*/

int freeze_super(struct super_block *sb)
{
int ret;

atomic_inc(&sb->s_active);
down_write(&sb->s_umount);
if (sb->s_writers.frozen != SB_UNFROZEN) {
deactivate_locked_super(sb);
return -EBUSY;
}

if (!(sb->s_flags & MS_BORN)) {
up_write(&sb->s_umount);
return 0; /* sic - it's "nothing to do" */
}

if (sb->s_flags & MS_RDONLY) {
/* Nothing to do really... */
sb->s_writers.frozen = SB_FREEZE_COMPLETE;
up_write(&sb->s_umount);
return 0;
}

sb->s_writers.frozen = SB_FREEZE_WRITE;
/* Release s_umount to preserve sb_start_write -> s_umount ordering */
up_write(&sb->s_umount);
sb_wait_write(sb, SB_FREEZE_WRITE);
down_write(&sb->s_umount);

/* Now we go and block page faults... */
sb->s_writers.frozen = SB_FREEZE_PAGEFAULT;
sb_wait_write(sb, SB_FREEZE_PAGEFAULT);

/* All writers are done so after syncing there won't be dirty data */
sync_filesystem(sb);

/* Now wait for internal filesystem counter */
sb->s_writers.frozen = SB_FREEZE_FS;
sb_wait_write(sb, SB_FREEZE_FS);

if (sb->s_op->freeze_fs) {
ret = sb->s_op->freeze_fs(sb);
if (ret) {
printk(KERN_ERR
"VFS:Filesystem freeze failed\n");
sb->s_writers.frozen = SB_UNFROZEN;
sb_freeze_unlock(sb);
wake_up(&sb->s_writers.wait_unfrozen);
deactivate_locked_super(sb);
return ret;
}
}
/*
* For debugging purposes so that fs can warn if it sees write activity
* when frozen is set to SB_FREEZE_COMPLETE, and for thaw_super().
*/

sb->s_writers.frozen = SB_FREEZE_COMPLETE;
lockdep_sb_freeze_release(sb);
up_write(&sb->s_umount);
return 0;
}


こんなにちゃんとしたコメントなかなか見られないぞ。freeze_super()では排他とステップを意識した処理がなされる。まず新しくwriteしようとする人が入ってこないようにして、readのpagefaultも許さないように状態を変えて、dirtyなinodeをsync_filesystem()を呼んで書き出し、Filesystem固有のs_op->freeze_fsの関数を呼び、sb->s_writers.frozenをSB_FREEZE_COMPLETEに設定する。SB_UNFROZENなどについては、kernel/include/linux/fs.h:1277にある。


fs.h

/* Possible states of 'frozen' field */

enum {
SB_UNFROZEN = 0, /* FS is unfrozen */
SB_FREEZE_WRITE = 1, /* Writes, dir ops, ioctls frozen */
SB_FREEZE_PAGEFAULT = 2, /* Page faults stopped as well */
SB_FREEZE_FS = 3, /* For internal FS use (e.g. to stop
* internal threads if needed) */

SB_FREEZE_COMPLETE = 4, /* ->freeze_fs finished successfully */
};

ちゃんとコメントがあるってステキ、惚れちゃいそう(謎) Linuxカーネルでもちゃんとしたコメントがあるところばかりではないしね。

で、ext4の場合はs_op->freeze_fsの関数はext4_freeze()になる。kernel/fs/ext4/super.c:4802より、


super.c

/*

* LVM calls this function before a (read-only) snapshot is created. This
* gives us a chance to flush the journal completely and mark the fs clean.
*
* Note that only this function cannot bring a filesystem to be in a clean
* state independently. It relies on upper layer to stop all data & metadata
* modifications.
*/

static int ext4_freeze(struct super_block *sb)
{
int error = 0;
journal_t *journal;

if (sb->s_flags & MS_RDONLY)
return 0;

journal = EXT4_SB(sb)->s_journal;

if (journal) {
/* Now we set up the journal barrier. */
jbd2_journal_lock_updates(journal);

/*
* Don't clear the needs_recovery flag if we failed to
* flush the journal.
*/

error = jbd2_journal_flush(journal);
if (error < 0)
goto out;

/* Journal blocked and flushed, clear needs_recovery flag. */
ext4_clear_feature_journal_needs_recovery(sb);
}

error = ext4_commit_super(sb, 1);
out:
if (journal)
/* we rely on upper layer to stop further updates */
jbd2_journal_unlock_updates(journal);
return error;
}


ext4では、journalの書き出しとsuperblockの書き出しを行っている。s_op->freeze_fsには、inodeに紐付かない書き込みを行うことを期待しているようだ。これで無事にfsfreezeの処理が完了する。


kernelのコード(fsthaw)

FITHAWのioctlがなされると、FIFREEZEの場合とほぼ同様に、sys_ioctl()->do_vfs_ioctl()->ioctl_fsthaw()->thaw_super()と来る(s_op->thaw_superがあるのはgfs2のみ) thaw_super()はfreeze_super()とは違いかなりすっきりとしている。kernel/fs/super.c:1490より、


super.c

/**

* thaw_super -- unlock filesystem
* @sb: the super to thaw
*
* Unlocks the filesystem and marks it writeable again after freeze_super().
*/

int thaw_super(struct super_block *sb)
{
int error;

down_write(&sb->s_umount);
if (sb->s_writers.frozen != SB_FREEZE_COMPLETE) {
up_write(&sb->s_umount);
return -EINVAL;
}

if (sb->s_flags & MS_RDONLY) {
sb->s_writers.frozen = SB_UNFROZEN;
goto out;
}

lockdep_sb_freeze_acquire(sb);

if (sb->s_op->unfreeze_fs) {
error = sb->s_op->unfreeze_fs(sb);
if (error) {
printk(KERN_ERR
"VFS:Filesystem thaw failed\n");
lockdep_sb_freeze_release(sb);
up_write(&sb->s_umount);
return error;
}
}

sb->s_writers.frozen = SB_UNFROZEN;
sb_freeze_unlock(sb);
out:
wake_up(&sb->s_writers.wait_unfrozen);
deactivate_locked_super(sb);
return 0;
}
EXPORT_SYMBOL(thaw_super);


やはりこちらも排他を気にしつつ、s_op->unfreeze_fsを呼んで、SB_UNFROZENにしている。ext4の場合s_op->unfreeze_fsはext4_unfreeze()になる。kernel/fs/ext4/super.c:4844


super.c

/*

* Called by LVM after the snapshot is done. We need to reset the RECOVER
* flag here, even though the filesystem is not technically dirty yet.
*/

static int ext4_unfreeze(struct super_block *sb)
{
if ((sb->s_flags & MS_RDONLY) || ext4_forced_shutdown(EXT4_SB(sb)))
return 0;

if (EXT4_SB(sb)->s_journal) {
/* Reset the needs_recovery flag before the fs is unlocked. */
ext4_set_feature_journal_needs_recovery(sb);
}

ext4_commit_super(sb, 1);
return 0;
}


ext4_set_feature_journal_needs_recovery()は、「mount時にjournalをreplayする必要がある」という意味のsuerblock上のフラグ(RECOVER)を立てる。そういえば見落としていたけどext4_freeze()の中でext4_clear_feature_journal_needs_recovery()を呼んでいたなぁ。

と、いずれにしてもfreezeする場合に比べると、thawは、やるべきことやらエラーやらが少なくてややこしくない。


kernelのコード(待たされるwrite)

先の実験の通り、__sb_start_write()あたりでやっていることが確認できているので、sb_start_write()あたりから確認する。kernel/include/linux/fs.h:1484より、


fs.h

/**

* sb_start_write - get write access to a superblock
* @sb: the super we write to
*
* When a process wants to write data or metadata to a file system (i.e. dirty
* a page or an inode), it should embed the operation in a sb_start_write() -
* sb_end_write() pair to get exclusion against file system freezing. This
* function increments number of writers preventing freezing. If the file
* system is already frozen, the function waits until the file system is
* thawed.
*
* Since freeze protection behaves as a lock, users have to preserve
* ordering of freeze protection and other filesystem locks. Generally,
* freeze protection should be the outermost lock. In particular, we have:
*
* sb_start_write
* -> i_mutex (write path, truncate, directory ops, ...)
* -> s_umount (freeze_super, thaw_super)
*/

static inline void sb_start_write(struct super_block *sb)
{
__sb_start_write(sb, SB_FREEZE_WRITE, true);
}

static inline int sb_start_write_trylock(struct super_block *sb)
{
return __sb_start_write(sb, SB_FREEZE_WRITE, false);
}

/**
* sb_start_pagefault - get write access to a superblock from a page fault
* @sb: the super we write to
*
* When a process starts handling write page fault, it should embed the
* operation into sb_start_pagefault() - sb_end_pagefault() pair to get
* exclusion against file system freezing. This is needed since the page fault
* is going to dirty a page. This function increments number of running page
* faults preventing freezing. If the file system is already frozen, the
* function waits until the file system is thawed.
*
* Since page fault freeze protection behaves as a lock, users have to preserve
* ordering of freeze protection and other filesystem locks. It is advised to
* put sb_start_pagefault() close to mmap_sem in lock ordering. Page fault
* handling code implies lock dependency:
*
* mmap_sem
* -> sb_start_pagefault
*/

static inline void sb_start_pagefault(struct super_block *sb)
{
__sb_start_write(sb, SB_FREEZE_PAGEFAULT, true);
}

/*
* sb_start_intwrite - get write access to a superblock for internal fs purposes
* @sb: the super we write to
*
* This is the third level of protection against filesystem freezing. It is
* free for use by a filesystem. The only requirement is that it must rank
* below sb_start_pagefault.
*
* For example filesystem can call sb_start_intwrite() when starting a
* transaction which somewhat eases handling of freezing for internal sources
* of filesystem changes (internal fs threads, discarding preallocation on file
* close, etc.).
*/

static inline void sb_start_intwrite(struct super_block *sb)
{
__sb_start_write(sb, SB_FREEZE_FS, true);
}


ちょっとコメントが長いけど、次の__sb_start_write()の関数のコメントにある通り、sb_start_write(), sb_start_pagefault(), sb_start_intwrite()はそれぞれ、LEVELを変えて__sb_start_write()を呼んでいる。kernel/fs/super.c:1311より、


super.c

/*

* This is an internal function, please use sb_start_{write,pagefault,intwrite}
* instead.
*/

int __sb_start_write(struct super_block *sb, int level, bool wait)
{
bool force_trylock = false;
int ret = 1;

#ifdef CONFIG_LOCKDEP
/*
* We want lockdep to tell us about possible deadlocks with freezing
* but it's it bit tricky to properly instrument it. Getting a freeze
* protection works as getting a read lock but there are subtle
* problems. XFS for example gets freeze protection on internal level
* twice in some cases, which is OK only because we already hold a
* freeze protection also on higher level. Due to these cases we have
* to use wait == F (trylock mode) which must not fail.
*/

if (wait) {
int i;

for (i = 0; i < level - 1; i++)
if (percpu_rwsem_is_held(sb->s_writers.rw_sem + i)) {
force_trylock = true;
break;
}
}
#endif
if (wait && !force_trylock)
percpu_down_read(sb->s_writers.rw_sem + level-1);
else
ret = percpu_down_read_trylock(sb->s_writers.rw_sem + level-1);

WARN_ON(force_trylock && !ret);
return ret;
}


percpu_down_read()の実装が見ても理解できなかったけど(特にpercpuな実装である理由)、まぁlevelの数だけあるsemaphoreの該当番目のを取るんだろう。


fsfreezeの用途

fsfreezeは主に、Filesystemの乗った(仮想)ブロックデバイスのスナップショットを取る目的で使われる(と思っている)。が、調べてみる限り、fs以外からくるのは__dm_suspend()->lock_fs()->freeze_bdev()->freeze_super()くらいしかなさそう。なのでLVMなどはツール類(ユーザランド)から直接FIFREEZEのioctlをしているんじゃないかと思う。

またこれとは別にEXT4_IOC_SHUTDOWNなんていうioctlもあり、これがfreeze_bdev()を使っている(Linux-4.11でEXT4_IOC_GOINGDOWNからEXT4_IOC_SHUTDOWNに変わった)   なんの目的で入ったかわからず、直接FIFREEZEのioctlすればいい気もするけど、まぁもっとext4 friendlyなツールを作る目的でもあるのかもしれない。

まぁ、強制的にシャットダウンさせたいけど強制的にumountできないなんて場合には役に立つのかもしれない。ちょっとだけ触れたext4のRECOVERYに絡むけど、後処理しておかないと次回mountが遅くなっちゃうんで。私のサイトへ自分でリンクする形で申し訳ないけど、参考、


あとがき

fsfreezeはもともとFilesystemのスナップショットを取る目的で作られたと思うんだけど、今は、Filesystemそのものに加えられたCheckpoint機能が優秀で、そっちに取って代わられた、と理解している。もちろんオンラインでメンテナンスしたい場合に使えるんだろうけど、素人目にはなんとなく、remountでreadonlyにしてしまった方が楽に思えなくもない。玄人的にはどうせZFSとかに頼ることになるんだろうし。

異なる視点でいくと、シャットダウン時に強制的にumountしたくてもなかなか全員をkillしてられないような場合には使えるのかもしれない。ただ、少しだけ紹介したEXT4_IOC_SHUTDOWNのように、Filesystemで機能を持ってしまったほうが使い勝手もよいのかもしれない。

fsfreezeっていったい何に使われてるんでしょうか。。


参考サイト