Linux内核写文件流程
接上篇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
您好,最近我遇到一个问题:
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
您能帮忙分析一下吗?
@枕边书 可能是jbd层的代码有问题,你抓的内核栈也不能说明时间耗在
建议你用perf抓一下延迟时间耗在了哪里,等待什么或者走了不同的路径。
如果是i_data_sem的信号量,可以把信号量等待队列打出来,参考读写信号量与实时进程阻塞挂死问题
问题是偶发的,连续写几十秒才触发一次,耗时统计被平均了看不出来明显的异常。我读了您的那篇文章,还是不知道怎么把信号量等待队列打出来,内核小白。。
我翻了网上很多资料,现在怀疑是在 jbd2 刷 journal 时,由于在 data=ordered 模式下,需要先将脏页刷回到磁盘,阻塞了 metadata journal 写磁盘。由于我是在不停地向同一个日志文件里写,可能会阻塞 inode 的写什么的。。
另外我把系统 writeback 的频率调高了(脏页1s过期,1s刷一次)之后,可能由于刷 journal 时没有太多的脏页要刷,所以问题也大大地缓解了。
缺少理论支持或直接证据。。
@枕边书 upsteam的代码有同样问题么,我建议你把问题以及在upstream上的复现方法发到kernel bugzilla,大家可以一起看一下
@OENHAN 最近又找资料发现禁用 delay allocate 的特性也可以解决这个问题。找不到方法来测试 upstream 的代码。。复现就用 `dd` 或一个不停往一个文件写入的脚本就行,直接贴上去麻烦大佬们了。。
https://bugzilla.kernel.org/show_bug.cgi?id=201461