块设备的读流程分析

关于VFS的通用读,我们不做考虑,本文以如下函数为根,往下分析:

do_generic_mapping_read(*ppos,*mapping,*desc)

本函数的目的是,从磁盘读数据到用户态,

先是从*ppos开始的页,一直读到*ppos+desc->count 为止的,这么多个页,

然后拷贝desc->count字节的数据到用户态。

也即,从磁盘读到内存缓冲区是按页读的,而从内存缓冲区读到用户态是按字节的。

函数核心是调用

mapping->a_ops->readpage(filp, page);

将磁盘数据读到指定page。

这个回调readpage就是各个文件系统相关的了。

以ext2为例,这个值是ext2_readpage,其实就是

mpage_readpage(page, ext2_get_block);

来考察do_mpage_readpage:

mpage_readpage()的主要工作是判断页的缓存块在磁盘上的块是否连续,
如果连续,则此页可以只提交一个bio请求,然后返回。
如果不连续,则调用block_read_full_page对页的每个缓存块提交一个bio请求。

这里备个忘:对于一个bio里的bio_vec数组,每个成员都代表一段磁盘地址连续的数据缓冲段。

也即一个段。

既然要把磁盘数据读到这个page上,那么就要找出这个page应该对应磁盘的哪个位置。
一个page对应着文件的某个固定位置,而文件所在的inode肯定知道数据放在哪里。
所以自然需要先根据page取inode

这个inode用来做什么?当时是要取这个inode文件所在设备dev了,不然怎么去查磁盘里的数据(这个inode主要给get_block函数用)。

struct inode *inode = page->mapping->host;

除此之外,还顺带获取inode所支持的块大小参数,以便后续计算偏移。

我们来看这种通用的情况,假设这个页被划分成4个块缓冲(通常情况下块大小为512字节,页大小是4K,于是每个页面有4个块)。

很重要的一点,就是判断,这4个缓冲,

对应的磁盘上的数据,是否连续。为什么要判断是否连续?我们知道,对于块设备来说,

磁盘上物理地址相近的寻址,肯定效率高的多。所以,这里埋下一个伏笔,我们

每次尽量提交磁盘地址相近的请求,这也是后面要讲的io调度。

判断页内的块缓冲,在磁盘是否相近,是通过page->private是否有值来完成。

那page->private的值在什么时候设置?
大略的讲,是在内核发现第n个块,与第n-1个块的磁盘号不连续时,设置的。

理论说多了,头脑会晕,我们来情景分析2个场景,
在看这两个场景前,先来一段公共操作。

首先,要找出这个页里的第一块,是在文件的哪个块,毕竟磁盘都是以块为单位操作的。
做法是,先算出页在文件里的偏移字节数:
page->index << PAGE_CACHE_SHIFT
其中,page->index是该页在mapping里的index,左移PAGE_CACHE_SHIFT就得到mapping里该page的字节偏移。
接着将该值除以块大小,得到块在文件里的偏移号:


page->index << PAGE_CACHE_SHIFT >> blkbits

block_in_file = page->index << (PAGE_CACHE_SHIFT - blkbits);

好,现在分别来看两个场景。

场景1)  初次访问文件。

很明显,这个时候刚刚在mapping的address_space里分配了一块纯净的page。
   page的private字段为0,因此代码姑且认为此page里的块缓冲都是磁盘连续的。
   于是,依次对页内的所有块进行处理(一般一个页有4个块)
将这些页内块号,传递给文件系统相关的get_block函数,即ext2_get_block,
计算出每个块在磁盘上的块号。假设得到第n个块的磁盘编号s(n),那么还要与前一个
块比较,是否编号连续,即s(n)是否等于s(n-1)+1
如果不等,则说明此page内的块缓冲在磁盘上不连续,需要额外处理。

 1.1) 假如块在磁盘上连续存放

则把这些块号依次保存到局部blocks数组。接着就分配新bio。

关键是把bio->bi_sector设置成这些块的第一个扇区号(因为磁盘连续),
并分配一个bio_vec,将此bio_vec的page设置为此page,并且offset设置为0(页内偏移),
长度则设置为PAGE_SIZE(不考虑文件洞)
最后就submit_bio把数据提交给块设备层。

可以看出,对于磁盘连续的情况,该page并没有为其分配块缓冲首部,同时也没有给page->private
置位。

1.2) 假如块在磁盘上非连续存放

则需要给页内的每个块都单独提交bio。

这是靠block_read_full_page来完成的。
   先检查此page的private标志,如果没有设置,则说明需要分配新缓冲区首部来指示这个page。
   这个是通过create_empty_buffers来完成的。
 
  同样的,根据page在mapping里的index,算出页的第一个块的序号index,接着对从index到
  index+3的4个块,分别调用ext2_get_block,算出各自在磁盘上的序号b_blocknr,从而生成最重要的
  bh结构(dev,b_blocknr),接着对这4个bh提交bio,即submit_bh(READ, bh);
  submit_bh新生成一个bio,
  bio的内存缓冲数据(读文件目的地址):
  bio_vec[0]的page设置为新page,bio_vec[0]的bv_len为块默认大小,
  bio_vec[0]的bv_offset(页内偏移)为相应的块在页内偏移。

bio的磁盘地址(读文件源地址):
  bio->bi_sector根据之前get_block的结果bh->b_blocknr计算
  bio->bi_bdev设置为文件所在块设备dev,这样有了dev和设备块逻辑号,即可定位块设备磁盘的扇区位置。

场景2)  之前已经访问过文件

根据场景1我们知道,如果文件的这个页page里的块数据,在磁盘中是分散存放的,那么这个page就会对应一个
缓冲区首部链表;如果连续,那么page的private是空。

对于连续存放的情况,每次走到do_mpage_readpage,都会对4个块执行ext2_get_block,
检查相邻的块在磁盘是否连续。也就是说,对于连续存放的情况,代码并没有做优化,而是
仍然每个块都要深入驱动的代码,查找对应的磁盘扇区。

对于非连续存放的情况,由于page的private保存了上次访问时设置的块缓冲区首部
(即bh带BH_Mapped标志,表示缓冲区首部的b_bdev和b_blocknr是有效的),因此可以
直接根据上次的结果,即保存的bh链表,去查找每个块在磁盘的扇区位置。

如果要优化连续存放的场景,笔者认为,可以给page->flag添加字段,来区分此page对应
的块在磁盘上是否连续,这样可以借助第一次访问得到的块磁盘扇区信息,直接操作磁盘,

这样就少了深入驱动查找磁盘扇区号的操作,也许可以提高性能。

接下来,简单说明一下submit_bio。
可以认为,submit将提交的bio封装为request,然后按一定规则插入与块设备相关的请求队列。
块设备的请求队列,是由块设备驱动分配,并且一个块设备只有一个请求队列。
所以,对于在同一个块设备的操作,需要对请求队列的插入做互斥。但是,如果系统有n块磁盘,
则就算这些磁盘用同一个块设备驱动,也需要n个请求队列,各个磁盘的操作就互不影响
,这样可以提高性能。

插入请求队列的函数是q->make_request_fn,该函数的期望是将上层提交的
bio经过排序、归并后,封装为request结构插入队列q中。这个排序、归并的算法就是
传说中的IO调度。

对于一个request来说,和bio一样,请求区间在磁盘上是连续的,这点很重要。

对于一个块设备来说,有两个队列,一个是驱动提供的队列q,另一个是各IO调度算法内部队列。
IO调度算法收到bio请求后,将其合并成request,然后排序插入IO内部队列,最后将合适的request
转移到驱动队列q,由驱动去自行提取q上的request。

来看合并和排序是怎么个概念:
合并,指的是将磁盘号临接的请求,合并成一个请求。
例如,request队列里,某个request的磁盘请求区间块为

(1024,2048),

如果某个bio的请求区间为
(2048,2048+512),

则该bio可以后向合并到

request(1024,2048+512);

如果bio的请求区间为

(1024-512,1024),

则可前向合并到

request(1024-512,2048);
如果bio的请求区间为

(3072,3072+512),

则此bio无法合并,需要新生成一个request。

如果不能合并,则需要把这个新生成的request,排序插入到IO调度算法队列里的对应位置。

再回到q->make_request_fn,在驱动初始化时可以指定q->make_request_fn的值,如果没有指定,
则默认是__make_request。
__make_request需要进行IO调度,即执行合并或者排序。
合并的代码在elv_merge(q, &req, bio);
如果不能合并,则get_request新生成一个request,将bio的值传递给此request,然后通过
add_request执行插入排序。

__elv_add_request(q, req, ELEVATOR_INSERT_SORT, 0);

对于deadline电梯算法来说,__elv_add_request会根据request->sector,插入到rb-tree,
然后deadline_move_request将rb-tree里的某个entry移动到驱动的q队列。
至于具体怎么选择这个request,就是算法核心相关了,由于笔者走到这里已经半夜12点,
因此不打算继续分析,后续有需求再说。

到最后,设备驱动需要提供一个do_request函数,这个函数遍历驱动的q队列,
挨个取出request,然后遍历request的各个段,将各个段的数据提交给scis层,

数据传输完毕。

那驱动是什么时候被激活去处理这些request呢?答案是定时处理。

定时器超时的时候,唤醒一次kblockd线程,kblockd会执行blk_unplug_work,最终去执行驱动的request。

具体流程代码大体如下:

 1 static void blk_unplug_timeout(unsigned long data)
 2 {
 3     request_queue_t *q = (request_queue_t *)data;
 4     blk_add_trace_pdu_int(q, BLK_TA_UNPLUG_TIMER, NULL,
 5                 q->rq.count[READ] + q->rq.count[WRITE]);
 6     kblockd_schedule_work(&q->unplug_work);
 7 }
 8 INIT_WORK(&q->unplug_work, blk_unplug_work);
 9 static void blk_unplug_work(struct work_struct *work)
10 {
11     request_queue_t *q = container_of(work, request_queue_t, unplug_work);
12     blk_add_trace_pdu_int(q, BLK_TA_UNPLUG_IO, NULL,
13                 q->rq.count[READ] + q->rq.count[WRITE]);
14     q->unplug_fn(q);
15 }
16 q->unplug_fn        = generic_unplug_device;
17 void __generic_unplug_device(request_queue_t *q)
18 {
19     if (unlikely(blk_queue_stopped(q)))
20         return;
21     if (!blk_remove_plug(q))
22         return;
23     q->request_fn(q); //驱动的request
24 }

io调度可以控制plug和unplug的速度,来累计尽可能多的连续磁盘地址的request,以提高磁盘访问效率。

块设备的读流程分析

时间: 2024-08-28 04:54:58

块设备的读流程分析的相关文章

Linux块设备加密之dm-crypt分析

Linux块设备加密之dm-crypt分析 来自 http://blog.csdn.net/sonicling/article/details/6275898 这篇文章算是<Device Mapper代码分析>的后续篇,因为dm-crypt是基于dm框架的,因此与上一篇一样,也以2.6.33内核代码为基础来讲述代码的分析过程.但是本文侧重点不同在于着重分析一下三个方面: 1.Linux密码管理 2.dm-crypt到与Linux密码的关联 3.dm-crypt的异步处理 一.Linux密码管理

linux块设备读写流程

在学习块设备原理的时候,我最关系块设备的数据流程,从应用程序调用Read或者Write开始,数据在内核中到底是如何流通.处理的呢?然后又如何抵达具体的物理设备的呢?下面对一个带Cache功能的块设备数据流程进行分析. 1. 用户态程序通过open()打开指定的块设备,通过systemcall机制陷入内核,执行blkdev_open()函数,该函数注册到文件系统方法(file_operations)中的open上.在blkdev_open函数中调用bd_acquire()函数,bd_acquire

块设备驱动程序

通用块层 常用数据结构: bio 磁盘描述符 gendisk generic_make_request 是通用块层的入口点 io调度层: 请求队列:request_queue 请求描述符:request 块设备: block_device 注册块设备 register_blkdev    预定主设备号. 块设备文件操作描述符表: open  blkdev_open release blkdev_close llseek block_llseek read genric_file_read wrt

Raid1源代码分析--读流程

我阅读的代码的linux内核版本是2.6.32.61.刚进实验室什么都不懂,处于摸索阶段,近期的任务就是阅读raid1的源码.第一次接触raid相关的东西,网上分析源码的资料又比较少,不详细.逐行阅读代码,做了笔记.如果要对raid1的读流程有个整体上的把握,需要将笔记中的主线提炼出来,这里不写了.理解不足或者有误之处,希望批评指正. 读流程主要涉及以下函数: 请求函数make_request 读均衡read_balance 回调函数raid1_end_read_request 读出错处理rai

Linux中块设备驱动程序分析

基于<Linux设备驱动程序>书中的sbull程序以对Linux块设备驱动总结分析. 开始之前先来了解这个块设备中的核心数据结构: struct sbull_dev { int size;                       /* Device size in sectors */ u8 *data;                       /* The data array */ short users;                    /* How many users

嵌入式驱动开发之--- 虚拟磁盘SBULL块设备驱动程序分析

#define SBULL_MINORS  16         /* 每个sbull设备所支持的次设备号的数量 */ #define KERNEL_SECTOR_SIZE 512  // 本地定义的常量,使用该常量进行内核512字节到实际 // 扇区大小的转换 #define INVALIDATE_DELAY  30*HZ 块设备的核心数据结构(the internal representation of our device): struct sbull_dev{ int size;    

块设备驱动架构分析

1. 块设备概念:块设备是指只能以块为单位进行访问的设备,块的大小一般是512个字节的整数倍.常见的块设备包括硬件,SD卡,光盘等.</span> 上边是通过一个编写好的块设备驱动,然后安装块设备驱动以及一些相关操作来体会块设备驱动!(此处省略) 2. 块设备驱动的系统架构 2.1 系统架构---VFS VFS是对各种具体文件系统的一种封装,用户程序访问文件提供统一的接口. 2.2 系统架构---Cache 当用户发起文件访问请求的时候,首先回到Disk Cache中寻址文件是否被缓存了,如果

块设备驱动框架分析(二)

参考:块设备驱动之一  块设备驱动之二  块设备驱动之三 总结上一篇的块设备驱动的步骤: 1. 分配gendisk: alloc_disk static struct gendisk * ramblock_disk = alloc_disk(16); /* 次设备号个数: 分区个数+1 */2. 设置2.1 分配/设置队列: // 它提供读写能力static struct request_queue  * ramblock_queue = blk_init_queue(do_ramblock_r

块设备驱动之I/O调度层之调度算法

通过generic_make_request提交请求给I/O调度层,这个函数最后调用到q->make_request_fn(q, bio),那么对于这个函数的调用就是I/O调度层的入口点,首先来看看这个make_request_fn在哪被赋于能量的 void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn) { /* * set defaults */ q->nr_requests = BLKDEV_MA