はじめに
ページキャッシュのwrite backを行うまでの流れに関して記憶があいまいであったため、ソースコードを確認した。せっかくなので、メモをする。
参照したLinuxカーネルのバージョンは5.10.20である。
ページキャッシュにwriteするまでの流れ
write()システムコールからたどる
Buffered I/Oがどのようになっているのか、writeから読んでいく。
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
return ksys_write(fd, buf, count);
}
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ハンドラはセットされない。
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()を呼び出すための関数である。
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()を読む。
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()が呼び出されている。
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については、一番シンプルそうな関数を選んで読むことにする。
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()では、ダーティな状態への遷移を主に取り扱っている。
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()のルートをたどる。
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()を呼ぶ。
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に依頼する。
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秒がデフォルトの遅延時間となる。
unsigned int dirty_writeback_interval = 5 * 100; /* centiseconds */
write back処理
先のqueue_delayed_work()で遅延処理されるのは、以下のとおりwb_workfnである。
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が行われる。
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;
}