Linux の close は fsync 相当を調べる

Linuxのcloseは暗にfsyncするから、ここであげられている

  • 100000回繰り返し
  • open
  • 8K write
  • close

というパターンだとfsyncコストが見えちゃうので良くないんじゃないかな

とのことで、そうなのか! と思ったので例によって深追いしてみました。

まず fsync(2) の実装は fs/sync.c にあります。

asmlinkage long sys_fsync(unsigned int fd)
{
        return __do_fsync(fd, 0);
}

static long __do_fsync(unsigned int fd, int datasync)
{
        struct file *file;
        int ret = -EBADF;

        file = fget(fd);
        if (file) {
                ret = do_fsync(file, datasync);
                fput(file);
        }
        return ret;
}

sys_fsync() → __do_fsync() と進んで

  • do_fsync() = 対象になるファイルオブジェクトの file_operations の fsync コールバックを呼ぶ、ページキャッシュを書き出す
  • fput() = 色々開放

をする。以下は do_sync() の中身。

long do_fsync(struct file *file, int datasync)
{
        int ret;
        int err;
        struct address_space *mapping = file->f_mapping;

        if (!file->f_op || !file->f_op->fsync) {
                /* Why?  We can still call filemap_fdatawrite */
                ret = -EINVAL;
                goto out;
        }

        ret = filemap_fdatawrite(mapping);

        /*
         * We need to protect against concurrent writers, which could cause
         * livelocks in fsync_buffers_list().
         */
        mutex_lock(&mapping->host->i_mutex);
        err = file->f_op->fsync(file, file->f_path.dentry, datasync);
        if (!ret)
                ret = err;
        mutex_unlock(&mapping->host->i_mutex);
        err = filemap_fdatawait(mapping);
        if (!ret)
                ret = err;
out:
        return ret;
}

を見るに file->f_op->fsync でファイルシステム実装毎の fsync コールバックを呼んだあと、filemap_fdatawrite() で address_space 構造体につながっているページを引きずり出して dirty なページを書き出すとかそんな処理をしているように推測。fsync によるハードウェアへの書き出し処理はここが核になりそう。

一方の fput()。

void fastcall fput(struct file *file)
{
        if (atomic_dec_and_test(&file->f_count))
                __fput(file);
}

とあって、atomic_dec_and_test() はファイルオブジェクトの f_count (参照カウンタ) を調べて 0 だったら true を返す。つまり fput() による解放処理は、該当ファイルオブジェクトが他のどこからか参照されてるあいだは動かない。リファレンスカウンタ方式。

辿っていくと

void fastcall __fput(struct file *file)
{
        struct dentry *dentry = file->f_path.dentry;
        struct vfsmount *mnt = file->f_path.mnt;
        struct inode *inode = dentry->d_inode;

        might_sleep();

        fsnotify_close(file);
        /*
         * The function eventpoll_release() should be the first called
         * in the file cleanup chain.
         */
        eventpoll_release(file);
        locks_remove_flock(file);

        if (file->f_op && file->f_op->release)
                file->f_op->release(inode, file);
        security_file_free(file);
        if (unlikely(S_ISCHR(inode->i_mode) && inode->i_cdev != NULL))
                cdev_put(inode->i_cdev);
        fops_put(file->f_op);
        if (file->f_mode & FMODE_WRITE)
                put_write_access(inode);
        put_pid(file->f_owner.pid);
        file_kill(file);
        file->f_path.dentry = NULL;
        file->f_path.mnt = NULL;
        file_free(file);
        dput(dentry);
        mntput(mnt);
}

にたどり着く。file->f_op->release でファイルシステム毎の release コールバックを呼んだ後、色々と雑多なものを解放する処理を行っているっぽい。手元には 2.4 のカーネル本しかないけど

引数としてファイルオブジェクトのアドレスを受け取り、利用カウンタ f_count の値を 1 減らします。さらに f_count が 0 になると、fput() は(定義されていれば)ファイル操作の release メソッドを呼び出し、対応する dエントリオブジェクトとファイルシステムディスクリプタを解放し、(ファイルが書き込み用にオープンされている場合には)iノードオブジェクトの i_writeaccess メンバの値も1減らし、最後に「使用中」のリストから「未使用」のリストへとファイルオブジェクトを移動します。

とある。ファイルオブジェクトの解放という処理のうち、カーネル内の各種データ構造やフラグを調整する処理を担当しているという感じでしょうか。

ということで fsync(2) は

  • do_sync() でハードウェアと同期して
  • fput() で色々クリアにする

ということをやっているようです。

次に close(2) を見てみます。

/*
 * Careful here! We test whether the file pointer is NULL before
 * releasing the fd. This ensures that one clone task can't release
 * an fd while another clone is opening it.
 */
asmlinkage long sys_close(unsigned int fd)
{
        struct file * filp;
        struct files_struct *files = current->files;
        struct fdtable *fdt;
        int retval;

        spin_lock(&files->file_lock);
        fdt = files_fdtable(files);
        if (fd >= fdt->max_fds)
                goto out_unlock;
        filp = fdt->fd[fd];
        if (!filp)
                goto out_unlock;
        rcu_assign_pointer(fdt->fd[fd], NULL);
        FD_CLR(fd, fdt->close_on_exec);
        __put_unused_fd(files, fd);
        spin_unlock(&files->file_lock);
        retval = filp_close(filp, files);

        /* can't restart close syscall because file table entry was cleared */
        if (unlikely(retval == -ERESTARTSYS ||
                     retval == -ERESTARTNOINTR ||
                     retval == -ERESTARTNOHAND ||
                     retval == -ERESTART_RESTARTBLOCK))
                retval = -EINTR;

        return retval;

out_unlock:
        spin_unlock(&files->file_lock);
        return -EBADF;
}

ごにょごにょとやってるけど中核は filp_close() の中っぽいですね。

/*
 * "id" is the POSIX thread ID. We use the
 * files pointer for this..
 */
int filp_close(struct file *filp, fl_owner_t id)
{
        int retval = 0;

        if (!file_count(filp)) {
                printk(KERN_ERR "VFS: Close: file count is 0\n");
                return 0;
        }

        if (filp->f_op && filp->f_op->flush)
                retval = filp->f_op->flush(filp, id);

        dnotify_flush(filp, id);
        locks_remove_posix(filp, id);
        fput(filp);
        return retval;
}

fsync(2) のときは file_operations 関連は release が呼ばれるだけだったけど、close(2) のときは flush も呼ばれる。けど ext3 には flush は定義されてない。(ので ext3 では if (filep->f_op) が偽になってスキップされる。reiserfs も flush なし、xfs にはあった。)

ということで

  • dnotify_flush()
  • fput()

が close(2) 処理の中核になりそう。

fput() は先に調べたとおりで dnotify_flush() は? http://hira.main.jp/wiki/pukiwiki.php?dnotify_flush()%2Flinux2.6 を見るにディレクトリ通知オブジェクトなるオブジェクトを解放する処理だそう。ディレクトリ通知オブジェクトというのがよくわかってないけど、まあそのファイルが帰属する dentry オブジェクトに緋もづくなんらかのオブジェクトっぽい。

深追ってみたけど fsync(2) にはページキャッシュのディスクへの書き出し処理はあるけど close(2) にはそれっぽいものは見つけられなかった。kosaki さんが言ってる「close は暗に fsync 相当」というのは fput で各種リソースを解放してるところがそれに当たるってことなんでしょか。(そこのオーバーヘッドが大きい?) それとも書き出し処理を見落としてたりするでしょか。