Chinaunix首页 | 论坛 | 博客
  • 博客访问: 692160
  • 博文数量: 192
  • 博客积分: 1875
  • 博客等级: 上尉
  • 技术积分: 2177
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-23 23:21
个人简介

有时候,就是想窥视一下不知道的东东,因为好奇!

文章分类

全部博文(192)

文章存档

2024年(8)

2023年(3)

2020年(1)

2019年(1)

2018年(1)

2017年(2)

2016年(69)

2015年(53)

2014年(14)

2013年(1)

2012年(5)

2011年(25)

2010年(9)

分类: LINUX

2016-08-05 23:41:12

内核知识收集
一. 概述
       在块设备上的操作,涉及内核中的多个组成部分。下图
    
  
系统调用read()读取磁盘上的文件。内核响应步骤如下: 
    a. 系统调用read()传递文件描述符/文件偏移量/读取长度等参数, 触发相应的VFS函数。
     b. VFS确定请求的数据是否已经在缓冲区中, 若数据不在缓冲区中, 确定如何执行读块设备操作。
            内核将大多数最近从块设备读出或写入的数据保存在RAM,所以有时候没必要访问磁盘上的数据。

     c. 内核通过映射层(Mapping Layer)确定数据在物理设备上的位置。由对应文件所在的文件系统(ext4/reiserfs/xfs等)来确定。
            1>内核取得文件所在文件系统的块大小,计算所请求数据块的长度,确证数据所在块号。
            2>映射层调用具体文件系统的函数,访问磁盘节点,根据逻辑块号确定所请求数据在磁盘上的位置。
            3>磁盘也分块,内核须确定数据块(通用块)在磁盘上的块号(磁盘块)。

     d. 内核通过通用块层(Generic Block Layer)在块设备上执行读操作,启动I/O操作, 传输请求的数据。
            1>一个i/o操作只针对一组连续的块,请求的数据不必位于相邻的块中。
            2>通用块层为所有的块设备提供一个抽象视图,隐藏硬件块设备的差异,可用通用数据结构描述。

     e. 内核I/O调度层(I/O Scheduler Layer)根据内核的调度策略, 对等待处理的I/O等待队列排序。
            把物理介质上相邻的数据请求聚集在一起。

     f. 块设备驱动(Block Device Driver)通过向磁盘控制器发送相应的命令,执行真正的数据传输

这里讲述上面步骤e通用块层的工作过程

二. 请求和请求队列
        设备驱动可以一次传输一个扇区(sector),但块I/O层并不会对一个扇区执行一次独立
的I/O操作, 因为在磁盘表面上定位一个扇区的位置,是很耗时的工作, 会导致磁盘性能的下降。
取而代之的是,内核尽可能聚集多个扇区,作为一次操作,这样就减少磁头的移动操作。
       当内核读或写一些磁盘数据时,它就创建一个块设备请求(block device request)。该请求主
要描述请求的扇区、操作的类型(读还是写)。然而,当请求创建后,内核并不是立即执行请求,
而是I/O操作仅仅是调度,且在稍后的时间才真正执行I/O操作。这种延迟是提高块设备性能的关键。
有新的数据块请求时,内核检查是否可以增大现有等待的请求来满足请求(即新请求的满足,
不需要进一步的查找操作);因为访问磁盘往往是顺序的,于是这种简单的提高块设备性能机制非
常有效。
       延迟请求的执行,使对块设备的处理变得复杂。例如,假设一个进程打开一个普通的文件,进
而文件系统驱动从磁盘上读取文件的inode信息。块设备驱动将请求挂在队列上,进程挂起直到inode
数据传输完毕。然而块设备驱动本身不能阻塞,因为其他其他访问该磁盘的进程也会被阻塞。
        为了避免块设备驱动阻塞,每个I/O操作的处理都是异步的。即块设备驱动是中断驱动的(ULK3-P541),
通用块设备层触发I/O调度器来创建一个新块设备请求或者扩大已有的请求,然后结束。过一段时间后,
块设备驱动激活并调用策略例程(strategy routine)来选择一个等待的请求,向磁盘控制器发送相
应的命令来完成请求。当I/O操作结束时,磁盘控制器发起一个中断,必要时,相应的处理程序又调
用策略例程去处理队列中的另一个请求。
        每个块设备驱动维护自己的请求队列(request queue),队列中包含了对该设备的等待请求链
表。若磁盘控制器处理多个磁盘,通常每个块设备都会有一个请求队列。在各自的请求队列上进行
单独的I/O调度,从而提高磁盘性能。
  a. 请求队列
           请求队列由数据结构request_queue表示。代码略
           事实上,一个请求队列就是双链表,其中的每个元素是请求(request)描述符。
           成员变量queue_head是请求队列中保存链表头,请求(request)描述符中的queuelist成员变量
将各个请求连接起来。请求队列中各个元素的顺序与各自的设备驱动程序相关。I/O调度器有多种方
式来对请求排序,这部分内容在I/O调度器中介绍。

       b. 请求
        对每个块设备的等待请求表示为请求描述符(request discriptor),保存在request数据结构中。代码略
        每个请求由一个或多个bio结构组成。如图2所示。最初,通用块设备层创建一个请求,请求只
包含一个bio。之后,通过加一个新段(segment)到原来的bio中,或者将其他的bio链接到该请求
中;I/O调度器可能会扩展该请求。这种情形可能性比较大,因为新请求的数据可能与已存在请求的
 数据相邻。request数据结构中的成员变量bio指向该请求中的第一个bio结构,biotail指向最后一个
bio结构。宏rq_for_each_bio可以遍历请求中的所有bio结构。
        request数据结构中的某些成员变量,在运行中可能会动态地发生变化。例如,当一个bio结构
中的数据传输完毕后,request中的成员变量bio就会更新,指向请求中的下一个bio。同时新的bio
可以加入到请求中,也即biotail也会发生变化。
       在磁盘扇区传输的过程中,request数据结构中的其他一些成员变量可能会被I/O调度器或设备
驱动改变。例如,nr_sectors是请求中所有仍需要传输的扇区数,current_nr_sectors保存当前bio
中待传输的扇区数。
       request数据结构中的成员变量flags保存一些标志。最重要的标志是REQ_RW,它决定数据传
输的方向(读还是写)。
  
三. IO调度器
         如果简单地以内核产生请求的次序直接将请求发给块设备的话,那么块设备性能肯定让人难以
接受,磁盘寻址是整个计算机中最慢的操作之一,每一次寻址—定位硬盘磁头到特定块上的某个位
置—需要花费不少时间。所以尽量缩短寻址时间无疑是提高系统性能的关键。
        为了优化寻址操作,内核不会一旦接收到I/O请求后,就按照请求的次序发起块I/O请求。而是,
它在提交前,先执行合并与排序(merging and sorting),这种操作可以极大地提高系统的整体性
能。在内核子系统中,负责以上操作的被称为I/O调度器(I/O scheduler)。
        I/O调度器将磁盘I/O资源分配给系统中所有挂起的块I/O请求。具体的说,这种资源分配是通过
将请求队列中挂起的请求合并和排序来完成的。注意不要将I/O调度器和进程调度器混淆。进程调度
器的作用是将处理器资源分配给系统运行的进程。这两种子系统看起来非常相似,但并不相同。进
程调度器和I/O调度器都是将虚拟资源分配给多个对象,对进程调度器来说,处理器被系统中运行的
进程共享、这种虚拟提供给用户的就是多任务和分时操作系统,像Unix系统、相反,I/O调度器虚拟
块设备给多个磁盘请求,以便降低磁盘寻址时间,确保磁盘性能的最优化。

        a. 调度器干啥
            I/O调度器的工作是管理块设备的请求队列。它决定队列中的请求排列顺序及在什么时候派发请
求到块设备。这样做有利于减少磁盘寻址时间,从而提高全局吞吐量。注意“全局”的的含义,I/O
调度器可能为了提高系统整体性能,而对某些请求不公。
            I/O调度器通过两种方法减少磁盘寻址时间:合并(merging)与排序 (sorting)。合并指将两
个或多个请求结合成一个新请求。考虑一下这种情况,文件系统提交请求到请求队列—从文件中读
取一个数据区,如果这时队列中已经存在一个请求,它访问的磁盘扇区和当前请求访问的磁盘扇区
相邻,那么这两个请求就可以合并为一个对单个或多个相邻磁盘扇区操作的新请求。通过合并请求,
I/O调度器将多次请求的开销压缩为一次请求的开销。更重要的是,请求合并后只需要传递给磁盘一
条寻址命令,就可以访问到请求合并前必须多次寻址才能访问完的磁盘区域了,因此合并请求显然
能减少系统开销和磁盘寻址次数。
           假设在读请求被提交给请求队列的时候,队列中并没有其他请求需要操作相邻的扇区,此时就
无法将当前请求和其他请求合并,当然,可以将其插入请求队列的尾部。但是如果有其他请求需要
操作磁盘上类似的位置呢?如果存在一个请求,它要操作的磁盘扇区位置与当前请求的比较接近,
那么是不是该让这两个请求在请求队列上也相邻呢?事实上,I/O调度器的确是这样处理上述情况
的,整个请求队列将按扇区增长方向有序排列。使所有请求按硬盘上扇区的排列顺序有序排列(尽
可能的),目的不仅是为了缩短单独一次请求的寻址时间,更重要的优化在于,通过保持磁盘头以
直线方向移动,缩短了所有请求的磁盘寻址时间。该排序算法类似于电梯调度—电梯不能随意的从
一层跳到另一层,它应该向一个方向移动,当抵达了同一方向的最后一层后,再掉头向另一个方向
移动。处于这种相似特性,I/O调度器也被称为电梯调度。
             
        b. 调度算法
            1. Noop
            2. CFQ
            3. "最后期限"算法
            4. "预期"算法
四. 发送请求到IO调试器
        a. __make_request()
            generic_make_request()触发请求队列的make_request_fn方法来发送一个请求到I/O调度器。
对于块设备,该方法通常由__make_request()实现,代码在文件driver/block/ll_rw_blk.c中。它接收的参数
有request_queue描述符q和bio描述符bio。
            __make_request()主要的操作...
            ......
        b. add_request()
            __make_request(),假设执行到第(5)步,即bio必须插入到一个新
的请求中,最终会调用add_request(q,req)
            ......
        c. 请求队列的由来
            块设备的请求队列是何时创建的?在通用块设备层又是如何从bio结构体从获取块设备的队列?
假设系统中有多个块设备,那么通用块设备层和I/O调度层, 又是如何能将上层的I/O请求加入正确的块设
备请求队列中?
            块设备的请求队列是在设备驱动初始化时创建。一个请求队列就是一个动态的数据结构,该结构
必须由块设备的I/O子系统创建。创建和初始化. 请求队列的函数是:
            request_queue_t * blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
该函数的参数是处理这个队列的rfn函数指针和控制访问队列权限的自旋锁。由于该函数主要负
责分配内存(实际上是分配很多的内存),因此可能会失败;所以在使用队列前一定要检查返回值。
           块设备的I/O请求,都会经过通用块设备层, 即都会执行generic_make_request()函数, 
其中通过bdev_get_queue(bio->bi_bdev)函数即可获取块设备请求队列。
            
            

阅读(1244) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~