文件系统预读机制的代码分析

1,背景

文件系统的目的很简单,就是将磁盘里的数据读取到用户进程指定的内存中。在linux内核实际的实现中,除了将指定位置的磁盘内容读取到page cache中之外,根据临近原则,还会比指定位置的内容多读取一些内容到内存中,提高代码的运行效率。

2,实验描述

首先我用如下命令创建了一个8k大小的文件:

dd if=/dev/zero of=file bs=8k count=1

同时,我运行一个应用程序,先读文件file的前4k数据,然后再读后4k的数据。代码如下:

#include <unistd.h>
#include <fcntl.h>

main()
{
	int fd;
	char buf[4096];
	
	sleep(30); //run ./funtion.sh to trace this process
	fd = open("file", O_RDONLY);
	read(fd, buf, 4096);
	read(fd, buf, 4096);

	close(fd);
}

最后,我使用ftrace的function_graph来获取这两次read的代码运行路径,脚本如下:

#!/bin/bash

debugfs=/sys/kernel/debug
echo nop > $debugfs/tracing/current_tracer
echo 0 > $debugfs/tracing/tracing_on
echo `pidof read` > $debugfs/tracing/set_ftrace_pid
echo function_graph > $debugfs/tracing/current_tracer
echo vfs_read > $debugfs/tracing/set_graph_function
echo 1 > $debugfs/tracing/tracing_on

我期望的效果是,通过function_graph的trace log,在第一次vfs_read运行的时候,可以看到从磁盘读取数据的过程,读取数据的大小除了应用指定的4k之外,剩余的4k也会预读到page cache中,这里面会涉及到文件系统,bio,io调度器和磁盘驱动这几个软件模块。在第二次vfs_read的过程中,由于第一次的预读,第二次直接page cache命中,可以看出预读机制对效率的提高。

3,实验步骤:
 

./read (期间留了30秒时间运行脚本,准备ftrace环境)

sudo ./function.sh

cat /sys/kernel/debug/tracing/trace > 8k.txt

4,实验结果:

使用vi -S折叠打开8k.txt,可以看到里面有两个vfs_read的调用过程,其中第一次读文件的过程如下:

可以简单看出里面包含了一次io调度和进程切换。

第二次vfs_read的过程如下:

比第一次的调用简单了很多,从时间来看,效率也提高了很多。

5,代码分析:

1)第一次vfs_read,没有page cache

(1)调用路径是new_sync_read -- call_read_iter --file->f_op->read_iter(kio, iter);  ext4_file_operations inode->i_fop = &ext4_file_operations; 用户空间的buf地址在struct iov_iter的.ubuf中,其他的参数在struct kiocb kiocb中。

(2)direct io的路径,不经过pagecache,直接从硬盘读写,本文不分析这个代码路径。

(3)获取page cache,count就是iter->count 。

(4)代码注释写的是“Start unchecked readahead”,什么是unchecked,我目前还不了解。

(5)我现在还不了解folio,但filemap_alloc_folio后面调用了__alloc_pages -- get_page_from_freelist,可以看出是从buddy分配内存

(6)这个函数里有一个trace event -- trace_mm_filemap_add_to_page_cache,使用trace event分析这个过程的时候用得到,function_graph只能看到过程,trace event可以看到参数。

(7)注释写的是“Now start the IO”,是要开始一个IO的过程了,请阅读本文的参考文档“宋宝华老师的:Linux文件读写(BIO)波澜壮阔的一生”。

(8)文件系统通知block层,要开始一个批量的读写硬盘的操作了,初始化tsk->plug = plug,plug包含一个request列表,后续会将bio_vec组织成bio,然后转换成request,最后放到plug的request列表中。这个plug是一个栈内的变量,作用域就在read_pages函数内。

(9)ext4_readahead是address_space_operations ext4_aops中readahead的回调函数,该函数分配和构建bio,为bio和page cache建立关系,最后调用submit_bio。

(10)调用blk_mq_attempt_bio_merge合并bio,同时调用blk_mq_bio_to_request将bio转换为request,调用blk_add_rq_to_plug将request添加到tsk->plug中

(11)函数注释写着标志一下批量IO提交的结束,但进入代码后,发现远不只于此,从io调度队列,到scsi磁盘硬件的操作,都从这个函数开始。

(12)将plug发送给io调度器,本例的io调度器是deadline,所以回调函数是dd_insert_requests,deadline调度器根据自己的策略和组织方式来合并request,这部分代码我还没读,先不涉及了。

(13)调用ata_scsi_rw_xlat进行硬件的操作,将磁盘数据读取到page cache中。这个读取数据的路径还是在当前任务的上下文中,相当于同步IO过程。

(14)或者,还有一个分支,还可以进入函数blk_mq_delay_run_hw_queues,启动kblockd_workqueue,回调函数是blk_mq_run_work_fn,这个读取是在kworker的上下文中,是一个异步的IO过程,代码路径在本文后面介绍。

(15)调用init_wait(wait) -- wait->func = wake_page_function(唤醒等待的线程);  __add_wait_queue_entry_tail(q, wait);将本任务的task_struct放到等待队列中。

(16)调用schedule函数,放弃CPU,任务进入休眠状态。

(17)scsi中断返回后,唤醒在队列中休眠等待的任务。

(18)最后将磁盘数据从page cache拷贝到用户的buf中

2)磁盘读取后返回的路径:

(1)调用wake_page_function,进而调用wake_up_state--try_to_wake_up唤醒在wait_queue_entry_t等待的任务。

(2)read线程在该函数醒来,此时数据已经就绪,可以拷贝到用户空间内存了。

3)workqueue路径

(1)blk_mq_run_work_fn运行在kworker的kblockd_workqueue任务的上下文中,输入命令“ps -aux |grep kblockd”,可以看到如下信息:

root         122  0.0  0.0      0     0 ?        I<   4月22   0:03 [kworker/3:1H-kblockd]

(2)最后也是调用ata驱动,进行硬件读写,操作完成后,会有中断通知cpu。

4)第二次vfs_read,page cache命中而且不用继续从硬盘readahead

(1)判断fbatch->是否为0,大于0就是命中,且不用继续readahead的情况

(2)最后将磁盘数据从page cache拷贝到用户的buf中

本文只是分析了代码调用的过程,可以帮助读者简要的了解预读机制,但距离能够解决这个过程的bug或者是性能问题,甚至代码优化,性能提升,还差的很远,还需要进一步运行到代码中,看看每个函数中具体的参数,page cache的地址,address_space是如何将page cache的地址和文件系统的块对应起来的,等等知识点。

6,调试技巧

1)function graph

#!/bin/bash

debugfs=/sys/kernel/debug
echo nop > $debugfs/tracing/current_tracer
echo 0 > $debugfs/tracing/tracing_on
echo `pidof read` > $debugfs/tracing/set_ftrace_pid
echo function_graph > $debugfs/tracing/current_tracer
echo vfs_read > $debugfs/tracing/set_graph_function
echo 1 > $debugfs/tracing/tracing_on

2) trace-cmd

trace-cmd start -e sched -e filemap -e ext4 -e block -e workqueue; ./io_scheduler_trace ;trace-cmd stop
trace-cmd extract
trace-cmd report -l > io_scheduler_report.txt

3) kprobe

echo 'p wake_page_function' > ./kprobe_events
echo 'stacktrace:5' > ./events/kprobes/p_wake_page_function_0/trigger

7,参考文献:

宋宝华老师的:Linux文件读写(BIO)波澜壮阔的一生

谢欢老师的:cat一个40k的文件,观察IO读取过程

还有另外两篇在CSDN上看到的很好的文章:

文件系统read之ext4_readpages/do_mpage_readpage函数 源码详解_ext4_mpage_readpages-CSDN博客

page cache之文件预读readahead内核源码详解_pagecache active file-CSDN博客

  • 27
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值