filesystem学习-02-read调用链分析

3510阅读 0评论2018-06-07 fireaxe
分类:嵌入式

本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。

作者:fireaxe.hq@outlook.com

博客:fireaxe.blog.chinaunix.net 



1. 基本原理

block用户态调用read函数时,会陷入内核的system call 函数sys_read。然后调用vfs层的读入口vfs_read。我们的read解析之旅也由此开始。

block解析的终点是调用块设备的接口为止,也就是request解析函数。(这里假设块设备提供的函数为wiccablk_request。该函数的入参是reqeust queue。而read的入参是文件描述符。本文除了理出中间的函数调用关系外,更重要的是,整理出参数传递过程中的转换,从而理解block layer的工作原理。

block layer是用linux下文件处理的核心。其对上是vfs接口层,对下是块设备驱动的request解析函数。中间对文件系统做了抽象,方便不同文件系统挂入其中。


块设备驱动:

提供块设备的读写接口,简单说就是读取一个或多个连续sector。以xxxblk_reqeust()的形式对上提供服务。上层模块会把读写请求包装成bio,放到驱动对应的request queue中,然后启动驱动去处理对应的request。驱动开发者就是要根据设备的特性,实现这种处理。


文件系统:

根据硬盘上的文件系统结构,构造superblock,构造inode节点,连接block层文件与目录处理函数(类似于透传,当然经常会加入一些特殊的处理)。

superblock会在mount时进行构造,只构造一次。文件与目录的inode节点则需要每次构造(当然如果上次构造的还在cache中,则不用再次构造了)。



2. read函数调用关系


2.1 第一条链:从read到磁盘读取


(1) ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)

(2) ssize_t __vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)

(3) ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)


iocb->ki_filp = *pposfilp;

iocb->ki_flags = iocb_flags(filp);

iocb->ki_pos = *ppos;

iov_iter_init(&iter, READ, &{ .iov_base = buf, .iov_len = len }, 1, len);


(4) filp->f_op->read_iter = ext4_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)

(5) generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)


filp = iocb->ki_filp;

ppos = &iocb->ki_pos;

written = read;

(6) ssize_t do_generic_file_read(struct file *filp, loff_t *ppos, struct iov_iter *iter, ssize_t written)


mapping = file->f_mapping; (就是file system中的inode->i_mapping

ra = & filp->f_ra;

offset = *ppos & ~PAGE_MASK;

req_size = (iter->count + PAGE_SIZE - 1) >> PAGE_SHIFT;

= (len + PAGE_SIZE - 1) >> PAGE_SHIFT


(7) void page_cache_sync_readahead(struct address_space *mapping,

struct file_ra_state *ra, struct file *filp, pgoff_t offset, unsigned long req_size)

hit_readahead_marker = false;


(8) unsigned long ondemand_readahead(struct address_space *mapping,

struct file_ra_state *ra, struct file *filp, bool hit_readahead_marker,

pgoff_t offset, unsigned long req_size)


lookahead_size = 0;

nr_to_read = req_size;


(9) int __do_page_cache_readahead(struct address_space *mapping, struct file *filp,

pgoff_t offset, unsigned long nr_to_read, unsigned long lookahead_size)

gfp = mapping->gfp_mask | __GFP_COLD | __GFP_NORETRY | __GFP_NOWARN


(10) int read_pages(struct address_space *mapping, struct file *filp,

struct list_head *pages, unsigned int nr_pages, gfp_t gfp)


blk_start_plug会初始化一个blk_plug节点,并挂入进程的task->plug。也即是说,block io请求与进程相关,只有同一个进程的请求才会merge到一起,这就避免了全局锁的使用。

blk_finish_plug会最终处理前面挂进来的plug。可以看到,plug只是用于通知进程有一个新的读写请求,但并不会立即执行,因为进程要尽量合并读写请求,以减少对磁盘的操作。

plug只是个请求,数据不会通过它传递。数据的传递通过inode->i_mapping->a_ops中的readpage实现。readpage会把读请求转换的为bio,并挂入blk_start_plug中的队列中去。等待blk_finish_plug时处理这些bio。(关于readpage的调用关系,在后面分析)


(11) void blk_start_plug(struct blk_plug *plug)

mapping->a_ops->readpage = ext4_readpage(struct file *filp, struct page *page);

void blk_finish_plug(struct blk_plug *plug)

from_schedule = false;

(12) void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)

q来自于plug中存储的list

depth用于限制递归层数;

后续的处理就比较简单了,q最终传到块设备驱动自定义的wiccablk_reqeust中,进行磁盘的数据的实际操作。

(13) void queue_unplugged(struct request_queue *q, unsigned int depth,

bool from_schedule)

(14) void __blk_run_queue(struct request_queue *q)

(15) void __blk_run_queue_uncond(struct request_queue *q)

(16) q->request_fn = void wiccablk_request(struct request_queue *q)


2.2 第二条链:mapping->a_ops->readpage生成bio


下面回过头来分析readpage,这个函数很重要,因为他关系到disk cache的实现与io调度器的使用。

cache的实现这里不做分析。bio的生成则完全由该函数实现,并会吧bio加入到对应的reqeust queue中去。


(1) int ext4_readpage(struct file *file, struct page *page)

mapping = page->mapping;

pages = NULL;

nr_pages = 1;

另一个入口是ext4_readpages,其原型如下:

ext4_readpages(struct file *file, struct address_space *mapping, struct list_head *pages, unsigned nr_pages)

往下传入时,参数如下:

mapping = page->mapping;

page = NULL;

(2) int ext4_mpage_readpages(struct address_space *mapping,

struct list_head *pages, struct page *page, unsigned nr_pages)


这一步将来成bio。此后的操作都将以bio作为基本调度单元。

下一步是把bio加入到reqeust queue

(3) blk_qc_t submit_bio(struct bio *bio)

(4) blk_qc_t generic_make_request(struct bio *bio)

(5) q->make_request_fn = blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)

此处要解释下make_request_fn的赋值,通常的块设备都会通过blk_init_queue()来注册q->request_fn,而blk_init_queue()中会调用如下语句给make_request_fn赋值:

blk_queue_make_request(q, blk_queue_bio);

也就是说,通常的块设备都默认使用blk_queue_bio作为make_request_fn函数。当然也有例外,很多模块都实现了自己的make_request_fn函数。

blk_queue_bio用于把bio加入到request中,然后把request家进入到pluglist中,并等待blk_flush_plug_list来处理。


3.3 IO scheduler


在blk_queue_bio中的处理,使用电梯算法进行IO调度,讲几种电梯算法的文章很多,这里不展开讨论。需要说的是,blk_queue_bio中调用了elv_merge,从而进入了IO

调度算法的范围。


(1) int elv_merge(struct request_queue *q, struct request **req, struct bio *bio)

(2) e->type->ops.elevator_merge_fn = int cfq_merge(struct request_queue *q, struct request **req, struct bio *bio)



注:用dump_stack打印出的调用链

[ 176.584162] [] wiccablk_request+0x4c/0x410 [test_wiccablk]

[ 176.584563] [] __blk_run_queue+0x40/0x58

[ 176.584923] [] queue_unplugged+0x38/0x140

[ 176.585293] [] blk_flush_plug_list+0x1d4/0x248

[ 176.585642] [] blk_finish_plug+0x40/0x50

[ 176.585986] [] __do_page_cache_readahead+0x1b8/0x290

[ 176.586379] [] ondemand_readahead+0x108/0x2b0

[ 176.586668] [] page_cache_sync_readahead+0x60/0xa0

[ 176.586970] [] generic_file_read_iter+0x5c4/0x740

[ 176.587271] [] ext4_file_read_iter+0x44/0x50

[ 176.587567] [] __vfs_read+0xd0/0x120

[ 176.587832] [] vfs_read+0x8c/0x148

[ 176.588327] [] SyS_read+0x54/0xb0

[ 219.581052] [] vfs_write+0xa8/0x1b8

[ 219.581321] [] SyS_write+0x54/0xb0


本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。

作者:fireaxe.hq@outlook.com

博客:fireaxe.blog.chinaunix.net 




上一篇:filesystem学习-01-最简block device driver实现
下一篇:sysfs基本原理与使用方法