LoginSignup
2
2

More than 3 years have passed since last update.

Linuxのページキャッシュのwrite back処理概要

Posted at

はじめに

ページキャッシュのwrite backを行うまでの流れに関して記憶があいまいであったため、ソースコードを確認した。せっかくなので、メモをする。
参照したLinuxカーネルのバージョンは5.10.20である。

ページキャッシュにwriteするまでの流れ

write()システムコールからたどる

Buffered I/Oがどのようになっているのか、writeから読んでいく。

fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, 
        size_t, count)
{
    return ksys_write(fd, buf, count);
}
fs/read_write.c
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
// 略
     if (file->f_op->write)
         ret = file->f_op->write(file, buf, count, pos);
     else if (file->f_op->write_iter)
         ret = new_sync_write(file, buf, count, pos);
     else
         ret = -EINVAL;

この文書では、ファイルシステムはEXT4を題材にしてコードを読むこととしたい。file_operations構造体は以下のようになっており、writeハンドラはセットされない。

fs/ext4/file.c
const struct file_operations ext4_file_operations = {
    .llseek     = ext4_llseek,
    .read_iter  = ext4_file_read_iter,
    .write_iter = ext4_file_write_iter,

よって、今回の場合、new_sync_write()が呼ばれることになる。new_sync_write()は、本質的にはext4_file_write_iter()を呼び出すための関数である。

fs/ext4/file.c
static ssize_t
ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    struct inode *inode = file_inode(iocb->ki_filp);

    if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb))))
        return -EIO;

#ifdef CONFIG_FS_DAX
    if (IS_DAX(inode))
        return ext4_dax_write_iter(iocb, from);
#endif
    if (iocb->ki_flags & IOCB_DIRECT)
        return ext4_dio_write_iter(iocb, from);
    else
        return ext4_buffered_write_iter(iocb, from);
}

今回は、ページキャッシュのwrite backの概要を知りたいので、ext4_buffered_write_iter()を読む。

fs/ext4/file.c
static ssize_t ext4_buffered_write_iter(struct kiocb *iocb,
                    struct iov_iter *from)
{
// 略
    ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos);
// 略

generic_perform_write()は、以下のようになっており、a_ops->write_begin()とa_ops->write_end()に挟まれて、iov_iter_copy_from_user_atomic()が呼び出されている。

mm/filemap.c
ssize_t generic_perform_write(struct file *file,
                struct iov_iter *i, loff_t pos)
{
    struct address_space *mapping = file->f_mapping;
    const struct address_space_operations *a_ops = mapping->a_ops;
// 略
    do {
// 略
        status = a_ops->write_begin(file, mapping, pos, bytes, flags,
                        &page, &fsdata);
        if (unlikely(status < 0))
            break;

        if (mapping_writably_mapped(mapping))
            flush_dcache_page(page);

        copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
        flush_dcache_page(page);

        status = a_ops->write_end(file, mapping, pos, bytes, copied,
                        page, fsdata);
// 略

上の処理の概要は以下のとおりである。

  • a_ops->write_begin()ではページキャッシュを取得する。
  • iov_iter_copy_from_user_atomic()では、ユーザ空間から渡された書き込みデータを該当ページキャッシュにコピーする。この時点では、RAMからRAMへのコピーである。
  • a->ops->write_end()では、主に該当ページキャッシュの状態がダーティーに遷移したときの処理を行う。実は、この中でwrite backの処理をWorkqueue経由で依頼する。

write_beginおよびwrite_end

write_begin, write_endについては、一番シンプルそうな関数を選んで読むことにする。

fs/ext4/inode.c
static const struct address_space_operations ext4_aops = {
    .readpage       = ext4_readpage,
    .readahead      = ext4_readahead,
    .writepage      = ext4_writepage,
    .writepages     = ext4_writepages,
    .write_begin        = ext4_write_begin,
    .write_end      = ext4_write_end,
    .set_page_dirty     = ext4_set_page_dirty,

write_begin

write_beginから読む。先に書いたとおり、ページキャッシュの取得をしている。ページキャッシュがない場合、ストレージ(バッキングストア)から読み出してFillするか、0-fillされたページを用意する。いずれにせよ、ページキャッシュとして何らかのメモリページを渡す。

今回は「ページキャッシュのwrite backの概要」が知りたいので、ページキャッシュを取得する処理の詳細は割愛する。

static int ext4_write_begin(struct file *file, struct address_space *mapping,
                loff_t pos, unsigned len, unsigned flags,
                struct page **pagep, void **fsdata)
{
// 略
retry_grab:
    page = grab_cache_page_write_begin(mapping, index, flags);
    if (!page)
        return -ENOMEM;
// 略
#ifdef CONFIG_FS_ENCRYPTION
// 略
#else
    if (ext4_should_dioread_nolock(inode))
        ret = __block_write_begin(page, pos, len,
                      ext4_get_block_unwritten);
    else
        ret = __block_write_begin(page, pos, len, ext4_get_block);
#endif
    if (!ret && ext4_should_journal_data(inode)) {
        ret = ext4_walk_page_buffers(handle, page_buffers(page),
                         from, to, NULL,
                         do_journal_get_write_access);
    }

write_end

先に書いたとおり、write_end()では、ダーティな状態への遷移を主に取り扱っている。

fs/ext4/inode.c
static int ext4_write_end(struct file *file,
              struct address_space *mapping,
              loff_t pos, unsigned len, unsigned copied,
              struct page *page, void *fsdata)
{
// 略
    if (inline_data) {
// 略
    } else
        copied = block_write_end(file, mapping, pos,
                     len, copied, page, fsdata);

block_write_end()から、__block_commit_write() --> mark_buffer_dirty() --> __mark_inode_dirty()のルートをたどる。

fs/buffer.c
int block_write_end(struct file *file, struct address_space *mapping,
            loff_t pos, unsigned len, unsigned copied,
            struct page *page, void *fsdata)
{          
    // 略
    /* This could be a short (even 0-length) commit */
    __block_commit_write(inode, page, start, start+copied);
    // 略

__mark_inode_dirty()は以下のとおりであり、wb_wakeup_delayed()を呼ぶ。

fs/fs-writeback.c
void __mark_inode_dirty(struct inode *inode, int flags)
{
// 略
    if ((inode->i_state & flags) != flags) {
// 略
        if (!was_dirty) {
// 略
            if (wakeup_bdi &&
                (wb->bdi->capabilities & BDI_CAP_WRITEBACK))
                wb_wakeup_delayed(wb);
            return;
        }
    }
// 略

また、wb_wakeup_delayed()は以下のようになっており、queue_delayed_work()を通して、dirty_writeback_internal * 10 ミリ秒後に起動するようにWorkqueueに依頼する。

mm/backing-dev.c
void wb_wakeup_delayed(struct bdi_writeback *wb)
{
    unsigned long timeout;

    timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
    spin_lock_bh(&wb->work_lock);
    if (test_bit(WB_registered, &wb->state))
        queue_delayed_work(bdi_wq, &wb->dwork, timeout);
    spin_unlock_bh(&wb->work_lock);
}

デフォルトでは、dirty_writeback_internal は500である。よって、500 * 10ミリ秒 = 5秒がデフォルトの遅延時間となる。

mm/page-writeback.c
unsigned int dirty_writeback_interval = 5 * 100; /* centiseconds */

write back処理

先のqueue_delayed_work()で遅延処理されるのは、以下のとおりwb_workfnである。

mm/backing-dev.c
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
           gfp_t gfp)
{
// 略
    INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
// 略

wb_workfn()は以下の関数で、通常想定されるルートでは、wb_do_writeback()を呼び出してwrite backを行う。

割愛するが、最終的にはwb_do_writeback()からいくつかの関数呼び出しを経てdo_writepages()が呼ばれ、その中でページャ経由でのWriteが行われる。

fs/fs-writeback.c
void wb_workfn(struct work_struct *work)
{   
    struct bdi_writeback *wb = container_of(to_delayed_work(work),
                        struct bdi_writeback, dwork);
// 略
    current->flags |= PF_SWAPWRITE;

    if (likely(!current_is_workqueue_rescuer() ||
           !test_bit(WB_registered, &wb->state))) {
        /*
         * The normal path.  Keep writing back @wb until its
         * work_list is empty.  Note that this path is also taken
         * if @wb is shutting down even when we're running off the
         * rescuer as work_list needs to be drained.
         */
        do {
            pages_written = wb_do_writeback(wb);
            trace_writeback_pages_written(pages_written);
        } while (!list_empty(&wb->work_list));
    } else {
// 略
    }

    if (!list_empty(&wb->work_list))
        wb_wakeup(wb);
    else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
        wb_wakeup_delayed(wb);

    current->flags &= ~PF_SWAPWRITE;
}
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2