Linux Kernel文件系統寫I/O流程代碼分析(一)
Linux Kernel文件系統寫I/O流程代碼分析(一)
在Linux VFS機制簡析(二)這篇博客上介紹了struct address_space_operations里底層文件系統需要實現的操作,實際編碼過程中發現不是那么清楚的知道這里面的函數具體是干啥,在什么時候調用。尤其是寫IO相關的操作,包括write_begin, write_end, writepage, writepages, direct_IO以及set_page_dirty等函數指針。
要搞清楚這些函數指針,就需要縱觀整個寫流程里這些函數指針的調用位置。因此本文重點分析和梳理了Linux文件系統寫I/O的代碼流程,以幫助實現底層文件系統的讀寫接口。
概覽
先放一張圖鎮貼,該流程圖沒有包括bdi_writeback回寫機制(將在下一篇中展示):

VFS流程
sys_write()
Glibc提供的write()函數調用由內核的write系統調用實現,對應的系統調用函數為sys_write()定義如下:
asmlinkage long sys_write(unsigned int fd, const char __user *buf,
size_t count);
sys_write()的實現在fs/read_write.c里:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
該函數獲取struct fd引用計數和pos鎖定,獲取pos并主要通過調用vfs_write()實現數據寫入。
vfs_write()
vfs_write()函數定義如下:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count);
if (ret >= 0) {
count = ret;
file_start_write(file);
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos);
else
ret = do_sync_write(file, buf, count, pos);
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
該函數首先調用rw_verify_area()檢查pos和count對應的區域是否可以寫入(如是否獲取寫鎖等)。然后如果底層文件系統指定了struct file_operations里的write()函數指針,則調用file->f_op->write()函數,否則直接調用VFS的通用寫入函數do_sync_write()。
do_sync_write()
VFS的do_sync_write()函數在底層文件系統沒有指定f_op->write()函數指針時默認調用,它也被很多底層系統直接指定為f_op->write()。其定義如下所示:
ssize_t do_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
struct kiocb kiocb;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
kiocb.ki_left = len;
kiocb.ki_nbytes = len;
ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&kiocb);
*ppos = kiocb.ki_pos;
return ret;
}
通過時上面的代碼可知,該函數主要生成struct kiocb,將其提交給f_op->aio_write()函數,并等待該kiocb的完成。所以底層文件系統必須實現f_op->aio_write()函數指針。
底層文件系統大部分實現了自己的f_op->aio_write(),也有部分文件系統(如ext4, nfs等)直接指向了通用的寫入方法:generic_file_aio_write()。我們通過該函數代碼來分析寫入的大致流程。
generic_file_aio_write()
VFS(其實是mm模塊)提供了通用的aio_write()函數,其定義如下:
ssize_t generic_file_aio_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t pos)
{
struct file *file = iocb->ki_filp;
struct inode *inode = file->f_mapping->host;
ssize_t ret;
BUG_ON(iocb->ki_pos != pos);
mutex_lock(&inode->i_mutex);
ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);
mutex_unlock(&inode->i_mutex);
if (ret > 0) {
ssize_t err;
err = generic_write_sync(file, pos, ret);
if (err < 0 && ret > 0)
ret = err;
}
return ret;
}
該函數對inode加鎖之后,調用__generic_file_aio_write()函數將數據寫入。如果ret > 0即數據寫入成功,并且寫操作需要同步到磁盤(如設置了O_SYNC),則調用generic_write_sync(),這里面將調用f_op->fsync()函數指針將數據寫盤。
函數__generic_file_aio_write()的代碼略多,這里貼出主要的片段如下:
ssize_t __generic_file_aio_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t *ppos)
{
...
if (io_is_direct(file)) {
loff_t endbyte;
ssize_t written_buffered;
written = generic_file_direct_write(iocb, iov, &nr_segs, pos,
ppos, count, ocount);
...
} else {
written = generic_file_buffered_write(iocb, iov, nr_segs,
pos, ppos, count, written);
}
...
從上面代碼可以看到,如果是Direct IO,則調用generic_file_direct_write(),不經過page cache直接寫入磁盤;如果不是Direct IO,則調用generic_file_buffered_write()寫入page cache。
Direct IO實現
generic_file_direct_write()
函數generic_file_direct_write()的主要代碼如下所示:
ssize_t
generic_file_direct_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long *nr_segs, loff_t pos, loff_t *ppos,
size_t count, size_t ocount)
{
...
if (count != ocount)
*nr_segs = iov_shorten((struct iovec *)iov, *nr_segs, count);
write_len = iov_length(iov, *nr_segs);
end = (pos + write_len - 1) >> PAGE_CACHE_SHIFT;
written = filemap_write_and_wait_range(mapping, pos, pos + write_len - 1);
if (written)
goto out;
if (mapping->nrpages) {
written = invalidate_inode_pages2_range(mapping,
pos >> PAGE_CACHE_SHIFT, end);
if (written) {
if (written == -EBUSY)
return 0;
goto out;
}
}
written = mapping->a_ops->direct_IO(WRITE, iocb, iov, pos, *nr_segs);
if (mapping->nrpages) {
invalidate_inode_pages2_range(mapping,
pos >> PAGE_CACHE_SHIFT, end);
}
if (written > 0) {
pos += written;
if (pos > i_size_read(inode) && !S_ISBLK(inode->i_mode)) {
i_size_write(inode, pos);
mark_inode_dirty(inode);
}
*ppos = pos;
}
out:
return written;
}
由于是Direct IO,在寫入之前需要調用filemap_write_and_wait_range()將page cache里的對應臟數據刷盤,以保障正確的寫入順序。filemap_write_and_wait_range()函數最終通過調用do_writepages()函數將臟頁刷盤(參見后面)。
然后調用invalidate_inode_pages2_range()函數將要寫入的區域在page cache里失效,以保證讀操作必須經過磁盤讀到最新寫入的數據。在本次寫操作完成后再次調用invalidate_inode_pages2_range()函數將page cache失效,避免寫入磁盤的過程中有新的讀取操作將過期數據讀到了cache里。
最終通過調用a_ops->dierct_IO()將數據Direct IO方式寫入磁盤。a_ops即struct address_operations,由底層文件系統實現。
Buffered IO實現
generic_file_buffered_write()
函數generic_file_buffered_write()的主要代碼如下所示:
ssize_t
generic_file_buffered_write(struct kiocb *iocb, const struct iovec *iov,
unsigned long nr_segs, loff_t pos, loff_t *ppos,
size_t count, ssize_t written)
{
struct file *file = iocb->ki_filp;
ssize_t status;
struct iov_iter i;
iov_iter_init(&i, iov, nr_segs, count, written);
status = generic_perform_write(file, &i, pos);
if (likely(status >= 0)) {
written += status;
*ppos = pos + status;
}
return written ? written : status;
}
該函數初始化一個struct iov_iter,然后主要通過調用generic_perform_write()函數寫入page cache。
generic_perform_write()
函數generic_perform_write()主要代碼如下所示:
static ssize_t generic_perform_write(struct file *file,
struct iov_iter *i, loff_t pos)
{
...
if (segment_eq(get_fs(), KERNEL_DS))
flags |= AOP_FLAG_UNINTERRUPTIBLE;
do {
...
offset = (pos & (PAGE_CACHE_SIZE - 1));
bytes = min_t(unsigned long, PAGE_CACHE_SIZE - offset,
iov_iter_count(i));
again:
if (unlikely(iov_iter_fault_in_readable(i, bytes))) {
status = -EFAULT;
break;
}
status = a_ops->write_begin(file, mapping, pos, bytes, flags,
&page, &fsdata);
if (unlikely(status))
break;
if (mapping_writably_mapped(mapping))
flush_dcache_page(page);
pagefault_disable();
copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
pagefault_enable();
flush_dcache_page(page);
mark_page_accessed(page);
status = a_ops->write_end(file, mapping, pos, bytes, copied,
page, fsdata);
if (unlikely(status < 0))
break;
copied = status;
cond_resched();
iov_iter_advance(i, copied);
if (unlikely(copied == 0)) {
bytes = min_t(unsigned long, PAGE_CACHE_SIZE - offset,
iov_iter_single_seg_count(i));
goto again;
}
pos += copied;
written += copied;
balance_dirty_pages_ratelimited(mapping);
if (fatal_signal_pending(current)) {
status = -EINTR;
break;
}
} while (iov_iter_count(i));
return written ? written : status;
}
該函數包括如下幾個步驟:
1.通過調用a_ops->write_begin()進行數據寫入前的處理,由底層文件系統實現,主要處理需要申請額外的存儲空間,以及從后端存儲(磁盤或者網絡)讀取不在緩存里的page數據。該函數返回locked的page。
2.從用戶空間拷貝數據到步驟1返回的page里。訪問用戶態內存時可能觸發缺頁異常,為避免陷入缺頁異常處理從而導致重入和死鎖(如mmap文件系統的內存),拷貝之前,通過pagefault_disable()將缺頁異常處理關閉,當發生缺頁異常時不進行異常處理。
3.通過調用底層文件系統的a_ops->write_end()將page這是為dirty并unlock。
4.循環步驟1-3,直到所有iov都得到處理,每次循環只處理一個page里的數據。
5.調用balance_dirty_pages_ratelimited()平衡內存中的臟頁,需要時將臟頁刷盤。
后記
從上可知,對于Buffered IO,并不一定有將數據寫入磁盤的操作,這就是延遲寫技術。數據寫入內核的page cache緩存后,后續由bdi_writeback機制負責臟頁的數據刷盤回寫。

浙公網安備 33010602011771號