사용자 buffer에서 page cache로 데이터를 복사했는데 filesystem이 그 전부를 받아들이지 않을 수 있습니다. 사용자 주소에 fault가 났거나, write-end 처리가 부분 복사를 거절했거나, 더 작은 단위로 다시 시도해야 하는 경우입니다. buffered write의 진행량은 단순한 메모리 복사량이 아닙니다.
upstream v6.18의 generic_perform_write()는 준비, 복사, 확정이라는 세 단계를 반복합니다. 실제 block 배치와 최종 저장은 filesystem이 맡지만, 공통 경로도 lock을 가진 상태의 사용자 접근과 short write를 세심하게 처리해야 합니다.
write_iter를 쓴다고 direct I/O인 것은 아닙니다
일반적인 write syscall은 VFS를 거쳐 filesystem의 write operation에 도달합니다. write_iter interface를 쓰는 filesystem이라도 buffered write일 수 있습니다. kiocb는 file, position과 I/O flag를 담고 iov_iter는 사용자 데이터의 남은 범위를 표현합니다.
generic wrapper는 크기와 쓰기 가능 여부 등을 검사하고 inode의 변경을 직렬화합니다. __generic_file_write_iter()는 setuid 같은 privilege 제거와 timestamp 갱신을 처리하고 buffered 또는 direct 경로로 내려갑니다. 이 내부 helper가 O_SYNC의 최종 동기화까지 모두 수행하지는 않습니다. 상위 wrapper는 inode lock을 놓은 뒤 generic_write_sync()를 호출하여 불필요한 lock 보유 중 대기를 피합니다. v6.18 mm/filemap.c
버전에 따라 task의 backing-device 관련 필드를 임시로 설정하던 코드나 callback 인자가 다르게 보일 수 있습니다. 이런 구현 세부를 현재의 필수 API로 외우기보다 확인하는 release의 함수 signature와 wrapper 계약을 함께 읽는 편이 정확합니다.
아래는 v6.18의 두 write wrapper 전체입니다. 바깥 함수가 inode lock 아래에서 generic_write_checks()를 수행하고 양수인 경우에만 실제 write로 내려갑니다. 안쪽 함수는 privilege 제거와 timestamp 갱신 실패를 그대로 반환합니다. current 코드에는 예전 current->backing_dev_info를 설정·해제하던 줄이 없습니다.
generic_file_write_iter_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
ssize_t generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct file *file = iocb->ki_filp;
struct inode *inode = file->f_mapping->host;
ssize_t ret;
inode_lock(inode);
ret = generic_write_checks(iocb, from);
if (ret > 0)
ret = __generic_file_write_iter(iocb, from);
inode_unlock(inode);
if (ret > 0)
ret = generic_write_sync(iocb, ret);
return ret;
}
__generic_file_write_iter_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
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 ret;
ret = file_remove_privs(file);
if (ret)
return ret;
ret = file_update_time(file);
if (ret)
return ret;
if (iocb->ki_flags & IOCB_DIRECT) {
ret = generic_file_direct_write(iocb, from);
/*
* 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 (ret < 0 || !iov_iter_count(from) || IS_DAX(inode))
return ret;
return direct_write_fallback(iocb, from, ret,
generic_perform_write(iocb, from));
}
return generic_perform_write(iocb, from);
}
안쪽 전체 정의에 있는 direct-I/O 분기도 자르지 않았습니다. 남은 데이터가 있고 DAX가 아닌 경우 buffered fallback을 시도할 수 있지만, direct write가 error이거나 남은 데이터가 없거나 DAX이면 같은 fallback을 수행하지 않습니다. 이 분기는 buffered loop가 모든 filesystem의 direct-I/O implementation도 대신한다는 뜻은 아닙니다.
write_begin은 복사 가능한 folio를 준비합니다
write_begin은 대상 offset에 대응하는 folio와 filesystem private state를 준비합니다. cache miss라면 allocation이 필요할 수 있고, 파일의 일부 block만 덮어쓴다면 덮어쓰지 않을 byte를 보존하기 위한 read도 필요할 수 있습니다. 새 block의 zeroing이나 allocation 실패 정리도 filesystem의 책임에 들어갑니다.
buffer 기반 경로에서는 cache folio를 locked 상태로 얻고, write와 stable 요구에 맞는 준비를 거쳐 block mapping을 구성합니다. 과거 grab_cache_page_write_begin()의 page·NULL 중심 API와 현재 folio·error pointer 중심 API를 섞어서는 안 됩니다. 같은 목적이라도 호출자가 확인해야 할 실패 표현이 달라집니다.
준비에 성공했다고 요청한 최대 chunk가 모두 그 folio 안에 들어간다는 보장은 없습니다. v6.18 공통 loop는 offset_in_folio()를 구한 다음 실제 folio 끝까지로 복사 길이를 다시 제한합니다. mapping이 큰 folio를 허용해도 반환된 folio의 크기를 직접 확인합니다.
다음은 현재 block_write_begin() 전체입니다. FGP_WRITEBEGIN은 LOCK·WRITE·CREAT·STABLE을 묶습니다. 여기의 WRITE는 쓰기 의도에 맞는 cache 처리이지 file permission 검사나 writable PTE 설치를 의미하지 않습니다. folio lookup의 error pointer는 그대로 PTR_ERR()로 반환합니다. 이 이른 실패에서는 output pointer를 갱신하는 줄에 도달하지 않으므로 caller는 status를 먼저 검사해야 합니다. block 준비가 실패한 경로는 얻은 folio를 unlock·put하고 NULL을 output으로 넘깁니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
int block_write_begin(struct address_space *mapping, loff_t pos, unsigned len,
struct folio **foliop, get_block_t *get_block)
{
pgoff_t index = pos >> PAGE_SHIFT;
struct folio *folio;
int status;
folio = __filemap_get_folio(mapping, index, FGP_WRITEBEGIN,
mapping_gfp_mask(mapping));
if (IS_ERR(folio))
return PTR_ERR(folio);
status = __block_write_begin_int(folio, pos, len, get_block, NULL);
if (unlikely(status)) {
folio_unlock(folio);
folio_put(folio);
folio = NULL;
}
*foliop = folio;
return status;
}
예전 page 기반 interface는 다음 v6.0 grab_cache_page_write_begin() 전체와 비교할 수 있습니다. 이 API의 NULL 실패와 현재 folio API의 error pointer를 한 코드의 혼합 조건으로 사용하지 않습니다. cache 정리만으로 filesystem이 이미 할당한 block의 실패 복구까지 모두 끝나는 것도 아닙니다.
grab_cache_page_write_begin_v6_0.c
// SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
/*
* Complete scope from Linux v6.0 mm/folio-compat.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/folio-compat.c
* SPDX above follows repository COPYING; source has no file SPDX.
* Policy: https://github.com/torvalds/linux/blob/v6.0/COPYING
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
struct page *grab_cache_page_write_begin(struct address_space *mapping,
pgoff_t index)
{
unsigned fgp_flags = FGP_LOCK | FGP_WRITE | FGP_CREAT | FGP_STABLE;
return pagecache_get_page(mapping, index, fgp_flags,
mapping_gfp_mask(mapping));
}
atomic copy는 사용자 buffer가 반드시 유효하다는 뜻이 아닙니다
folio와 filesystem 관련 lock을 가진 채 사용자 주소의 일반적인 fault를 처리하면 임의의 filesystem 경로로 재진입할 수 있습니다. lock 순환으로 이어질 수 있으므로 공통 write loop는 copy_folio_from_iter_atomic()을 사용합니다.
이 선택은 사용자 buffer의 모든 byte를 반드시 복사한다는 보장이 아닙니다. 잠재적인 fault를 그 자리에서 모두 해결하지 않기 때문에 short copy나 0-byte copy가 가능합니다. architecture에 필요한 dcache flush도 이 주변에서 처리됩니다. x86만 보고 공통 코드의 cache maintenance를 무의미한 작업이라고 지워 읽어서는 안 됩니다.
아래는 구현코드가 아닌 호출관계입니다.
generic_perform_write()
-> balance_dirty_pages_ratelimited()
-> a_ops->write_begin()
-> copy_folio_from_iter_atomic()
-> a_ops->write_end()
-> reconcile iterator progress
-> retry or advance position
이 순서는 v6.18 기준입니다. dirty balancing이 매 iteration의 write_begin 앞에 있다는 점은 오래된 구현의 위치와 구별해야 합니다. 최초 호출 직전 하나의 folio가 방금 dirty였어야만 작동하는 함수라는 뜻도 아닙니다.
아래는 v6.18 generic_perform_write() 전체입니다. fatal signal, write-begin 오류, atomic copy의 short result, write-end 반환과 iterator 복구, chunk 축소, fault-in 재시도와 최종 position 갱신을 한 함수 안에서 볼 수 있습니다. iocb->ki_pos는 이 함수가 양수 진행량을 반환하기 전에 갱신합니다. 오래된 caller의 별도 position 증가를 여기에도 덧붙이면 두 번 전진시킬 수 있습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
ssize_t generic_perform_write(struct kiocb *iocb, struct iov_iter *i)
{
struct file *file = iocb->ki_filp;
loff_t pos = iocb->ki_pos;
struct address_space *mapping = file->f_mapping;
const struct address_space_operations *a_ops = mapping->a_ops;
size_t chunk = mapping_max_folio_size(mapping);
long status = 0;
ssize_t written = 0;
do {
struct folio *folio;
size_t offset; /* Offset into folio */
size_t bytes; /* Bytes to write to folio */
size_t copied; /* Bytes copied from user */
void *fsdata = NULL;
bytes = iov_iter_count(i);
retry:
offset = pos & (chunk - 1);
bytes = min(chunk - offset, bytes);
balance_dirty_pages_ratelimited(mapping);
if (fatal_signal_pending(current)) {
status = -EINTR;
break;
}
status = a_ops->write_begin(iocb, mapping, pos, bytes,
&folio, &fsdata);
if (unlikely(status < 0))
break;
offset = offset_in_folio(folio, pos);
if (bytes > folio_size(folio) - offset)
bytes = folio_size(folio) - offset;
if (mapping_writably_mapped(mapping))
flush_dcache_folio(folio);
/*
* Faults here on mmap()s can recurse into arbitrary
* filesystem code. Lots of locks are held that can
* deadlock. Use an atomic copy to avoid deadlocking
* in page fault handling.
*/
copied = copy_folio_from_iter_atomic(folio, offset, bytes, i);
flush_dcache_folio(folio);
status = a_ops->write_end(iocb, mapping, pos, bytes, copied,
folio, fsdata);
if (unlikely(status != copied)) {
iov_iter_revert(i, copied - max(status, 0L));
if (unlikely(status < 0))
break;
}
cond_resched();
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 (chunk > PAGE_SIZE)
chunk /= 2;
if (copied) {
bytes = copied;
goto retry;
}
/*
* 'folio' is now unlocked and faults on it can be
* handled. Ensure forward progress by trying to
* fault it in now.
*/
if (fault_in_iov_iter_readable(i, bytes) == bytes) {
status = -EFAULT;
break;
}
} else {
pos += status;
written += status;
}
} while (iov_iter_count(i));
if (!written)
return status;
iocb->ki_pos += written;
return written;
}
write_end가 인정한 양만 진행량이 됩니다
write_end에는 요청한 byte 수와 실제 복사한 byte 수가 각각 전달됩니다. 반환값은 filesystem이 받아들인 진행량 또는 error입니다. 공통 loop는 status != copied이면 iterator를 되돌려 복사했지만 확정하지 못한 범위를 다시 남깁니다.
예를 들어 3000 byte를 복사했지만 write-end가 1000 byte만 받아들였다면, 다음 작업이 3000 byte 뒤에서 시작해서는 안 됩니다. 사용자가 요구한 stream 중 2000 byte를 건너뛰게 되기 때문입니다. 반대로 단순히 전체 iterator를 처음으로 되돌리면 이미 인정된 byte를 중복 처리하게 됩니다.
0 반환도 별도 취급합니다. v6.18은 큰 chunk를 줄일 수 있으며, 일부를 복사했지만 전부 거절된 상황에서는 그 복사량에 맞춰 재시도합니다. 아무것도 복사하지 못했다면 folio가 unlock된 뒤 사용자 범위를 미리 fault-in하여 진행 가능성을 확인합니다. 여전히 전혀 읽을 수 없으면 -EFAULT로 끝납니다. lock을 가진 채 같은 atomic copy만 무한 반복하지 않습니다.
버전 차이를 직접 보려면 다음 v6.0 전체 loop를 비교할 수 있습니다. 이 버전은 page 단위 길이와 callback 인자를 사용하고, write-begin 전에 사용자 범위를 fault-in하며 진행 뒤에 dirty balancing을 합니다. current의 large-folio chunk, zero-copy 뒤 fault-in 위치와 동일한 구현이라고 부르지 않습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.0 mm/filemap.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/filemap.c
* Copyright (C) 1994-1999 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
ssize_t generic_perform_write(struct kiocb *iocb, struct iov_iter *i)
{
struct file *file = iocb->ki_filp;
loff_t pos = iocb->ki_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;
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) == bytes)) {
status = -EFAULT;
break;
}
if (fatal_signal_pending(current)) {
status = -EINTR;
break;
}
status = a_ops->write_begin(file, mapping, pos, bytes,
&page, &fsdata);
if (unlikely(status < 0))
break;
if (mapping_writably_mapped(mapping))
flush_dcache_page(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();
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;
balance_dirty_pages_ratelimited(mapping);
} while (iov_iter_count(i));
return written ? written : status;
}
dirty 확정과 저장 완료를 구별합니다
buffer 기반의 block_write_end()와 block_commit_write()는 수정한 buffer를 uptodate·dirty로 만들고, 필요한 경우 folio 전체 상태를 올립니다. generic_write_end()는 파일 크기 갱신 같은 공통 후처리도 담당합니다. 이때 cache와 inode가 dirty 검색에 잡힐 수 있어야 합니다. 이것이 device에 I/O를 제출하고 기다린 결과는 아닙니다. v6.18 fs/buffer.c
block_write_end()는 short copy인데 folio가 아직 uptodate가 아니면 수락량을 0으로 만들 수 있습니다. 부분 block에 복사한 내용을 다음 read가 덮어쓸 수 있기 때문입니다. 아직 초기화되지 않은 새 buffer 범위를 zero하는 후처리도 필요합니다. 이 함수 혼자 folio lock을 놓지는 않습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
int block_write_end(loff_t pos, unsigned len, unsigned copied,
struct folio *folio)
{
size_t start = pos - folio_pos(folio);
if (unlikely(copied < len)) {
/*
* The buffers that were written will now be uptodate, so
* we don't have to worry about a read_folio reading them
* and overwriting a partial write. However if we have
* encountered a short write and only partially written
* into a buffer, it will not be marked uptodate, so a
* read_folio might come in and destroy our partial write.
*
* Do the simplest thing, and just treat any short write to a
* non uptodate folio as a zero-length write, and force the
* caller to redo the whole thing.
*/
if (!folio_test_uptodate(folio))
copied = 0;
folio_zero_new_buffers(folio, start+copied, start+len);
}
flush_dcache_folio(folio);
/* This could be a short (even 0-length) commit */
block_commit_write(folio, start, start + copied);
return copied;
}
상위 generic_write_end()는 i_rwsem으로 size 변경이 직렬화된 상태에서, folio lock을 놓기 전에 i_size를 갱신합니다. writeout이 예전 size를 보고 방금 쓴 부분을 EOF 밖으로 zero하지 못하게 하는 순서입니다. 이후 folio를 unlock·put하고, 필요하면 파일 확장 처리와 inode dirty 표시를 이어갑니다. inode dirty 처리를 folio lock 밖으로 미루는 데에는 journal transaction과의 lock ordering도 관계됩니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
int generic_write_end(const struct kiocb *iocb, struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct folio *folio, void *fsdata)
{
struct inode *inode = mapping->host;
loff_t old_size = inode->i_size;
bool i_size_changed = false;
copied = block_write_end(pos, len, copied, folio);
/*
* No need to use i_size_read() here, the i_size cannot change under us
* because we hold i_rwsem.
*
* But it's important to update i_size while still holding folio lock:
* page writeout could otherwise come in and zero beyond i_size.
*/
if (pos + copied > inode->i_size) {
i_size_write(inode, pos + copied);
i_size_changed = true;
}
folio_unlock(folio);
folio_put(folio);
if (old_size < pos)
pagecache_isize_extended(inode, old_size, pos);
/*
* Don't mark the inode dirty under page lock. First, it unnecessarily
* makes the holding time of page lock longer. Second, it forces lock
* ordering of page lock and transaction start for journaling
* filesystems.
*/
if (i_size_changed)
mark_inode_dirty(inode);
return copied;
}
마지막 두 helper도 v6.18 전체 정의입니다. 첫 helper가 겹치는 buffer를 dirty·uptodate로 만들고, 두 번째 helper가 처음 dirty인 buffer에서 folio와 inode의 dirty 추적으로 연결합니다. 이미 dirty인 경우의 barrier와 재검사는 새 수정과 concurrent writeback 사이의 ordering을 지키려는 것입니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
void block_commit_write(struct folio *folio, size_t from, size_t to)
{
size_t block_start, block_end;
bool partial = false;
unsigned blocksize;
struct buffer_head *bh, *head;
bh = head = folio_buffers(folio);
if (!bh)
return;
blocksize = bh->b_size;
block_start = 0;
do {
block_end = block_start + blocksize;
if (block_end <= from || block_start >= to) {
if (!buffer_uptodate(bh))
partial = true;
} else {
set_buffer_uptodate(bh);
mark_buffer_dirty(bh);
}
if (buffer_new(bh))
clear_buffer_new(bh);
block_start = block_end;
bh = bh->b_this_page;
} while (bh != head);
/*
* If this is a partial write which happened to make all buffers
* uptodate then we can optimize away a bogus read_folio() for
* the next read(). Here we 'discover' whether the folio went
* uptodate as a result of this (potentially partial) write.
*/
if (!partial)
folio_mark_uptodate(folio);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
void mark_buffer_dirty(struct buffer_head *bh)
{
WARN_ON_ONCE(!buffer_uptodate(bh));
trace_block_dirty_buffer(bh);
/*
* Very *carefully* optimize the it-is-already-dirty case.
*
* Don't let the final "is it dirty" escape to before we
* perhaps modified the buffer.
*/
if (buffer_dirty(bh)) {
smp_mb();
if (buffer_dirty(bh))
return;
}
if (!test_set_buffer_dirty(bh)) {
struct folio *folio = bh->b_folio;
struct address_space *mapping = NULL;
if (!folio_test_set_dirty(folio)) {
mapping = folio->mapping;
if (mapping)
__folio_mark_dirty(folio, mapping, 0);
}
if (mapping)
__mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
}
}
loop는 성공한 양만큼 position과 누적 written을 늘립니다. 뒤에서 error가 발생해도 이미 쓴 byte가 있으면 그 양을 돌려줄 수 있습니다. 따라서 호출자는 양수 반환이 요청 전체의 완료인지 확인해야 합니다. 동기 write 옵션은 후속 sync에서 error를 발견할 수도 있으므로 마지막 반환값까지 확인해야 합니다.
복사한 양, filesystem이 수락한 양, 저장이 끝난 양을 나누어 읽으면 partial write의 의미가 훨씬 선명해집니다. 같은 양수 반환도 어느 계층에서 무엇을 완료한 결과인지 확인해야 합니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, File Write Dirty Tracking.
- Linux v6.18 mm/filemap.c:
generic_perform_write()와 generic write wrapper 전체 구현. - Linux v6.18 fs/buffer.c: buffer 기반 write 준비와 write-end.
- Linux v6.18 include/linux/fs.h:
generic_write_sync()및 callback interface.