본문 바로가기
Kernel

Page Cache: write

by hyeyoo 2022. 3. 9.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.

글을 좀 오랜만에 쓴다. 이것저것 하느라 글을 그동안 못썼다. 흠, 다음 글은 뭐쓰지?

이전 글

 

VFS: read_iter() & write_iter()

원래는 페이지 캐시를 정리하려고 했는데 정리하다보니 read/write 매커니즘을 정리할 수밖에 없었다. 이 글에선 read_iter()가 어떻게 동작한는지 간단하게 알아본다. 관련 글 이 글은 가상 파일시스

hyeyoo.com

 

Page Cache: filemap_read

이전 글 이전 글에서는 read() 시스템 호출에서 어떤 일이 일어나는지 알아봤다. read() 시스템 호출은 모드와 권한이 적절한지 확인한 후, 1) Direct IO를 수행하거나 2) filemap_read()로 페이지 캐시에서

hyeyoo.com

앞선 글에서 read()를 할 때 페이지 캐시의 동작을 살펴봤으므로 이번에는 write()할 때의 동작을 살펴보자.

큰 그림

쓰기가 수행될 때도 디스크와 항상 동기화하는건 아니다. 일단 페이지 캐시에 데이터를 쓴 다음에 나중에 동기화한다.

페이지에 데이터를 씀으로써 페이지와 내용과 디스크의 파일이 달라지면 커널은 동기화를 해야한다. 이렇게 페이지와 디스크의 내용이 달라졌을 때 그 페이지를 dirty page라고 부른다. 디스크와 내용이 같은 것들은 clean page라고 한다.

커널은 주기적으로 페이지 캐시의 내용을 디스크와 동기화한다.

__generic_file_write_iter()

이 함수는 generic한 파일시스템에 쓰기를 수행하는 함수로, 많은 파일시스템들이 이 함수의 래퍼 함수인 generic_file_write_iter()를 write_iter()에 사용하고 있다.

/**
 * __generic_file_write_iter - write data to a file
 * @iocb:	IO state structure (file, offset, etc.)
 * @from:	iov_iter with data to write
 *
 * This function does all the work needed for actually writing data to a
 * file. It does all basic checks, removes SUID from the file, updates
 * modification times and calls proper subroutines depending on whether we
 * do direct IO or a standard buffered write.
 *
 * It expects i_rwsem to be grabbed unless we work on a block device or similar
 * object which does not need locking at all.
 *
 * This function does *not* take care of syncing data in case of O_SYNC write.
 * A caller has to handle it. This is mainly due to the fact that we want to
 * avoid syncing under i_rwsem.
 *
 * Return:
 * * number of bytes written, even for truncated writes
 * * negative error code if no data has been written at all
 */
ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *file = iocb->ki_filp;
	struct address_space *mapping = file->f_mapping;
	struct inode 	*inode = mapping->host;
	ssize_t		written = 0;
	ssize_t		err;
	ssize_t		status;

	/* We can write back this queue in page reclaim */
	current->backing_dev_info = inode_to_bdi(inode);
	err = file_remove_privs(file);
	if (err)
		goto out;

	err = file_update_time(file);
	if (err)
		goto out;
	if (iocb->ki_flags & IOCB_DIRECT) {
		loff_t pos, endbyte;

		written = generic_file_direct_write(iocb, from);

저번 글에서 알아보았듯 일부 어플리케이션은 페이지 캐시를 직접 관리하기 때문에 운영체제에서 페이지 캐시를 사용할 이유가 없다. 따라서 iocb->ki_flags에 IOCB_DIRECT가 설정된 경우에는 페이지 캐시를 거치지 않고 직접 디스크에 쓴다.

		/*
		 * If the write stopped short of completing, fall back to
		 * buffered writes.  Some filesystems do this for writes to
		 * holes, for example.  For DAX files, a buffered write will
		 * not succeed (even if it did, DAX does not handle dirty
		 * page-cache pages correctly).
		 */
		if (written < 0 || !iov_iter_count(from) || IS_DAX(inode))
			goto out;

		status = generic_perform_write(file, from, pos = iocb->ki_pos);
		/*
		 * If generic_perform_write() returned a synchronous error
		 * then we want to return the number of bytes which were
		 * direct-written, or the error code if that was zero.  Note
		 * that this differs from normal direct-io semantics, which
		 * will return -EFOO even if some bytes were written.
		 */
		if (unlikely(status < 0)) {
			err = status;
			goto out;
		}

direct IO가 실패했거나 (written < 0) 모든 데이터를 쓰기에 성공한 경우 (iov_iter_count(from) == 0)에는 함수를 종료하지만, direct IO가 중간에 종료된 경우에는 페이지 캐시를 통해서 generic_perform_write()를 호출해 buffered write를 수행한다.

		/*
		 * We need to ensure that the page cache pages are written to
		 * disk and invalidated to preserve the expected O_DIRECT
		 * semantics.
		 */
		endbyte = pos + status - 1;
		err = filemap_write_and_wait_range(mapping, pos, endbyte);
		if (err == 0) {
			iocb->ki_pos = endbyte + 1;
			written += status;
			invalidate_mapping_pages(mapping,
						 pos >> PAGE_SHIFT,
						 endbyte >> PAGE_SHIFT);
		} else {
			/*
			 * We don't know how much we wrote, so just return
			 * the number of bytes which were direct-written
			 */
		}

buffered write를 수행한 후에는 filemap_write_and_wait_range()로 페이지 캐시에 있는 내용을 디스크와 동기화하고, invalidate_mapping_pages()로 디스크에 기록한 페이지 중, clean하고 (dirty하지 않은), 아무도 lock하지 않은 페이지들을 캐시에서 제거한다. 여기서 사용하지 않는 페이지 캐시를 바로 없애는건 direct IO를 하기 때문에 그렇다. buffered IO를 할 때는 바로 없애지 않는다.

	} else {
		written = generic_perform_write(file, from, iocb->ki_pos);
		if (likely(written > 0))
			iocb->ki_pos += written;
	}
out:
	current->backing_dev_info = NULL;
	return written ? written : err;
}
EXPORT_SYMBOL(__generic_file_write_iter);

IOCB_DIRECT가 명시되지 않은 경우에는 바로 generic_perform_write()로 페이지 캐시에 buffered write를 수행한다.

generic_perform_write()

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;
	long status = 0;
	ssize_t written = 0;
	unsigned int flags = 0;

	do {
		struct page *page;
		unsigned long offset;	/* Offset into pagecache page */
		unsigned long bytes;	/* Bytes to write to page */
		size_t copied;		/* Bytes copied from user */
		void *fsdata;

		offset = (pos & (PAGE_SIZE - 1));
		bytes = min_t(unsigned long, PAGE_SIZE - offset,
						iov_iter_count(i));

쓸 데이터의 크기, 오프셋 계산하는 부분

again:
		/*
		 * Bring in the user page that we will copy from _first_.
		 * Otherwise there's a nasty deadlock on copying from the
		 * same page as we're writing to, without it being marked
		 * up-to-date.
		 */
		if (unlikely(fault_in_iov_iter_readable(i, bytes))) {
			status = -EFAULT;
			break;
		}

		if (fatal_signal_pending(current)) {
			status = -EINTR;
			break;
		}

현재 쓰려는 부분에 써도 되는지(이건 뭔지 잘 모르겠다.), 시그널 확인하는 부분

		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);

실제로 데이터를 쓰는 부분은 a_ops->write_begin() ~ a_ops->write_end()로 감싸져있다. write_begin()에서는 파일시스템별로 쓰기를 준비하는 부분이며, write_begin()이 성공하면 파라미터로 넘겼던 &page에다가 쓰기를 한 후 write_end()를 호출하면 된다.

writeably mapped page가 있는지 확인하는건 저번 글에서 알아본 D-cache aliasing 문제를 피하기 위해 페이지 캐시에 쓰기 전후로 flush_dcache_page()를 호출해준다.

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

		status = a_ops->write_end(file, mapping, pos, bytes, copied,
						page, fsdata);
		if (unlikely(status != copied)) {
			iov_iter_revert(i, copied - max(status, 0L));
			if (unlikely(status < 0))
				break;
		}
		cond_resched();

이제 copy_page_from_iter_atomic()으로 page에 데이터를 복사하고, 다시 flush_dcache_page()를 해준 후 write_end()를 호출한다. 근데 만약 write_end()가 실패하면 파일시스템이 제대로 쓰기를 처리하지 못한 것이므로 iter를 원래대도 되돌려서 다음에 다시 쓸 수 있도록 한다.

		if (unlikely(status == 0)) {
			/*
			 * A short copy made ->write_end() reject the
			 * thing entirely.  Might be memory poisoning
			 * halfway through, might be a race with munmap,
			 * might be severe memory pressure.
			 */
			if (copied)
				bytes = copied;
			goto again;
		}
		pos += status;
		written += status;

만약 write_end()에서 부분적으로라도 데이터를 쓴게 아니라 하나도 쓰지 못했다면 다시 시도한다.

		balance_dirty_pages_ratelimited(mapping);
	} while (iov_iter_count(i));

	return written ? written : status;
}
EXPORT_SYMBOL(generic_perform_write);

쓰기를 한 후에는 balance_dirty_pages_ratelimited()를 호출한다. 이 함수는 페이지를 dirty화 할때마다 호출해주는 함수인데, dirty 페이지가 일정 수준을 넘어서면 페이지들을 디스크와 동기화한다.

 

filemap.c - mm/filemap.c - Linux source code (v5.16.10) - Bootlin

 

elixir.bootlin.com

https://www.kernel.org/doc/Documentation/filesystems/vfs.txt

댓글