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博客