글을 좀 오랜만에 쓴다. 이것저것 하느라 글을 그동안 못썼다. 흠, 다음 글은 뭐쓰지?
이전 글
앞선 글에서 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 페이지가 일정 수준을 넘어서면 페이지들을 디스크와 동기화한다.
https://www.kernel.org/doc/Documentation/filesystems/vfs.txt
'Kernel' 카테고리의 다른 글
메모리 일관성 모델 (Memory Consistency Model) (6) | 2022.08.04 |
---|---|
Page Cache: filemap_read (3) | 2022.02.09 |
VFS: read_iter() & write_iter() (0) | 2022.01.24 |
Direct Memory Access API (1) | 2021.12.29 |
[Linux Kernel] 리눅스는 얼마나 작아질 수 있을까? (1) | 2021.12.05 |
댓글