函数generic_file_read()通过执行宏init_sync_kiocb来初始化描述符kiocb,并设置一个同步操作对象的有关字段。具体地说就是,改宏设置ki_key字段为KIOCB_SYNC_KEY,ki_filp字段为filp、ki_obj字段为current。
然后将刚填好的iovec和kiocb描述符地址传给__ generic_file_aio_read()。函数__ generic_file_aio_read()是所有文件系统实现同步和异步操作所使用的通用例程。
如果是直接 io (filp->f_flags 被设置了 O_DIRECT 标志,即不经过 cache)的方式,则调用 generic_file_direct_IO 函数;如果是 page cache 的方式,则调用 do_generic_file_read 函数。函数 do_generic_file_read 仅仅是一个包装函数,它又调用 do_generic_mapping_read 函数,此函数在文件include/linux/fs.h中。
do_generic_mapping_read函数在mm/filemap.c文件中,do_generic_mapping_read函数主要流程如下:
- 根据文件当前的读写位置,在 page cache 中找到缓存请求数据的 page
- 如果该页已经最新,将请求的数据拷贝到用户空间
- 否则, Lock 该页
- 调用 readpage 函数向磁盘发出添页请求(当下层完成该 IO 操作时会解锁该页),代码:
- 再一次 lock 该页,操作成功时,说明数据已经在 page cache 中了,因为只有 IO 操作完成后才可能解锁该页。此处是一个同步点,用于同步数据从磁盘到内存的过程。
- 解锁该页
- 到此为止数据已经在 page cache 中了,再将其拷贝到用户空间中(之后 read 调用可以在用户空间返回了)
readpage 函数在include/linux/fs.h,变量 mapping 为 inode 对象中的 address_space 对象,address_space 对象是嵌入在 inode 对象之中的,那么不难想象: address_space 对象成员 a_ops 的初始化工作将会在初始化 inode 对象时进行,如下图:
变量 ext2_aops 或者变量 ext2_nobh_aops的定义如下:
readpage 成员都指向函数 ext2_readpage,so: 函数 do_generic_mapping_read 最终调用 ext2_readpage 函
数处理读数据请求。ext2_readpage函数在fs/ext2/inode.c文件中
mpage_readpage()函数在fs/mpage.c
该函数首先调用函数 do_mpage_readpage 函数创建了一个 bio 请求,该请求指明了要读取的数据块所在磁盘的位置、数据块的数量以及拷贝该数据的目标位置——缓存区中 page 的信息。然后调用 mpage_bio_submit 函数处理请求。 mpage_bio_submit 函数则调用 submit_bio 函数处理该请求,后者最终将请求传递给函数 generic_make_request ,并由 generic_make_request 函数将请求提交给通用块层处理。
__generic_make_request根据bio中保存的设备号取得请求队列q,检查当前IO调度器是否可用,如果可以,继续;否则等待调度器可用,然后调用q->make_request_fn所指函数即将该请求加入到请求队列。
对 make_request_fn 函数的调用可以认为是 IO 调度层的入口,该函数用于向请求队列中添加请求。该函数是在创建请求队列时指定的,代码如下(blk_init_queue 函数中):
函数 blk_queue_make_request 将函数 __make_request 的地址赋予了请求队列 q 的 make_request_fn 成员
__make_request 函数的主要工作为:
- 检测请求队列是否为空,若是,延缓驱动程序处理当前请求(其目的是想积累更多的请求,这样就有机会对相邻的请求进行合并,从而提高处理的性能),并跳到3,否则跳到2
- 试图将当前请求同请求队列中现有的请求合并,如果合并成功,则函数返回,否则跳到3
- 该请求是一个新请求,创建新的请求描述符,并初始化相应的域,并将该请求描述符加入到请求队列中,函数返回
将请求放入到请求队列中后,何时被处理就由 IO 调度器的调度算法决定了(有关 IO 调度器的算法内容请参见参考资料)。一旦该请求能够被处理,便调用请求队列中成员 request_fn 所指向的函数处理。
request_fn 函数是块设备驱动层的入口。它是在驱动程序创建请求队列时由驱动程序传递给 IO 调度层的。IO 调度层通过回调 request_fn 函数的方式,把请求交给了驱动程序。而驱动程序从该函数的参数中获得上层发出的 IO 请求,并根据请求中指定的信息操作设备控制器(这一请求的发出需要依据物理设备指定的规范进行)。
当设备完成了 IO 请求之后,通过中断的方式通知 cpu ,而中断处理程序又会调用 request_fn 函数进行处理。当驱动再次处理该请求时,会根据本次数据传输的结果通知上层函数本次 IO 操作是否成功,如果成功,上层函数解锁 IO 操作所涉及的页面(在 do_generic_mapping_read 函数中加的锁)。该页被解锁后, do_generic_mapping_read() 函数就可以再次成功获得该锁(数据的同步点),并继续执行程序了。之后,函数 sys_read 可以返回了。最终 read 系统调用也可以返回了。
至此, read 系统调用从发出到结束的整个处理过程就全部结束了。