接上篇Linux内核读文件流程,写这篇Linux内核写文件流程。文中涉及的内核代码版本是linux内核版本号:3.0.13-0.27 sles11sp2版本。

用户态write函数到内核态的调用是:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
 size_t, count)

SYSCALL_DEFINE3调用的vfs_write,vfs_write调用rw_verify_area检查当前是否有写权限,然后调用虚拟文件系统(vfs)的f_op指针write函数。f_op指针write函数在ext3_file_operations中定义的,定义为do_sync_write。

ret = rw_verify_area(WRITE, file, pos, count);
if (ret >= 0) {
     count = ret;
     if (file->f_op->write)
           ret = file->f_op->write(file, buf, count, pos);
     else
           ret = do_sync_write(file, buf, count, pos);
}

在write中函数定义了iov和kiocb,调用aio_write完成数据写入,aio_write是在ext3_file_operations中定义为generic_file_aio_write;然后等待wait_on_retry_sync_kiocb将数据刷新到磁盘

for (;;) {
         ret = filp->f_op->aio_write(&kiocb, &iov, 1, kiocb.ki_pos);
         if (ret != -EIOCBRETRY)
         break;
         wait_on_retry_sync_kiocb(&kiocb);
 }

generic_file_aio_write同样在开始检查各种锁等控制开关,直接通过__generic_file_aio_write将功能实现,但大部分检查是在__generic_file_aio_write里面测试的,如vfs_check_frozen检查文件系统是否有冻僵(不能写入),generic_write_checks权限块设备等检查。文件的修改时间也是在此处刷新的:file_update_time。然后检查是否有直接写入(O_DIRECT)的参数,O_DIRECT部分后面再说,如果没有则是generic_file_buffered_write,最后进入到generic_perform_write.

generic_perform_write也是文件写入的主要完成部分,整个部分是由一个do{}while循环完成的。首先是通过write_begin函数进行一些预处理工作,具体指向ext3_write_begin函数。ext3_write_begin首先用grab_cache_page_write_begin获取页高速缓存,调用__block_write_begin,在其中用get_block获取物理块号,同时启用journal机制

当write_begin已经完成的时候,将用户态数据拷贝到内核态的page上。

 pagefault_disable();
 copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
 pagefault_enable();

通过write_end完成具体写入,由于ext3的journal有3中不同的写入方式,此处介绍默认的order方式,ext3_ordered_write_end函数首先调用block_write_end函数,其中调用__block_commit_write提交写入的数据,但事实上其提交只是对buff的状态做了处理,并没有其他大的操作。

set_buffer_uptodate(bh);
 mark_buffer_dirty(bh);

而后的walk_page_buffers只是通过journal_dirty_data_fn利用journal机制将数据标记为脏,然后用update_file_sizes更新文件所属inode的属性,并置为脏。

static void update_file_sizes(struct inode *inode, loff_t pos, unsigned copied)
{
/* What matters to us is i_disksize. We don't write i_size anywhere */if (pos + copied > inode->i_size)
i_size_write(inode, pos + copied);
if (pos + copied > EXT3_I(inode)->i_disksize) {
EXT3_I(inode)->i_disksize = pos + copied;
mark_inode_dirty(inode);
}
}

此时将inode添加到孤儿链表中,具体介绍见链接。

if (pos + len > inode->i_size && ext3_can_truncate(inode))
 ext3_orphan_add(handle, inode);

事实上前面都没有做刷新磁盘的操作,最后还是回到generic_perform_write函数,有balance_dirty_pages_ratelimited函数,顾名思义,平衡脏页比率,在此处进行脏页的刷新。另外一篇文章有详细介绍,点击Linux缓存写回机制

如此才完成了文件的写入过程。


Linux内核写文件流程来自于OenHan

链接为:https://oenhan.com/linux-kernel-write

5 thoughts on “Linux内核写文件流程”

  1. 您好,最近我遇到一个问题:
    write 系统调用偶发地很慢,延迟可达 100-600ms。禁用了 journal 或 将 data mode 改为 writeback 后就没有延迟问题了,但是不知道是什么原理。。

    抓了一次线程栈发现如下:

    [] call_rwsem_down_read_failed+0x14/0x30
    [] ext4_da_get_block_prep+0x1a4/0x4b0 [ext4]
    [] __block_write_begin+0x1a7/0x490
    [] ext4_da_write_begin+0x15c/0x340 [ext4]
    [] generic_file_buffered_write+0x11e/0x290
    [] __generic_file_aio_write+0x1d5/0x3e0
    [] generic_file_aio_write+0x5d/0xc0
    [] ext4_file_write+0xb5/0x460 [ext4]
    [] do_sync_write+0x8d/0xd0
    [] vfs_write+0xbd/0x1e0
    [] SyS_write+0x58/0xb0
    [] tracesys+0xdd/0xe2
    [] 0xffffffffffffffff

    您能帮忙分析一下吗?

  2. 问题是偶发的,连续写几十秒才触发一次,耗时统计被平均了看不出来明显的异常。我读了您的那篇文章,还是不知道怎么把信号量等待队列打出来,内核小白。。
    我翻了网上很多资料,现在怀疑是在 jbd2 刷 journal 时,由于在 data=ordered 模式下,需要先将脏页刷回到磁盘,阻塞了 metadata journal 写磁盘。由于我是在不停地向同一个日志文件里写,可能会阻塞 inode 的写什么的。。
    另外我把系统 writeback 的频率调高了(脏页1s过期,1s刷一次)之后,可能由于刷 journal 时没有太多的脏页要刷,所以问题也大大地缓解了。
    缺少理论支持或直接证据。。

    1. @枕边书 upsteam的代码有同样问题么,我建议你把问题以及在upstream上的复现方法发到kernel bugzilla,大家可以一起看一下

发表回复