精华内容
下载资源
问答
  • 典型的块设备包括
    千次阅读
    2021-10-16 19:24:08

    2020-03-24 19:11:40

    1、块存储

    - 典型设备: 磁盘阵列,硬盘

    - 主要是将裸磁盘空间映射给主机使用的。

    - 使用场景:

    • - docker容器、虚拟机远程挂载磁盘存储分配。

    • - 日志存储。

    • - 文件存储。

    • - ...

    2、文件存储

    - 典型设备: FTP、NFS服务器为了克服块存储文件无法共享的问题,所以有了文件存储。在服务器上架 设FTP与NFS服务,就是文件存储。

    - 使用场景:

    • - 日志存储。

    • - 多个用户有目录结构的文件存储共享。

    • - ...

    文件存储通常需要一个app 的服务器将文件存储挂载到本地。

    3、对象存储

    - 典型设备: 内置大容量硬盘的分布式服务器(swift, s3),多台服务器内置大容量硬盘,安装上对象存储管理软件,对外提供读写访问功能。

    - 使用场景: (适合更新变动较少的数据,没有目录结构, 不能直接打开/修改文件)

    • - 图片存储。

    • - 视频存储。

    • - 文件

    • - 软件安装包

    • - ...

    对象存储实际上更贴近http的应用。

    区别说明:

        通常我们说存储分为块、文件和对象,从应用场景来看:

        对象存储近年来在国内市场逐渐火热起来,主要是特定行业的海量非结构化数据的应用场景。由于对象存储采用扁平的文件组织方式,所以在文件量上升至千万、亿级别,容量在PB级别的时候,这种文件组织方式下的性能优势就显现出来了,文件不在有目录树深度的问题,历史和近线数据有同样的访问效率。另外,对象存储多采用分布式架构,可以在商用x86服务器上轻松构建对象存储,磁盘介质也大多采用低速的SATA盘,所以在成本上也具优势。

        块存储主要用于结构化数据类应用,在相对小的容量空间上可以提供更高的IO性能,所以从后端存储产品形态上来看,一般是多控多活的紧耦合集中式SAN架构,存储介质大多采用高速的SAS和近年来已成主流的固态硬盘,前端多采用FC光纤协议组网。另外,存储双活、存储虚拟化、存储复制等技术也支撑着这块的生态。

        文件存储主要是集中式或分布式的NAS类应用,以灵活的共享文件系统为特色,承载百万文件量和TB级别的文件类应用。

        我的总结:说白了,块存储,是最底层的存储,它关注的是磁盘的基本存储单元——块(Block),块存储可以没有软件服务器。而文件存储(像NAS)和对象存储(S3、七牛云),是软件层面的存储,底层也是基于块存储的。而文件存储和对象存储的区别在于,他们组织文件的方式,文件存储是有目录树的、有标准属性的(权限、用户、读写),而对象存储是扁平的、没有目录的、附加属性是灵活的,他们的接口协议也不一样。对象存储接口主要以S3与Swift为代表,其接口就是简单的GET、PUT、DEL和其他扩展,文件存储的话是以POSIX接口为主,以libcephfs为代表。有些分布式存储,比如Ceph,是同时支持块存储、对象存储和文件存储的,只是对应的接口不同,强烈建议看下这篇文章:一篇文章让你理解Ceph的三种存储接口(块/文件/对象)

        这三种存储,分别对应了不同的访问协议,这也就决定了他们的本质差别。

    • 先说一下文件存储,主要操作对象是文件和文件夹。以 NFS 为例,文件相关的接口包括:LOOKUP/ACCESS/READ/WRITE/CREATE/REMOVE/RENAME 等等,文件夹相关的接口包括:MKDIR/RMDIR/READDIR 等等。同时也会有 FS_STAT/FS_INFO 等接口用于提供文件系统级别的信息。POSIX,SAMBA 等也是文件存储协议。协议更注重接口的灵活,以及访问权限控制。

    • 块存储,主要操作对象是磁盘。以 SCSI 为例,主要接口有 Read/Write/Read Capacity/Inquiry 等等。FC,iSCSI,也是块存储协议。和文件存储相比,没有文件和目录树的概念,一般协议也不会定义磁盘的创建和删除操作。协议更注重传输控制。

    • 对象存储,主要操作对象是对象(Object)。以 S3 为例,主要接口有 PUT/GET/DELETE 等。和块存储相比,没有随机读写的接口。和文件存储相比,没有目录树的概念。协议更注重简洁。

    问两个问题:

        0、文件存储,底层是块存储。那么对象存储,底层也是文件存储吗?这个问题有点难解释,后面单独说明。

        1、FastDFS、HDFS属于文件存储还是对象存储?答案:对象存储。或者说“基于文件存储的对象存储”。

        2、作为容器的持久化卷,应该用那种存储?答案:要使用块存储,或支持POSIX接口的文件存储,比如NAS、GlusterFS、CephFS等。

    解释:

        对象存储,底层不一定是文件存储,也可能是跳过文件,直接到块存储。我们通常眼睛看见的文件,只不过是操作系统给我们形象化的数据标志(一个图标、一个名字),对文件的操作是基于操作系统的文件系统函数。但是文件系统不止一种,有一些虚拟的文件系统,它甚至可以不直接使用操作系统的文件系统,而是直接调用更底层的命令,甚至直接操作物理存储的块数据。这个地址:http://www.dbseeker.com/blog/?post=7,是说Oracle的底层数据存储,可以看到Oracle自己做了一个逻辑卷管理,这个逻辑卷管理,它的逻辑块可以直接映射物理块数据。这里再做个答疑:Oracle能部署在NAS卷上面吗?答案是:可以,但是性能下降很大(有人说下降100倍),理论上可以,那是因为NAS磁盘,应该是兼容操作系统内核的绝大部分API,Oracle的逻辑卷管理,也是用的操作系统的底层API(非上层的文件系统API),所以没问题。但是NAS和本地磁盘还是有所区别,比如阿里云的官方文档上写着NAS 上使用 inotifywait (Linux文件系统函数)存在的问题,另外NAS的应用场景里面没有提到数据库:NAS的应用场景

        当然,并不是所有对象存储,都跳过了文件系统,像Kafka、HDFS的数据,都是直接写文件,只是会将原文件拆分,而FastDFS更省事,直接将原文件完整保存下来,本质上这些都是基于文件存储来实现的(但是HDFS、FastDFS的归类还是对象存储)。

    关于POSIX标准、文件系统类型、VFS及FUFS,参见这篇文章:《文件系统、POSIX标准及VFS》

    更多相关内容
  • 典型的字符设备包括鼠标、键盘、串行口等。 字符设备与块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般紧接着发生。块设备则不然,它利用一块系统内存作为缓冲区,若用户进程对设备的请求能...
    在Linux操作系统下有3类主要的设备文件类型:块设备、字符设备和网络设备。这种分类方法可以将控制输入/输出设备的驱动程序与其他操作系统软件分离开来。字符设备是指存取时没有缓存的设备。典型的字符设备包括鼠标、键盘、串行口等。
    字符设备与块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般紧接着发生。块设备则不然,它利用一块系统内存作为缓冲区,若用户进程对设备的请求能满足用户的要求,就返回请求的数据;否则,就调用请求函数来进行实际的I/O操作块设备主要是针对磁盘等慢速设备设计的,以免耗费过多的CPU时间用来等待网络设备可以通过BSD套接口访问数据
    一.主设备号和次设备号
    主设备号标识设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。我们可以通过次设备号获得一个指向内核设备的直接指针,也可将次设备号当作设备本地数组的索引,不管用哪种方式,除了知道次设备号用来指向驱动程序所实现的设备之外,内核本身基本上不关心关于次设备号的任何其他消息。
    ◎设备编号的内部表达
    内核用dev_t类型(<linux/types.h>)来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20位表示次设备号。
    在实际使用中,是通过<linux/kdev_t.h>中定义的宏来转换格式。
     (dev_t)-->主设备号、次设备号       MAJOR(dev_t dev)     MINOR(dev_t dev)
     主设备号、次设备号-->(dev_t)       MKDEV(int major,int minor) 
    ◎分配和释放设备编号
    建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在<linux/fs.h>中声明:
    int register_chrdev_region(dev_t first, unsigned int count,char *name);   //指定设备编号
    int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count, char *name);  //动态生成设备编号
    void unregister_chrdev_region(dev_t first, unsigned int count);      //释放设备编号 
    分配之设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
    ◎动态分配主设备号

        某些主设备号已经静态地分配给了大部分公用设备。在内核源码树的Documentation/device.txt......


    可以讲字符设备和块设备归为一类,它们都是可以顺序/随机地进行读取和存储的单元,二者驱动主要在于块设备需要具体的burst实现,对访问也有一定的边界要求。其他的没有什么不同。
    网络设备是特殊设备的驱动,它负责接收和发送帧数据,可能是物理帧,也可能是ip数据包,这些特性都有网络驱动决定。它并不存在于/dev下面,所以与一般的设备不同。网络设备是一个net_device结构,并通过register_netdev注册到系统里,最后通过ifconfig -a的命令就能看到。
    不论是什么设备,设备级的数据传输都是基本类似的,内核里的数据表示只是一部分,更重要的是总线的访问,例如串行spi,i2c,并行dma等。

    展开全文
  • dm-cache 与 bcache在 LSFMM 2013 峰会上,Mike Snitzer, Kent Overstreet, Alasdair Kergon, 和 Darrick Wong 共同主持了一个讨论,内容是关于两个彼此独立的块设备层缓存方案 —— dm-cache 和 bcache。...



    dm-cache 与 bcache

    在 LSFMM 2013 峰会上,Mike Snitzer, Kent Overstreet, Alasdair Kergon, 和 Darrick Wong 共同主持了一个讨论,内容是关于两个彼此独立的块设备层缓存方案 —— dm-cachebcache。 
    Snitzer 首先介绍了 3.9 kernel 引入的 dm-cache。这个方案使用率内核中的 device mapper 框架,实现了快速设备对慢速的“原始”设备的 writeback 或 writethrough 缓存。Snitzer 介绍到,dm-cache 的原理是使用 device mapper 核心,并在上面增加一个策略层。这个策略层“非常类似”一个插件接口,可以实现不同的策略。这些策略(以及缓存的内容)决定了缓存是否可以命中,是否需要进行数据迁移(在原始设备与缓存设备之间进行某个方向的数据移动)。目前已经实现了包括最近最少用(LRU)、最常使用(MFU)等策略,但目前只有缺省的“mq”策略合并到内核中了,以便减少起初需要测试到的策略数量。文件系统可以为策略提供 hints,比如某些块脏了,或是某些块已经被放弃了。这些信息有助于策略更好地决定块要存储到的位置。
    之后,Overstreet 介绍了 bcache 的状态更新,后者已经在排队进入 3.10 了。他介绍到,bcache 已经有“很多用户”了,而且代码已经比较稳定了。他最近主要是在进行 bug fix。与 dm-cache 实现的的分级存储不同,按照 Overstreet 的介绍,bcache 更像一个传统的缓存。它可以用来存储任何 extents,甚至是是一个扇区,而 dm-cache 只能对整块数据进行缓存。
    就在峰会开始前,Wong 给多个邮件列表(包括 dm-devel, linux-bcache, linux-kernel)发出过一封 email 来比较 bcache, dm-cache, 和 EnhanceIO。他分别编译了带有上述各个特性的内核,进行了一些测试。他发现,EnhanceIO 是最慢的,bcache 是它的 4-6 倍,而 dm-cache 更快,达到了 15 倍,不过 dm-cache 无法完成有些测试。所有比较都在一块普通硬盘上进行了相同的测试,有时,由于未知的原因,dm-cache 的表现或多或少的和硬盘完全一样。他指出,有些测试会导致 mkfs 创建的 inode table 倍缓存住,而通常这并不是一种有效的使用缓存的方法。Snitzer 表示,他曾试着去重现 Wong 的测试结果,但结果对 bcache 和 dm-cache 都得倒了更差的成绩。他说他希望和 Overstreet 一起去找出原因。而 Overstreet 则表示,不应该过多地看综合测试的成绩,这些测试是游泳的,但可能带有误导性。
    Ric Wheeler 提问,Snitzer 是否“在真实工作负载下看到了实质性的性能提升”,Snitzer提到,在不同的 Git 标签之间进行切换就是 dm-cache 可以完胜的一个例子,不过他需要一些帮助来获得更贴近实际使用的负载。一个参会者问道,这些方案是否假设缓存设备总是存在。Snitzer 说,dm-cache 确实如此假设,但这是需要改进的地方。Overstreet 说, bcache 并不要求 cache 设备一直都在。因为 bcache 已经在实际产品中使用了,所以它有机会去碰到这些疑难场景,并可以处理这些缓存设备无法工作的情形。Snitzer 表示,dm-cache 确实还有很多事情要做。起初,它是进行 cache 和原始设备的并行 IO,但最终不得不回到顺序 IO。而且,对于流水线中有 NVM 设备的情况下,存储层次也是如此。因为 dm “到处都是分层”,dm-cache 刚好适合进行存储分层,不过 Overstreet 指出,bcache 也可以做到分级存储。这个讨论最终没有一个明确的结论,只是提到需要让两个方案的“真实世界”性能读数变得更好。同时也指出了,不同的测试者进行的测试得到的结论差异是巨大的,这也是真实世界的情况的一个写照。
    - See more at: http://wangxu.me/blog/#sthash.smQmCr76.dpuf 


    文章来源:http://blog.csdn.net/cybertan/article/details/9475767






    Linux块设备加速缓存之bcache



    什么是bcache

    bcache是linux内核块层cache。它使用类似SSD来作为HDD硬盘的cache,从而起到加速作用。
    HDD硬盘便宜并且空间更大,SSD速度快但更贵。如果能两者兼得,岂不快哉?bcache能做到。
    bcache使用SSD作为其他块设备cache。类似ZFS的L2Arc,但bcache还增加了写回策略,并且是与文件系统无关的。bcache被设计成只需要最小的代价,无需配置就能在所有环境中工作。默认状态下bcache不缓存顺序IO,只缓存随机读写。bcache适用于桌面、服务器,高级存储阵列,甚至是嵌入式环境。

    设计bcache目标是让被缓存设备与SSD一样快(包括缓存命中、缓存不命中、透写和回写)。现在还未达到初衷,特别是顺序写。同时测试结果表明离目标很接近,甚至有些情况下表现更好,例如随机写。
    bcache是数据安全的。对于写回策略缓存来说,可靠性是非常重要的,出错就意味着丢失数据。bcache是用电池备份阵列控制器的替代选择,同时也要求bcache在异常掉电时也是数据安全的。对于写而言,必须在所有数据写到可靠介质之后才能向上层返回写成功,在异常掉电情况下,写不能是部分完成的。大量工作已经投入到这部分数据安全的工作中。

    bcache性能设计目标是等同于SSD。最大程度上去最小化写放大,并避免随机写。bcache将随机写转换为顺序写,首先写到SSD,然后回写缓存使用SSD缓存大量的写,最后将写有序写到磁盘或者阵列上。对于RAID6阵列,随机写性能很差,还要花费不菲的价格购买带有电池保护的阵列控制器。现在有了bcache,你就可以直接使用linux自带的优秀软RAID,甚至可以在更廉价的硬件上获取更高的随机写性能。

    特性
    1、一个缓存设备可以作为多个设备的缓存,并且可以在设备运行时动态添加和删除缓存。
    2、异常关机恢复,只有当写到磁盘后缓存才会确认写完成。
    3、正确处理写阻塞和刷缓存
    4、支持writethrough, writeback和writearound
    5、检测并避开顺序IO(可配置关闭该选项)
    6、当检测到SSD延迟超过配置边界值,减少到SSD流量(当一个SSD作为多个磁盘缓存时使用)
    7、缓存不命中时预读(默认关闭)
    8、高性能的writeback实现:脏数据都是排序后再回写。如果设置了writeback水位线,PD控制器会根据脏数据比例来平滑处理到后台writeback流量。
    9、使用高效率了B+树,bcache随机读可以达到1M IOPS
    10、稳定,已经有产品应用


    性能
    7/25/12 随机测试
    在我的测试机上,我将SSD盘划分为两个相同大小的分区,一个分区用于测试SSD裸盘,另一个作为硬盘缓存。
    bcache配置修改:cache_mode设置为writeback,writeback_percent设置为40。(如果writeback_percent不为0,bcache使用PD控制器根据缓存的脏数据块来平滑处理下发到磁盘的流量)。同时还关闭了拥塞阀值,因为当SSD延迟达到极限时,如果bcache切换到writethrough将会影响结果。

    SSD盘为Intel 160G MLC SSD,也就是Intel SSDSA2M160。
    FIO作为性能测试,测试脚本如下:
    [global] randrepeat=1 ioengine=libaio bs=4k ba=4k size=8G direct=1 gtod_reduce=1 norandommap iodepth=64
    FIO运行在SSD裸设备上,但对于这类性能测试软件来说应该没有影响。

    裸SSD设备上随机写测试结果如下:
    root@utumno:~# fio ~/rw4k
    
    randwrite: (g=0): rw=randwrite, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=64
    fio 1.59
    Starting 1 process
    Jobs: 1 (f=1): [w] [100.0% done] [0K/49885K /s] [0 /12.2K iops] [eta 00m:00s]
    randwrite: (groupid=0, jobs=1): err= 0: pid=1770
      write: io=8192.3MB, bw=47666KB/s, iops=11916 , runt=175991msec
      cpu          : usr=4.33%, sys=14.28%, ctx=2071968, majf=0, minf=19
      IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
         submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
         complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
         issued r/w/d: total=0/2097215/0, short=0/0/0
    
    Run status group 0 (all jobs):
      WRITE: io=8192.3MB, aggrb=47666KB/s, minb=48810KB/s, maxb=48810KB/s, mint=175991msec, maxt=175991msec
    
    Disk stats (read/write):
      sdb: ios=69/2097888, merge=0/3569, ticks=0/11243992, in_queue=11245600, util=99.99%

    添加了bcache:
    root@utumno:~# fio ~/rw4k
    
    randwrite: (g=0): rw=randwrite, bs=4K-4K/4K-4K, ioengine=libaio, iodepth=64
    fio 1.59
    Starting 1 process
    Jobs: 1 (f=1): [w] [100.0% done] [0K/75776K /s] [0 /18.5K iops] [eta 00m:00s]
    randwrite: (groupid=0, jobs=1): err= 0: pid=1914
      write: io=8192.3MB, bw=83069KB/s, iops=20767 , runt=100987msec
      cpu          : usr=3.17%, sys=13.27%, ctx=456026, majf=0, minf=19
      IO depths    : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
         submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
         complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
         issued r/w/d: total=0/2097215/0, short=0/0/0
    
    Run status group 0 (all jobs):
      WRITE: io=8192.3MB, aggrb=83068KB/s, minb=85062KB/s, maxb=85062KB/s, mint=100987msec, maxt=100987msec
    
    Disk stats (read/write):
      bcache0: ios=0/0, merge=0/0, ticks=0/0, in_queue=0, util=0.00%

    添加了bcache之后IOPS为18.5K,裸SSD设备为12.2K。bcache表现更佳是因为bcache按顺序将写请求发送到SSD,但额外加入了更新索引的开销。bcache对随机写做了优化,bcache还从高IO深度(64)获益,因为在高IO深度的情况下就可以将多次下标更新合并为一次写请求。
    高IO深度就代表着高系统负载,当IO深度下调时IOPS也出现变化:

    IO depth of 32: bcache 20.3k iops, raw ssd 19.8k iops

    IO depth of 16: bcache 16.7k iops, raw ssd 23.5k iops

    IO depth of 8: bcache 8.7k iops, raw ssd 14.9k iops

    IO depth of 4: bcache 8.9k iops, raw ssd 19.7k iops

    SSD性能在不同IO深度时出现了波动。对于不同的写模型会有不同的结果,我们只关注两者的相对数值。
    当测试随机4K写,IO深度为1时,bcache写次数是裸SSD设备的两倍:每一次写都需要再更新一次索引。

    随机读

    IO depth of 64: bcache 29.5k iops, raw ssd 25.4k iops

    IO depth of 16: bcache 28.2k iops, raw ssd 27.6k iops

    bcache略胜一筹,可能跟要读的数据相关。这里的结论是随机读时bcache与裸SSD读性能是相同的。
    这里要注意的是,读4K随机写下去的数据,这样的测试模型对于bcache是不好的。这意味btree都是4K大小,btree将比通常时候大得多。在实际应用中,平均大小是100K。btree变大就意味着索引占用更大的内存空间,并且有一部分是在二级索引。根据个人经验这些开销在大型机器IOPS超过500K时才会有实际影响。
    如果大家有其他的测试方法或者我的测试方法中有什么问题请通知邮件告诉我。

    常见问题
    关机、设备移除
    系统关机时cache仍然是脏的,就是说后端磁盘的数据并不可靠。如果要保证后端磁盘数据是安全的,就需要手动移动cache或者将cache设置为writethrough模式。

    自动挂载
    bcache会自动匹配cache和后端设备。匹配过程与设备对系统可用的次序没有关系。
    带bcache的根目录分区
    为了让根分区能够使用bcache,需要添加rootdelay=3到启动参数,这样才能让udev规则在系统mount根文件系统之前运行。
    已格式化过的磁盘或分区
    如果一个分区或者磁盘设备启动时没有创建bcache,可能是因为超级块发生错误。为了让bcache能够正确检测到之前的设备,udev规则会首先检查是否符合bcache规则和blkid检查。udev规则检查设备超级块从而识别文件系统类型,如果该超级块不符合bcache文件系统类型那么就不会添加bcache。

    # cat /usr/lib/udev/rules.d/61-bcache.rules
    ....
    # Backing devices: scan, symlink, register
    IMPORT{program}="/sbin/blkid -o udev $tempnode"
    # blkid and probe-bcache can disagree, in which case don't register
    ENV{ID_FS_TYPE}=="?*", ENV{ID_FS_TYPE}!="bcache", GOTO="bcache_backing_end"
    ...
    
    # lsblk -o NAME,MAJ:MIN,RM,SIZE,TYPE,FSTYPE,MOUNTPOINT,UUID,PARTUUID
    
    NAME        MAJ:MIN RM   SIZE TYPE FSTYPE MOUNTPOINT UUID                                 PARTUUID
    sda           8:0    0 111.8G disk
    ├─sda1        8:1    0     3G part vfat   /esp       7E67-C0BB                            d39828e8-4880-4c85-9ec0-4255777aa35b
    └─sda2        8:2    0 108.8G part ext2              93d22899-cd86-4815-b6d0-d72006201e75 baf812f4-9b80-42c4-b7ac-5ed0ed19be65
    sdb           8:16   0 931.5G disk
    └─sdb1        8:17   0 931.5G part ntfs              FAD2B75FD2B71EB7                     90c80e9d-f31a-41b4-9d4d-9b02029402b2
    sdc           8:32   0   2.7T disk bcache            4bd63488-e1d7-4858-8c70-a35a5ba2c452
    └─bcache1   254:1    0   2.7T disk btrfs             2ff19aaf-852e-4c58-9eee-3daecbc6a5a1
    sdd           8:48   0   2.7T disk bcache            ce6de517-7538-45d6-b8c4-8546f13f76c1
    └─bcache0   254:0    0   2.7T disk btrfs             2ff19aaf-852e-4c58-9eee-3daecbc6a5a1
    sde           8:64   1  14.9G disk
    └─sde1        8:65   1  14.9G part ext4   /          d07321b2-b67d-4daf-8022-f3307b605430 5d0a4d76-115f-4081-91ed-fb09aa2318d

    在上面的例子中有一个分区之前是ext2文件系统。bcache将通过以下指令自动构建:
    # make-bcache -B /dev/sdc /dev/sdd -C /dev/sda2

    因为设备/dev/sdc和/dev/sdd标识了bcache文件系统,因此会在系统启动时自动添加,而/dev/sda2则需要手动添加。在/dev/sda2偏移1024处仍残留有之前文件系统的超级块信息,而bcache信息是从4096偏移开始记录,修复的方法是:
    # dd if=/dev/zero count=1 bs=1024 seek=1 of=/dev/sda2

    在系统重启之后所有磁盘被正确识别:
    # lsblk -o NAME,MAJ:MIN,RM,SIZE,TYPE,FSTYPE,MOUNTPOINT,UUID,PARTUUID
    
    NAME        MAJ:MIN RM   SIZE TYPE FSTYPE MOUNTPOINT UUID                                 PARTUUID
    sda           8:0    0 111.8G disk
    ├─sda1        8:1    0     3G part vfat   /esp       7E67-C0BB                            d39828e8-4880-4c85-9ec0-4255777aa35b
    └─sda2        8:2    0 108.8G part bcache            93d22899-cd86-4815-b6d0-d72006201e75 baf812f4-9b80-42c4-b7ac-5ed0ed19be65
      ├─bcache0 254:0    0   2.7T disk btrfs             2ff19aaf-852e-4c58-9eee-3daecbc6a5a1
      └─bcache1 254:1    0   2.7T disk btrfs             2ff19aaf-852e-4c58-9eee-3daecbc6a5a1
    sdb           8:16   0 931.5G disk
    └─sdb1        8:17   0 931.5G part ntfs              FAD2B75FD2B71EB7                     90c80e9d-f31a-41b4-9d4d-9b02029402b2
    sdc           8:32   0   2.7T disk bcache            4bd63488-e1d7-4858-8c70-a35a5ba2c452
    └─bcache1   254:1    0   2.7T disk btrfs             2ff19aaf-852e-4c58-9eee-3daecbc6a5a1
    sdd           8:48   0   2.7T disk bcache            ce6de517-7538-45d6-b8c4-8546f13f76c1
    └─bcache0   254:0    0   2.7T disk btrfs             2ff19aaf-852e-4c58-9eee-3daecbc6a5a1
    sde           8:64   1  14.9G disk
    └─sde1        8:65   1  14.9G part ext4   /          d07321b2-b67d-4daf-8022-f3307b605430 5d0a4d76-115f-4081-91ed-fb09aa2318dd

    同样地,残留超级块还会引起类似的其他错误。


    文章来源:http://blog.csdn.net/liumangxiong/article/details/17839797





    Linux内核之bcache简介



    bcache是按照SSD特性来设计的,只按擦除桶大小进行分配,使用btree和日志混合方法来跟踪缓存数据,缓存数据可以是桶上的任意一个扇区。bcache最大程度上减少了随机写的代价,它按顺序填充一个桶,重新使用时只需将桶设置为无效。
    bcache支持写直达和回写策略。回写默认情况下是关闭的,可以在运行时改变。bcache还在最大程度上保护你的数据,在系统异常关机时数据仍然是可靠的。因为它被设计为只有在数据完全写回存储设备才确认写成功。
    回写策略能够缓存绝大多数的写请求,然后再按照索引将脏数据按次序写回到后端存储设备。
    SSD的特点就是随机IO速度很快,而对于大块顺序IO的提升却并不大。bcache会检测顺序IO并忽略;还会对每一个任务记录动态的平均IO大小,当平均IO大小超过截止值时该任务后面的IO将会被忽略,这样就可以透传备份或者大文件拷贝。
    在flash上发现数据IO错误时,首先会尝试读以恢复数据或者将该缓存项置为无效。对于不可恢复的错误,例如元数据或脏数据,bcache将会自动关闭缓存。如果有脏数据在缓存中,这时会首先关闭回写策略然后再等待脏数据刷回。
    从这里开始
    首先需要安装bcache-tools,它提供了make-bcache工具。缓存设备和后端磁盘在使用前都需要先初始化:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. make-bcache -B /dev/sdb  
    2. make-bcache -C /dev/sdc  

    make-bcache提供了同时初始化多个设备的功能,并自动绑定缓存设备和后端磁盘:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. make-bcache -B /dev/sda /dev/sdb -C /dev/sdc  

    bcache-tools现在已经包含了udev规则文件,bcache设备可以立即被内核感知。如果没有udev规则,需要手动注册设备:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. echo /dev/sdb > /sys/fs/bcache/register  
    2. echo /dev/sdc > /sys/fs/bcache/register  

    注册了后端磁盘后,bcache设备会出现在/dev/目录下,现在就可以格式化然后使用了。bcache设备默认是透传模式,因此需要绑定缓存。
    bcache显示如下:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. /dev/bcache<N>  

    还有(有udev规则文件时):
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. /dev/bcache/by-uuid/<uuid>  
    2. /dev/bcache/by-label/<label>  

    如果要开始使用:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. mkfs.ext4 /dev/bcache0  
    2. mount /dev/bcache0 /mnt  

    bcache的sysfs控制项在/sys/block/bcache<N>/bcache。
    bcache设备是按集合来管理的,但目前一个集合只支持一个bcache设备,将来会支持多个设备、元数据和脏数据镜像。新cache设备显示为/sys/fs/bcache/<UUID>

    cache绑定:
    在缓存设备和后端设备都注册之后,还要将缓存设备绑定到后端设备从而使用缓存。绑定操作如下:
    echo <CSET-UUID>  > /sys/block/bcache0/bcache/attach
    这个操作只需要做一次就可以了。下一次系统重启时,只需要重新注册所有bcache设备。如果后端设备还有未定回的缓存数据,那么就不会创建/dev/bcache<N>,直到缓存设备回来,尤其在写回策略时特别重要。
    如果需要在没有缓存设备的时候强制使用设备:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. echo 1 > /sys/block/sdb/bcache/running  

    注意这里参数是后端设备,而不是bcache设备,何况此时bcache设备还没有创建。如果是使用分区创建的bcache设备,例如sdb2对应的目录是/sys/block/sdb/sdb2/bcache。
    在强制使用bcache设备后,缓存设备添加到系统,这个缓存设备的所有缓存数据将会设置为无效。缓存设备的脏数据是不会继续,因为这些脏数据将有可能使现在文件系统崩溃。

    错误处理
    bcache尝试处理IO错误而不影响正常操作,但如果错误数超过阀值(默认是0,可配置)会关闭缓存并切换到透传模式。
    -如果是读错误则尝试直接从后端设备读
    -如果是写直达写错误,将缓存对应数据块设置为无效
    -去绑定时,刷回脏数据。脏数据写回失败目前是没有处理的。

    性能相关问题
    bcache有很多配置选项和可调参数。默认值适合于典型配置,如果想要更好性能则需要调整相关参数。
    -写性能差
    如果写性能不理想,那么建议调到写回策略
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. #echo writeback > /sys/block/bcache0/cache_mode  

    -性能差,或者流量并没有缓存到SSD
    默认情况下,bcache不会缓存顺序IO和大文件。
    打开顺序IO缓存:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. #echo 0 > /sys/block/bcache0/bcache/sequential_cutoff  

    设置回默认值:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. #echo 4M > /sys/block/bcache0/bcache/sequential_cutoff  

    -流量小,缓存不命中
    现实生活中,并不是所有SSD都能提供足够快的速度作为磁盘的缓存,特别一个SSD作为多块磁盘的缓存,或者顺序IO时。所以需要避免SSD成为系统瓶颈。
    为了避免bcache设备因为SSD变慢,当延迟超过阀值时逐渐减少流量。需要关闭拥塞控制项:
    [html]  view plain copy 在CODE上查看代码片 派生到我的代码片
    1. #echo 0 > /sys/fs/bcache/<cache set>/congested_read_threshold_us  
    2. #echo 0 > /sys/fs/bcache/<cache set>/congested_write_threshold_us  

    对于读,默认值是2000us(2ms),对于写是20000.

    SYSFS - 后端设备
    /sys/block/<bdev>/bcache, /sys/block/bcache*/bcache, /sys/fs/bcache/<cset-uuid>/bdev*

    SYSFS - 缓存集合
    /sys/fs/bcache/<cset-uuid>

    SYSFS - 缓存设备
    /sys/block/<cdev>/bcache

    英文:Documentation/bcache.txt

    文章来源:http://blog.csdn.net/liumangxiong/article/details/18090043





    使用 LVM (基于dm-cache) 新的缓存特性


    如果你有一台带有慢速硬盘和快速SSD的电脑,你想使用SSD作为快速持久缓存用来提升访问硬盘的速度。然而直到最近,你有三个选择:bcachedm-cache都upstream,或者Flashcache/EnhanceIO。Flashcache不是upstream。dm-cache要求你首先坐下来,使用计算器计算块的偏移。bcache是三个选择中最全面的。

    但是最近LVM已经增减了缓存的支持(构建在dm-cache之上),因此在理论上,你能让已存在的逻辑卷转换到已缓存的设备。



    安装

    为了在实践中了解是怎样工作的,我在以前的无盘虚拟群集中添加了3块硬盘。

    在镜像配置中有两个2TB的WD硬盘。通过蓝色(冷)线连接。在左侧是三星EVO 250GB SSD,作为缓存的红色(热)盘。

    另一个新闻:哦,现在品牌制造商的SSD是真的便宜!

    lsblk输出如下,sda和sdb是WD硬盘,sdc是三星SSD:

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # lsblkNAME                                     MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINT
    sda                                        8:0    0   1.8T  0 disk  
    └─sda1                                     8:1    0   1.8T  0 part  
       └─md127                                  9:127  0   1.8T  0 raid1 
    sdb                                        8:16   0   1.8T  0 disk  
    └─sdb1                                     8:17   0   1.8T  0 part  
       └─md127                                  9:127  0   1.8T  0 raid1 
    sdc                                        8:32   0 232.9G  0 disk  
    └─sdc1                                     8:33   0 232.9G  0 part


    性能

    在创建 缓存之前,先看看硬盘的速率如何.  以下数据包含了 ext4 和 LVM overhead (ie. 体现在文件系统里,并非块存储). 我还用了 O_DIRECT.

    HDD writes: 114 MBytes/sec 
    HDD reads: 138 MBytes/sec 
    SSD writes: 157 MBytes/sec 
    SSD reads: 197 MBytes/sec

    这些数字并没有体现出SSDs的好处 — 就是随机访问硬盘时,性能没有什么区别.

    Terminology

    lvmcache(7) [没有什么能拷贝的地方] 文档指出了一些用到的术语:

    ?
    1
    2
    3
    4
    5
    origin LV           OriginLV      large slow LV
    cache data LV       CacheDataLV   small fast LV  for  cache pool data
    cache metadata LV   CacheMetaLV   small fast LV  for  cache pool metadata
    cache pool LV       CachePoolLV   CacheDataLV + CacheMetaLV
    cache LV            CacheLV       OriginLV + CachePoolLV


    创建 LVs

    文档中很夸张的提到,移除错误的 LV 将会完全颠覆之前的 OriginLV, 为了测试这一特性我 在有几个镜像文件的慢速HDDs上创建了一个 OriginLV:

    ?
    1
    2
    3
    # lvcreate -L 100G -n testoriginlv vg_guests
       Logical volume  "testoriginlv"  created
    # mkfs -t ext4 /dev/vg_guests/testoriginlv

    需要注意的是resizing cached LVs功能目前并未提供 (可能以后出来 — 现在只能先移除缓存,从新分配大小再重新创建缓存).



    创建缓存层

    从文件的角度来说,这并不明确,但是一切都必须在一个简单的卷组中. 也就是说,你必须创建一个既包括慢速和快速的磁盘卷组 - 它并不是简单工作。

    因此,我的第一个步骤是扩大我现有的VG ,包括快盘:

    ?
    1
    2
    # vgextend vg_guests /dev/sdc1
       Volume group  "vg_guests"  successfully extended

    我创建了快速的SSD两个LVs 。一个是CacheDataLV ,这就是缓存发生。另一个是用于存储被高速缓存在CacheDataLV的数据块的索引的CacheMetaLV 。该文件说, CacheMetaLV应该是千分之一的CacheDataLV的大小,但最少为8MB 。由于我的总可用空间快是232GB ,而我希望有一个1000:1的分裂,我大方的选择一个1GB的CacheMetaLV , 229G的CacheDataLV ,而且会留下一些遗留下来的空间(我最终的分割结果是229:1 ) 。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # lvcreate -L 1G -n lv_cache_meta vg_guests /dev/sdc1
       Logical volume  "lv_cache_meta"  created
    # lvcreate -L 229G -n lv_cache vg_guests /dev/sdc1
       Logical volume  "lv_cache"  created
    # lvs
       LV                     VG        Attr       LSize
       lv_cache               vg_guests -wi-a----- 229.00g
       lv_cache_meta          vg_guests -wi-a-----   1.00g
       testoriginlv           vg_guests -wi-a----- 100.00g
    # pvs
       PV         VG        Fmt  Attr PSize   PFree  
       /dev/md127  vg_guests lvm2 a--    1.82t 932.89g
       /dev/sdc1   vg_guests lvm2 a--  232.88g   2.88g

    (你会发现,我的缓存大于我的测试OriginLV ,但是这很好,因为一旦我已经制定了所有的陷阱,我真正的OriginLV将超过1 TB).


    为什么要在PV上留下2.88GB的空间?我也不太清楚,只是第一次使用时,我并没有预留空间,结果执行 lvconvert命令后 [如下] 提示需要1GB的扩展空间。

    把 CacheDataLV和CacheMetaLV 放到“缓存池”里 :

    ?
    1
    2
    3
    # lvconvert --type cache-pool --poolmetadata vg_guests/lv_cache_meta vg_guests/lv_cache
       Logical volume  "lvol0"  created
       Converted vg_guests /lv_cache  to cache pool.

    接着把缓存池连到OriginLV来创建最终的缓存工程:

    ?
    1
    2
    # lvconvert --type cache --cachepool vg_guests/lv_cache vg_guests/testoriginlv
       vg_guests /testoriginlv  is now cached.



    结果

    看起来还不错? 在使用缓存 LV后,得到的数据如下:

    LV-cache writes: 114 MBytes/sec 
    LV-cache reads: 138 MBytes/sec

    和原来硬盘的结果一致.

    还好没出什么岔子. MikeSnitzer说明了我的dd测试在dm-cache中无效的原因 Mike Snitzer gave me an explanation of why my test usingddisn’t a useful test of dm-cache.

    接下来我会创建一些请求,然后看看他们的性能如何 (就是刚才我提到的问题).



    英文原文:Using LVM’s new cache feature




    文章来源:http://www.oschina.net/translate/using-lvms-new-cache-feature





    展开全文
  • 块设备的处理 一个进程在某个磁盘文件上发出一个 read() 系统调用,内核对进程请求回应的一般步骤: read() 调用一个适当的 VFS 函数,将文件描述符和文件内的偏移量传递给它。 虚拟文件系统位于块设备处理...

    块设备的处理

    在这里插入图片描述

    一个进程在某个磁盘文件上发出一个 read() 系统调用,内核对进程请求回应的一般步骤:

    1. read() 调用一个适当的 VFS 函数,将文件描述符和文件内的偏移量传递给它。
      虚拟文件系统位于块设备处理体系结构的上层,提供一个通用的文件系统模型,Linux 支持的所有系统均采用该模型。
    2. VFS 函数确定所请求的数据是否已经存在,如有必要,它决定如何执行 read 操作。
      有时候没有必要访问磁盘上的数据,因为内核将大多数最近从快速设备读出或写入其中的数据保存在 RAM 中。
    3. 假设内核从块设备读数据,那么它就必须确定数据的物理位置。因此,内核依赖映射层执行下面步骤:
      a. 内核确定该文件所在文件系统的块大小,并根据文件块的大小计算所请求数据的长度。
      本质上,文件被看作拆分成许多块,因此内核确定请求数据所在的块号(文件开始位置的相对索引)。
      b. 映射层调用一个具体文件系统的函数,它访问文件的磁盘节点,然后根据逻辑块号确定所请求数据在磁盘上的位置。
      因为磁盘也被看作拆分成许多块,所以内核必须确定所请求数据的块对应的号。
      由于一个文件可能存储子磁盘上的不连续块中,因此存放在磁盘索引节点中的数据结构将每个文件块号映射为一个逻辑块号。
    4. 现在内核可以对块设备发出读请求。内核利用通用块层启动 I/O 操作来传送所请求的数据。
      一般,每个 I/O 操作只针对磁盘上一组连续操作的块。
      由于请求的数据不必位于相邻的块中,所以通用层可能启动几次 I/O 操作。
      每次 I/O 操作是由一个“块 I/O”结构描述符,它收集底层组件所需要的所有信息以满足所发出的请求。
    5. 通用块层下面的“I/O 调度程序”根据预先定义的内核策略将待处理的 I/O 数据传送请求进行归类。
      调度程序的作用是把物理介质上相邻的数据请求聚集在一起。
    6. 最后,块设备驱动程序向磁盘控制器的硬件接口发出适当的命令,从而进行实际的数据传送。

    块设备中的数据存储涉及了许多内核组件,每个组件采用不同长度的块管理磁盘数据:

    • 硬件块设备控制器:扇区。
    • 虚拟文件系统、映射层和文件系统:块,一个块对应文件系统中的一个最小的磁盘存储单元。
    • 块设备驱动程序:段,一个段就是一个内存页或内存页的一部分,包含磁盘上相邻的数据块。
    • 硬盘高速高速缓存:页,每页正好装在一个页框中。
    • 通用块层将所有的上层和下层的组件组合在一起,了解数据的扇区、块、段和页。
      在这里插入图片描述

    扇区

    块设备的每次数据传输都作用于一组称为扇区的相邻字节。

    大部分磁盘设备中,扇区大小为 512 字节。

    扇区的下标存放在类型为 sector_t 的 32 位或 64 位的变量中。

    扇区是硬件设备传输数据的基本单位,而块是 VFS 和文件系统传输数据的基本单位。

    Linux 中,块大小必须是 2 的幂,且不能超过一个页框。
    此外,它必须是扇区大小的整数倍,因为每个块必须包含整个扇区。

    每个块都需要自己的块缓冲区,它是内核用来存放内容的 RAM 内存区。
    缓冲区的首部是一个与每个缓冲区相关的 buffer_head 类型的描述符。

    buffer_head 中的某些字段:

    • b_page:块缓冲区所在页框的页描述符地址。如果页框位于高端内存中,那么 b_data 字段存放页中块缓冲区的偏移量;
      否则,存放缓冲区本身的起始线性地址。
    • b_blocknr:存放逻辑块号(如磁盘分区中的块索引)。
    • b_bdev:标识使用缓冲区首部的块设备。

    对磁盘的每个 I/O 操作就是在磁盘与一些 RAM 单元间相互传送一些相邻扇区是内容。
    大多数情况下,磁盘控制器之间采用 DMA 方式进行数据传送。
    块设备驱动程序只要向磁盘控制器发送一些适当的命令就可以触发一次数据传送,完成后,控制器会发出一个中断通知块设备驱动程序。

    新的磁盘控制器支持所谓的分散-聚集 DMA 传送方式:磁盘可与一些非连续的内容区相互传送数据。

    启动一次分散-聚集 DMA 传送,块设备驱动程序需要向磁盘控制器发送:

    • 要传送的起始磁盘扇区号和总的扇区数
    • 内存区的描述符链表,其中链表的每项包含一个地址和一个长度

    磁盘控制器负责整个数据传送。

    为了使用分散-聚集 DMA 传送方式,块设备驱动程序必须能处理称为段的数据存储单元。
    一个段就是一个内存页或内存页中的一部分,它们包含一些相邻磁盘扇区中的数据。
    因此,一次分散-聚集 DMA 操作可能同时传送几个段。

    如果不同的段在 RAM 中相应的页框正好是连续的且在磁盘上相应的数据块也是相邻的,那么通用块层可合并它们,产生更大的物理段。

    通用块层

    通用块层是一个内核组件,它处理来自系统中的所有块设备发出的请求。
    由于该层提供的函数,内核可容易地做到:

    • 将数据缓冲区放在高端内存:仅当 CPU 访问时,才将页框映射为内核中的线性地址空间,并在数据访问后取消映射。
    • 通过一些附加的手段,实现一个所谓的“零-复制”模式,将磁盘数据直接存放在用户态地址空间而不是首先复制到内核内存区;事实上,内核为 I/O 数据传送使用的缓冲区所在的页框就映射在进程的用户态线性地址空间中。
    • 管理逻辑卷,例如由 LVM(逻辑卷管理器)和 RAID(廉价磁盘冗余阵列)使用的逻辑卷:几个磁盘分区,即使位于不同的块设备中,也可被看作一个单一的分区。
    • 发挥大部分新磁盘控制器的高级特性,如大主板磁盘高速缓存、增强的 DMA 性能、I/O 传送请求的相关调度等。

    Bio 结构

    bio 描述符,通用块的核心数据结构,描述了块设备的 I/O 操作。
    每个 bio 结构都包含一个磁盘存储区标识符(存储区中的起始扇区号和扇区数目)和一个或多个描述符与 I/O 操作相关的内存区的段。

    bio 中的某些字段:

    • bio_vec:描述 bio 中的每个段。
    • bi_io_vec:bio_vec 数据结构的第一个元素。
    • bi_vcnt:存放了 bio_vec 数组中当前的元素个数。
    • bi_idx:bio_vec 数组中段的当前索引。块 I/O 操作器间 bio 描述符一直保持更新,例如,如果块设备驱动程序在一次分散-聚集 DMA 操作中不能完成全部的数据传送,则 bio 中的 bi_idx 会不断更新来指向待传送的第一个段。为了从索引 bi_idx 指向当前段开始不断重复 bio 中的段,设备驱动程序可以执行 bio_for_each_segment。
    • bi_cnt:bio 的引用计数器值。

    当通用块层启动一次新的 I/O 操作时,调用 bio_alloc() 分配一个新的 bio 结构。
    bio 结构由 slab 分配器分配,内存不足时,内核也会使用一个备用的 bio 小内存池。
    内核也为 bio_vec 分配内存池。

    bip_put() 减少 bi_cnt,等于 0 时,释放 bio 结构及相关的 bio_vec 结构。

    磁盘和磁盘分区表示

    磁盘是一个由通用块层处理的逻辑块设备。
    通常一个磁盘对应一个硬件块设备,如硬盘、软盘或光盘,也可是一个虚拟设备,建立在几个物理磁盘分区上或一些 RAM 专用页中的内存区上。
    任何情形中,借助通用块层提供的服务,上层内核组件可以同样的方式工作在所在的磁盘上。

    磁盘由 gendisk 对象描述,某些字段:

    • flags:存放关于磁盘的信息。如果设置 GENHD_FL_UP 标志,则磁盘将被初始化并可使用。
      如果为软盘或光盘这样的可移动磁盘,则设置 GENHD_FL_REOVABLE 标志。
    • fops:指向表 block_device_operations,该表为块设备的主要操作存放了几个定制的方法。
    • part:磁盘的分区描述符数组。

    通常硬盘被划分成几个逻辑分区。每个块设备文件要么代表整个磁盘,要么代表磁盘中的某个分区。

    如果将一个磁盘分成几个分区,则分区表保存在 hd_struct 结构的数组中。

    alloc_disk():当内核发现系统中一个新的磁盘时(在启动阶段,或将一个可移动介质插入一个驱动器中时,或在运行器附加一个外置式磁盘时),分配并初始化一个新的 gendisk 对象,如果新磁盘被分成几个分区,还会分配并初始化一个适当的 hd_struct 类型的数组。

    add_disk():将新的 gendisk 对象插入到通用块层的数据结构中。

    提交请求

    当向通用块层提交一个 I/O 操作请求时,内核所执行的步骤(假设被请求的数据块在磁盘上相邻,且内核已经知道了它们的物理位置)。

    首先,bio_alloc() 分配一个新的 bio 描述符,然后,内核通过设置一些字段初始化 bio 描述符:

    • bi_sector = 数据的起始扇区号(如果块设备分成了几个分区,那么扇区号是相对于分区的起始位置的)。
    • bi_size = 涵盖整个数据的扇区数目。
    • bi_bdev = 块设备描述符的地址。
    • bi_io_vec = bio_vec 结构数组的起始地址,数组中的每个元素描述了 I/O 操作中的一个段(内存缓存)。
    • bi_vcnt = bio 中总的段数。
    • bi_rw = 被请求操作的标志,READ(0)或 WRITE(1)。
    • bi_end_io = 当 bio 上的 I/O 操作完成时所执行的完成程序的地址。

    bio 描述符被初始化后,内核调用 generic_make_request(),它是通用块层的主要入口点,该函数执行下列操作:

    1. 如果 bio->bi_sector > 块设备的扇区数,bio->bi_flags = BIO_EOF,打印一条内核出错信息,调用 bio_endio() 并终止。
      bio_endio() 更新 bio 描述符中的 bi_size 和 bi_sector,然后调用 bio 的 bi_end_io 方法。
      bi_end_io 函数依赖于触发 I/O 数据传送的内核组件。
    2. 获取与块设备请求相关的请求队列 q,其地址存放在块设备描述符的 bd_disk 字段,其中的每个元素由 bio->bi_bdev 指向。
    3. 调用 block_wait_queue_running() 检查当前正在使用的 I/O 调度程序是否可被动态取代;如果可以,则让当前进程睡眠直到启动一个新的 I/O 调度程序。
    4. 调用 blk_partition_remap() 检查块设备是否指的是一个磁盘分区(bio->bi_bdev != bio->bi_dev->bd_contains)。
      如果是,从 bio->bi_bdev 获取分区的 hd_struct 描述符,从而执行下面的子操作:
      a. 根据数据传送的方向,更新 hd_struct 描述符中的 read_sectors 和 reads 值 或 write_sectors 和 writes 值。
      b. 调整 bio->bi_sector 值,使得把相对于分区的起始扇区号转变为相对于整个磁盘的扇区号。
      c. bio->bi_bedv = 整个磁盘的块设备描述符(bio->bd_contains)。
      从现在开始,通用块层、I/O 调度程序及设备驱动程序将忘记磁盘分区的存在,直接作用于整个磁盘。
    5. 调用 q->make_request_fn 方法将 bio 请求插入请求队列 q 中。
    6. 返回。

    总结:主要是分配并初始化 bio 描述符,以描述符 I/O 操作请求;获取请求队列,将相对于磁盘分区的 I/O 操作请求转换为相对于整个磁盘的 I/O 操作请求;I/O 操作请求入队列。

    I/O 调度程序

    只要可能,内核就试图把几个扇区合并在一起,作为一个整体处理,以减少磁头的平均移动时间。

    当内核组件要读或写一些磁盘数据时,会创建一个块设备请求。
    请求描述的时所请求的扇区及要对它执行的操作类型(读或写)。
    但请求发出后内核不一定会立即满足它,I/O 操作仅仅被调度,执行会向后推迟。
    当请求传送要给新的数据块时,内核检查能否通过稍微扩展前一个一直处于等待状态的请求而满足新的请求。

    延迟请求复杂化了块设备的处理。
    因为块设备驱动程序本身不会阻塞,否则会阻塞试图访问同一磁盘的任何其他进程。

    为防止块设备驱动程序被挂起,每个 I/O 操作都是异步处理的。
    特别是块设备驱动程序是中断驱动的:

    1. 通用块层调用 I/O 调度程序产生一个新的块设备请求,或扩展一个已有的块设备请求,然后终止。
    2. 激活的块设备驱动程序会调用一个策略例程来选择一个待处理的请求,并向磁盘控制器发出一条命令以满足该请求。
    3. 当 I/O 操作终止时,磁盘控制器就产生一个中断,相应的中断处理程序就又调用策略例程去处理队列中的另一个请求。

    每个块设备驱动程序都维持着自己的请求队列,它包含设备待处理的请求链表。
    如果磁盘控制器正在处理几个磁盘,那么通常每个物理块都有一个请求队列。
    在每个请求队列上单独执行 I/O 调度,可提供高盘性能。

    请求队列描述符

    请求队列由一个大的数据结构 request_queue 表示。

    request_queue 中的某些字段:

    • queue_head:请求队列是一个双向链表,其元素是请求描述符(request 数据结构)。queue_head 存放链表的头。
    • queuelist:把任一请求链接到前一个和后一个元素之间。队列链表中元素的排序方式对每个块设备驱动程序是特定的。
    • backing_dev_info:一个 backing_dev_info 类型的小对象,存放了关于基本硬件块设备的 I/O 数据流量的信息。
      如,关于预读及请求队列拥塞状态的信息。

    请求描述符

    每个块设备的待处理请求都是用一个请求描述符表示的,存放于 request 数据结构。

    • bio、biotail:每个请求包含一个或多个 bio 结构。最初,通用层创建一个仅包含一个 bio 结构的请求。
      然后,I/O 调度程序要么向初始 bio 中增加一个新段,要么将另一个 bio 结构链接到请求,从而“扩展”该请求。
      bio 字段指向第一个 bio 结构,biotail 指向最后一个 bio 结构。
    • nr_sectors:整个请求中还需传输的扇区数。
    • current_nr_sectors:存放当前 bio 结构中还需传输的扇区数。
    • flags:存放很多标志,最重要的一个是 REQ_RW,确定数据传送的方向,READ(0)或 WRITE(1)。
    • rl:指向 request_list 结构的指针。

    对请求描述符的分配进行管理

    在重负载和磁盘操作频繁时,固定数目的动态内存将成为进程想把新请求加入请求队列 q 的瓶颈。
    为解决该问题,每个 request_queue 描述符包含一个 request_list 数据结构,其中包括:

    • 一个指针,指向请求描述符的内存池。
    • 两个计数器,分别记录分配给 READ 和 WRITE 请求的请求描述符。
    • 两个标志,分别标记读或写请求的分配是否失败。
    • 两个等队列,分别存放了为获得空闲的读和写请求描述符而睡眠的进程。
    • 一个等待队列,存放等待一个请求队列被刷新(清空)的进程。

    blk_get_request() 从一个特定请求队列的内存池中获得一个空闲的请求描述符;
    如果内存区不足且内存池已经用完,则挂起当前进程,或返回 NULL(不能阻塞内核控制路径)。
    如果分配成功,则将请求队列的 request_list 数据结构的地址存放在请求描述符的 rl 字段。

    blk_put_request() 释放一个请求描述符;如果该描述符的引用计数器为 0,则将描述符归还回它原来所在的内存池。

    避免请求队列拥塞

    request_queue 的 nr_requests 字段存放每个数据传送方向所允许处理的最大请求数。
    缺省情况下,一个队列至多 128 个待处理读请求和 128 个待处理写请求。
    如果待处理的读(写)请求数超过了 nr_requests,设置 request_queue 的 queue_flags 字段的 QUEUE_FLAG_READFULL(QUEUE_FLAG_WRITEFULL)标志将该队列标记为已满,
    试图把请求加入某个传送方向的可阻塞进程被放到 request_list 结构所对应的等待队列中睡眠。

    如果给定传送方向上的待处理请求数超过了 request 的 nr_congestion_on 字段中的值(缺省为 113),则内核认为该队列是拥塞的,并试图降低新请求的创建速率。
    blk_congestion_wait() 挂起当前进程,直到所请求队列都变为不拥塞或超时已到。

    激活块设备驱动程序

    延迟激活块设备驱动程序有利于集中相邻块的请求。
    这种延迟是通过设备插入和设备拔出技术实现的。
    块设备驱动程序被插入时,该驱动程序不被激活,即使在驱动程序队列中有待处理的请求。

    blk_plug_device() 插入一个块设备:插入到某个块设备驱动程序的请求队列中。
    参数为一个请求队列描述符的地址 q。
    设置 q->queue_flags 字段中的 QUEUE_FLAG_PLUGGED 位,然后重启 q->unplub_timer 字段中的内嵌动态定时器。

    blk_remove_plug() 拔出一个请求队列 q:清除 QUEUE_FLAG_PLUGGED 标志并取消 q->unplug_timer 动态定时器。
    当所有可合并的请求都被加入请求队列时,内核就会显式调用该函数。
    此外,如果请求队列中待处理的请求数超过了请求队列描述符的 unplug_thresh 字段中存放的值(缺省为 4),I/O 调度程序也会去掉该请求队列。

    如果一个设备保持插入的时间间隔为 q->unplug_delay(通常为 3ms),则说明 blk_plug_device() 激活的动态定时器时间已用完,因此会执行 blk_unplug_timeout()。
    因而,唤醒内核线程 kblocked 所操作的工作队列 kblocked_workueue。
    kblocked 执行 blk_unplug_work(),其地址存放在 q->unplug_work 中。
    接着,该函数会调用请求队列中的 q->unplug_fn 方法,该方法通常由 generic_unplug_device() 实现。
    generic_unplug_device() 的功能是拔出块设备:

    1. 检查请求队列释放仍然活跃。
    2. 调用 blk_remove_plug()。
    3. 执行策略例程 reuqest_fn 方法开始处理请求队列中的下一个请求。

    I/O 调度算法

    I/O 调度程序也被称为电梯算法。

    Linux 2.6 中提供了四种不同类型的 I/O 调度程序或电梯算法,分别为“预期”算法,“最后期限”算法,“CFQ(完全公平队列)”算法,及“Noop(No Operation)”算法。
    对于大多数块设备,内核使用缺省电梯算法可在引导时通过内核参数 elevator=<name> 进行再设置,其中<name>可取值为:as、deadline、cfg 和 noop。
    缺省为“预期”I/O 调度程序。设备驱动程序也可定制自己的 I/O 调度算法。

    系统管理员可为一个特定的块设备改变 I/O 调度程序。
    如,为了改变第一个 IDE 通道的主磁盘所使用的 I/O 调度程序,管理员可把一个电梯算法的名称写入 sysfs 特殊文件系统的 /sys/block/hada/queue/scheduler 文件中。

    请求队列中使用的 I/O 调度算法由一个 elevator_t 类型的 elevator 对象表示,该对象的地址存放在请求队列描述符的 elevator 字段。
    elevator 对象包含了几个方法:链接和断开 elevator,增加和合并队列中的请求,从队列中删除请求,获得队列中下一个待处理的请求等。
    elevator 也存放了一个表的地址,表中包含了处理请求队列所需的所有信息。
    每个请求描述符包含一个 elevator_private 字段,指向一个由 I/O 调度程序用来处理请求的附加数据结构。

    一般,所有的算法都使用一个调度队列,队列中包含的所有请求按照设备驱动程序应当处理的顺序排序。
    几乎所有的算法都使用另外的队列对请求进行分类和排序。
    它们允许设备驱动将 bio 结构增加到已存放请求中,还可合并两个“相邻的”请求。

    “Noop”算法

    最简单的 I/O 调度算法。没有排序的队列。

    “CFQ”算法

    目标是在触发 I/O 请求的所有进程中确保磁盘 I/O 带宽的公平分配。
    为此,算法使用多个排序队列(缺省为 64)存放不同进程发出的请求。
    当处理一个请求时,内核调用一个散列函数将当前进程的线程组标识符换为队列的索引值,然后将一个新的请求插入该队列的末尾。

    算法采用轮询方式扫描 I/O 输入队列,选择第一个非空队列,然后将该队列中的一组请求移动到调度队列的末尾。

    “最后期限”算法

    除了调度队列外,还使用了四个队列。
    其中的两个排序队列分别包含读请求和写请求,请求根据起始扇区数排序。
    另外两个最后期限队列包含相同的读和写请求,但根据“最后期限”排序。
    引入这些队列是为了避免请求饿死。

    补充调度队列:

    1. 首先确定下一个请求的数据方向。
      如果同时要调度读和写两请求,算法会选择“读”方向,除非“写”方向已经被放弃很多次了。
    2. 检查与被选择方向相关的最后期限队列:如果队列中的第一个请求的最后期限已用完,那么将该请求移到调度队列的末尾;也可从超时的那个请求开始移动来自排序队列的一组请求。
      如果将要移动的请求在磁盘上物理相邻,则组的长度会变长,否则变短。
    3. 如果没有请求超时,算法对来自排序队列的最后一个请求之后的一组请求进行调度。
      当指针到达排序队列的末尾时,搜索又从头开始(“单方向算法”)。

    “预期”算法

    是 Linux 提供的最复杂的一种 I/O 调度算法。
    它是“最后期限”算法的一个演变:两个最后期限队列和两个排序队列;I/O 调度程序在读和写请求之间交互扫描排序队列,不过更倾向于读请求。
    扫描基本上是连续的,除非某个请求超时。读请求的缺省超时时间是 125ms,写请求为 250ms。
    算法还遵循一些附加的启发式规则:

    • 有些情况下,算法可能在排序队列当前位置之后选择一个请求,从而强制磁头从后搜索。
      这通常发生在该请求之后的搜索距离小于在排序队列当前位置之后对该请求搜索距离的一半时。
    • 算法统计系统中每个进程触发的 I/O 操作种类。
      当刚刚调度了由某个进程 p 发出的一个读请求后,立马检查排序队列中下一个请求是否来自同一进程 p。
      如果是,立即调度下一请求。否则,查看关于该进程 p 的统计信息:如果确定 p 可能很快发出另一个读请求,则延迟一小段时间(缺省约 7ms)。
      因此,算法预测进程 p 发出的读请求与刚被调度的请求在磁盘上可能是“近邻”。

    向 I/O 调度程序发出请求

    generic_make_request() 调用请求队列描述符的 make_request_fn 方法向 I/O 调度程序发送一个请求。
    通常该方法由 __make_request() 实现,__make_request() 参数为 request_queue 类型的描述符 q、bio 结构的描述符 bio。执行下列操作:

    1. 如果需要,调用 blk_queue_bounce() 建立一个回弹缓冲区。然后,对该缓冲区而不是原先的 bio 结构进行操作。
    2. 调用 I/O 调度程序的 elv_queue_empty() 检查请求队列中是否存在待处理请求。调度队列可能是空的,但 I/O 调度程序的其他队列可能包含待处理请求。如果没有,调用 blk_plug_device() 插入请求队列,然后跳到第 5 步。
    3. 插入的请求队列包含待处理请求。
      调用 I/O 调度程序的 elv_merge() 检查新的 bio 结构是否可以并入已存在的请求中,将返回三个可能值:
    • ELEVATOR_NO_MERGE:已经存放在的请求中不能包含 bio 结构,跳到第 5 步。
    • ELEVATOR_BACK_MERGE:bio 结构可作为末尾的 bio 而插入到某个请求 req 中,调用 q->back_merge_fn 方法检查是否可扩展该请求。
      如果不行,跳到第 5 步;否则,将 bio 描述符插入 req 链表的末尾并更新 req 的相应字段值。
      然后,函数试图将该请求与后面的请求合并。
    • ELEVATOR_FRONT_MERGE:bio 结构可作为某个请求 req 的第一个 bio 被插入,函数调用 q->front_merge_fn 方法检查是否可扩展该请求。
      如果不行跳到第 5 步;否则,将 bio 描述符插入 req 链表的首部并更新 req 的相应字段值。
      然后,试图将该请求与前面的请求合并。
    1. bio 已经被并入存放在的请求中,跳到第 7 步终止函数。
    2. bio 必须被插入一个新的请求中。分配一个新的请求描述符。
      如果没有空闲的内存,那么挂起当前进程,直到设置了 bio->bi_rw 中的 BIO_RW_AHEAD 标志,表明这个 I/O 操作是一次预读;这种情形下,函数调用 bio_endio() 并终止:不执行数据传输。
    3. 初始化请求描述符中的字段,主要有:
      a. 根据 bio 描述符的内容初始化各个字段,包括扇区数、当前 bio 及当前段。
      b. 设置 flags 字段中的 REQ_CMD 标志。
      c. 如果第一个 bio 段的页框存放在低端内存,则将 buffer 字段设置为缓冲区的线性地址。
      d. rq_disk = bio->bi_bdev->bd_disk 的地址。
      e. 将 bio 插入请求链表。
      f. start_time = jiffies 值。
    4. 所有操作都完成。终止前,检查是否设置了 bio->bi_rw 中的 BIO_RW_SYNC 标志,如果是,对请求队列调用 generic_unplug_device() 卸载设备驱动程序。
    5. 函数终止。

    总结:根据请求队列是否为空,不空时是否与已有请求合并,来确定 bio 与现有请求合并还是新分配、初始化一个新的 bio 描述符,并插入请求链表。然后根据需要卸载驱动程序,函数终止。

    blk_queue_bounce()

    功能是查看 q->bounce_gfp 中的标志及 q->bounce_pfn 中的阈值,从而确定回弹缓冲区是否必须。
    通常当请求中的一些缓冲区位于高端内存,而硬件设备不能访问它们时发生该情况。

    当处理老式设备时,块设备驱动程序通常更倾向于直接在 ZONE_DMA 内存区分配 DMA 缓冲区。

    如果硬件设备不能处理高端内存中的缓冲区,则 blk_queue_bounce() 检查 bio 中的一些缓冲区是否真的必须是回弹的。
    如果是,则将 bio 描述符复制一份,接着创建一个回弹 bio;当段中的页框号等于或大于 q->bounce_pfn 时,执行下列操作:

    1. 根据分配的标志,在 ZONE_NORMAL 或 ZNOE_DMA 内存区中分配一个页框。
    2. 更新回弹 bio 中段的 bv_page 字段,使其指向新页框的描述符。
    3. 如果 bio->bio_rw 代表一个写操作,则调用 kmap() 临时将高端内存页映射到内核地址空间中,然后将高端内存页复制到低端内存页上,最后调用 kunmap() 释放该映射。

    然后 blk_queue_bounce() 设置回弹 bio 中的 BIO_BOUNCED 标志,为其初始化一个特定的 bi_end_io 方法,最后它将存放在 bio 的 bi_private 字段中,该字段指向初始 bio 的指针。
    当回弹 bio 上的 I/O 数据传送终止时,bi_end_io 方法将数据复制到高端内存区中(仅适合读操作),并释放该回弹 bio 结构。

    块设备驱动程序

    块设备驱动程序是 Linux 块子系统中最底层组件。
    它们从 I/O 调度程序获得请求,然后按要求处理这些请求。

    块设备

    一个块设备驱动程序可能处理几个块设备。
    每个块设备由一个 block_device 结构描述符表示。

    block_device 的某些字段:

    • bd_list:所有块设备的描述符被插入一个全局链表中,链表首部由变量 all_bdevs 表示;
      链表链接所用的指针位于块设备描述符的 bd_list 字段。
    • bd_contains、bd_part、bd_part_count:如果块设备描述符对应一个磁盘分区,则 bd_contains 指向与整个磁盘相关的块设备描述符;bd_part 指向 hd_struct 分区描述符。
      否则,bd_contains 指向块设备描述符本身,bd_part_count 记录磁盘上的分区已经被打开了多少次。
    • bd_holder:代表块设备持有者的线性地址。
      持有者不是进行 I/O 数据传送的块设备驱动程序,而是一个内核组件,典型为安装在该设备上的文件系统。
      当块设备文件被打开进行互斥访问时,持有者就是对应的文件对象。
    • bd_holders:bd_claim() 将 bd_holder 设置为一个特定的地址;bd_release() 将该字段重新设置为 NULL。
      同一内核组件可多次调用 bd_claim(),每次调用都增加 bd_holders 值;
      为释放块设备,内核组件必须调用 bd_release() bd_holders 次。

    在这里插入图片描述

    访问块设备

    当内核接收一个打开块设备文件的请求时,必须先确定该设备文件是否已经是打开的。
    如果是,则内核没必要创建并初始化一个新的块设备描述符,而是更新已存在的块设备描述符。
    然而,真正的复杂性在于具有相同主设备号和次设备号但不同路径名的块设备被 VFS 看作不同的文件。
    因此,内核无法通过简单地在一个对象的索引节点高速缓存中检查块设备文件的存在就确定相应的块设备已经在使用。

    主、次设备号和相应的块设备描述符之间的关系是通过 bdev 特殊文件系统来维护的。
    每个块设备描述符都对应一个 bdev 特殊文件:
    块设备描述符的 bd_inode 字段指向相应的 bdev 索引节点;
    而该索引节点将为块设备的主、次设备号和相应描述符的地址进行编码。

    bdget() 参数为块设备的主设备号和次设备号,在 bdev 文件系统中查询相关的索引节点;
    如果不存在这样的节点,则分配一个新索引节点和新块设备描述符。
    返回一个与给定主、次设备号对应的块设备描述符的地址。

    找到块设备描述符后,内核通过检查 bd_openers 字段来确定块设备当前是否在使用:
    如果为正值,则块设备已经在使用(可能通过不同的设备文件)。
    同时,内核也维护一个与已打开的块设备文件对应的索引节点对象的链表。
    该链表存放在块设备描述符的 bd_inodes 字段;
    索引节点对象的 i_devices 字段存放于链接链表中的前后元素的指针。

    注册和初始化设备驱动程序

    定义驱动程序描述符

    首先,设备驱动程序需要一个 foo_dev_t 类型的自定义描述符 foo,它拥有驱动硬件设备所需的数据。
    该描述符存放每个设备的相关信息,如操作设备使用的 I/O 端口、设备发出中断的 IRQ 线、设备的内部状态等。
    同时也包含块 I/O 子系统所需的一些字段:

    struct foo_dev_t
    {
    	[...]
    	spinlock_t lock;  // 保护 foo 描述符中字段值的自旋锁
    	struct gendisk *gd;  // 指向 gendisk 描述符的指针,该描述符描述由该驱动程序处理的整个块设备
    	[...]
    };
    

    预定主设备号

    驱动程序通过 register_blkdev() 预定一个主设备号:

    // 预定主设备号 FOO_MAJOR 并将设备名称 foo 赋给它
    // 预定的主设备号和驱动程序之间的数据结构还没有建立连接
    // 结果为产生一个新条目,该条目位于 /proc/devices 特殊文件的已注册设备号列表中
    err = register_blkdev(FOO_MAJOR, "foo");  
    if(err)
    	goto error_major_is_busy;
    

    初始化自定义描述符

    为初始化于块 I/O 子系统相关的字段,设备驱动程序主要执行下列操作:

    spin_lock_init(&foo.lock); // 初始化自旋锁
    
    // 分配一个磁盘描述符,也分配一个存放磁盘分区描述符的数组
    // 16 表示驱动程序可支持 16 个磁盘,每个磁盘可包含 15 个分区(0 分区不使用)
    foo.gd = alloc_disk(16);   
    
    if(!foo.gd)
    	goto error_no_gendisk;
    

    初始化 gendisk 描述符

    接下来,驱动程序初始化 gendisk 描述符的一些字段:

    // foo 描述符的地址存放在 gendisk 的 private_data 字段
    // 因此被块 I/O 子系统当作方法调用的低级驱动程序函数可迅速查找到驱动程序描述符
    // 如果驱动程序可并发地处理多个磁盘,可提高效率
    foo.gd->private_data = &foo; 
    
    foo.gd->major = FOO_MAJOR;
    foo.gd->first_minor = 0;
    foo.gd->minors = 16;
    
    // 将 capacity 字段初始化为以 512 字节扇区为单位的磁盘大小,该值也可能在探测硬件并询问磁盘参数时确定。
    set_capacity(foo.gd, foo_disk_capacity_in_sectors);
    
    strcpy(foo.gd->disk_name, "foo");
    foo.gd->fops = &foo_ops;
    

    初始化块设备操作表

    gendisk 描述符的 fops 字段步初始化为自定义的块设备方法表的地址。
    类似地,设备驱动程序的 foo_ops 表中包含设备驱动程序的特有函数。
    如,如果硬件设备支持可移动磁盘,通用块将调用 media_changed 方法检测自从最后一次安装或打开该设备以来,磁盘是否被更换。
    通常通过硬件控制器发送一些低级命令完成该检查,因此,每个设备驱动程序所实现的 media_changed 方法都不同。

    类似地,仅当通用块层不知道如何处理 ioctl 命令时才调用 ioctl 方法。
    如,当一个 ioctl() 询问磁盘构造时,即磁盘使用的柱面数、磁道数、扇区数即磁头数时,通常用该方法。
    因此,每个设备驱动程序所实现的 ioctl 方法也都不同。

    分配和初始化请求队列

    // 分配一个请求队列描述符,并将其中许多字段初始化为缺省值
    // 参数为设备描述符的自旋锁的地址(foo.gd->rq->queue_lock)
    // 和设备驱动程序的策略例程的地址(foo.gd->rq->request_fn)
    // 也初始化 foo.gd->rq->elevator 字段为缺省的 I/O 调度算法
    foo.gd->rq = blk_init_queue(foo_strategy, &foo.lock); 
    if(!foo.gd->rq)
    	goto error_no_request_queue;
    
    // 使用几个辅助函数将请求队列描述符的不同字段设为设备驱动程序的特征值
    blk_queue_hardsect_size(foo.gd->rd, foo_hard_sector_size);
    blk_queue_max_sectors(foo.gd->rd, foo_max_sectors);
    blk_queue_max_hw_segments(foo.gd->rd, foo_max_hw_segments);
    blk_queue_max_phys_segments(foo.gd->rd, foo_max_phys_segments);
    

    设置中断处理程序

    设备驱动程序为设备注册 IRQ 线:

    // foo_interrupt() 是设备的中断处理程序
    request_irq(foo_irq, foo_interrupt, SA_INTERRUPT | SA_INTERRUPT | SA_SHIRQ, "foo", NULL);
    

    注册磁盘

    最后一步是“注册”和激活磁盘,可简单地通过执行下面的操作完成:

    // 参数为 gendisk 描述符的地址
    add_disk(foo.gd);
    

    add_disk() 执行下面步骤:

    1. 设置 gd->flags 的 GENHD_FL_UP 标志。
    2. 调用 kobj_map() 建立设备驱动程序和设备的主设备号(连同相关范围内的次设备号)之间的连接。
    3. 注册设备驱动程序模型的 gendisk 描述符的 kobject 结构,它作为设备驱动程序处理的一个新设备(如 /sys/block/foo)。
    4. 如果需要,扫描磁盘中的分区表;对于查找到的每个分区,适当地初始化 foo.gd->part 数组中相应的 hd_struct 描述符。
      同时注册设备驱动程序模型中的分区(如 /sys/block/foo/foo1)。
    5. 注册设备驱动程序模型的请求队列描述符中内嵌的 kobject 结构(如 /sys/block/foo/queue)。

    一旦 add_disk() 返回,设备驱动程序就可以工作了。
    进程初始化的函数终止;策略例程和中断处理程序开始处理 I/O 调度程序传送给设备驱动程序的每个请求。

    策略例程

    策略例程是块设备驱动程序的一个函数或一组函数,它与硬件块设备之间相互作用以满足调度队列中的请求。
    通过请求队列描述符中的 request_fn 方法可调用策略例程,如 foo_strategy(),I/O 调度程序层将请求队列描述符 q 的地址传给该函数。

    把新的请求插入空的请求队列后,策略例程通常才被启动。
    只要块设备驱动程序被激活,就应该对队列中的所有请求进行处理,直到队列为空才结束。

    块设备驱动程序采用如下策略:

    • 策略例程处理队列中的第一个请求并设置块设备控制器,以便在数据传送完成时产生一个中断。然后策略例程终止。
    • 当磁盘控制器产生中断时,中断控制器重新调度策略例程。
      策略例程要么为当前请求再启动一次数据传送,要么当请求的所有数据块已经传送完成时,把该请求从调度队列中删除然后开始处理下一个请求。

    请求是由几个 bio 结构组成的,而每个 bio 结构又由几个段组成。
    基本上,块设备驱动程序以以下方式使用 DMA:

    • 驱动程序建立不同的 DMA 传送方式,为请求的每个 bio 结构的每个段进行服务。
    • 驱动程序建立以一种单独的分散-聚集 DMA 传送方式,为请求的所有 bio 中的所有段服务。

    设备驱动程序策略例程的设计依赖块控制器的特性。

    如,foo_strategy() 策略例程执行下列操作:

    1. 通过调用 I/O 调度程序的辅助函数 elv_next_request() 从调度队列中获取当前的请求。如果调度队列为空,就结束这个策略例程:
    req = elv_next_request(q);
    if(!req) 
    	return;
    
    1. 执行 blk_fs_request 宏检测是否设置了请求的 REQ_CMD 标志,即请求是否包含一个标准的读或写操作:
    if(!blk_fs_request(req))
    	goto handle_special_request;
    
    1. 如果块设备控制器支持分散-聚集 DMA,那么对磁盘控制器进行编程,以便为整个请求执行数据传送并再传送完成时产生一个中断。
      blk_rq_map_sg() 辅助函数返回一个可以立即被用来启动数据传送的分散-聚集链表。
    2. 否则,设备驱动程序必须一段一段地传送数据。
      这种情形下,策略例程执行 rq_for_each_bio 和 bio_for_each_segment 两个宏,分别遍历 bio 链表和每个 bio 中的链表:
    rq_for_each_bio(bio, rq)
    	bio_for_each_segment(bvec, bio, i)
    	{
    		local_irq_save(flags);
    
    		// 如果要传送的数据位于高端内存,kmap_atomic() 和 kunmap_atomic() 是必需的
    		addr = kmap_atomic(bvec->bv_page, KM_BIO_SRC_IRQ);
    
    		// 对硬件设备进行编程,以便启动 DMA 数据传送并在 I/O 操作完成时产生一个中断
    		foo_start_dma_transfer(addr+bvec->bv_offset, bvec->bv_len);
    
    		kunmap_atomic(bvec->bv_page, KM_BIO_SRC_IRQ);
    		local_irq_restore(flags);
    	}
    
    1. 返回。

    中断处理程序

    块设备驱动程序的中断处理程序在 DMA 数据传送结束时被激活。
    它检查是否已经传送完成请求的所有数据块,如果是,中断处理程序就调用策略例程处理调度队列中的下一个请求;
    否则,中断处理程序更新请求描述符的相应字段并调用策略例程处理还没有完成的数据传送。

    设备驱动程序 foo 的中断处理程序的一个典型片段如下:

    irqreturn_t foo_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    {
    	struct foo_dev_t *p = (struct foo_dev_t *)dev_id;
    	struct request_queue *rq = p->gd->rq;
    	[...]
    	if(!end_that_request_first(rq, uptodata, nr_seectors))
    	{
    		blkdev_dequeue_request(rq);
    		end_that_request_last(rq);
    	}
    	rq->request_fn(rq);
    	[...]
    	return IRQ_HANDLED;
    }
    

    end_that_request_first() 和 end_that_request_last() 共同承担结束一个请求的任务。

    end_that_request_first() 接收的参数:

    • 一个请求描述符
    • 一个指示 DMA 数据传送完成的标志
    • DMA 所传送的扇区数

    end_that_request_first() 扫描请求中的 bio 结构及每个 bio 中的段,然后采用如下方式更新请求描述符的字段值:

    • 修改 bio 字段,使其执行请求中的第一个未完成的 bio 结构。
    • 修改未完成 bio 结构的 bi_idx 字段,使其指向第一个未完成的段。
    • 修改未完成的 bv_offset 和 bv_len 字段,使其指定仍需传送的数据。

    end_that_request_first() 的返回值、中断处理程序相应的处理:

    • 0,已经完成请求中的所有数据块。
      中断处理程序把请求从请求队列中删除(主要由 blkdev_dequeue_request() 完成),然后调用 end_that_request_last(),并再次调用策略例程处理调度队列中的下一个请求。
    • 1,中断处理程序重新调用策略例程,继续处理该请求。

    end_that_request_last() 功能:
    更新一些磁盘使用统计数,把请求描述符从 I/O 调度程序 rq->elevator 的调度队列中删除,唤醒等待请求描述符完成的任一睡眠进程,并释放删除的那个描述符。

    打开块设备文件

    内核打开一个块设备文件的时机:

    • 一个文件系统被映射到磁盘或分区上时
    • 激活一个交换分区时
    • 用户态进程向块设备文件发出一个 open() 系统调用时

    在所有情况下,内核本质上执行相同的操作:
    寻找块设备描述符(如果块设备没有在使用,则分配一个新的描述符),为即将开始的数据传送设置文件操作方法。

    仅考虑 open 方法,它由 dentry_open() 调用。
    blkdev_open() 参数为 inode 和 filp,分别为索引节点和文件对象的地址,本质上执行下列操作:

    1. 执行 bd_acquire(inode) 从而获得块设备描述符 bdev 的地址。该函数参数为索引节点对象的地址,执行下列主要步骤:
      a. 如果索引节点对象的 inode->i_bdev 字段不为 NULL,表明块设备文件已经打开,该字段存放了相应块描述符的地址。
      增加与块设备相关联的 bdev 特殊文件系统的 inode->i_bdev->bd_inode 索引节点的引用计数器值,并返回描述符 inode->i_bdev 的地址。
      b. 否则,块设备文件没有被打开。
      根据块设备相关联的主设备号和次设备号,执行 bdget(inode->i_rdev) 获取块设备描述符的地址。
      如果描述符不存在,bdget() 就分配一个。
      c. inode->i_bdev = 块设备描述符的地址,以便加速将来对相同块设备文件的打开操作。
      d. inode->i_mapping = bdev 索引节点中相应字段的值。inode->i_mapping 指向地址空间对象。
      e. 把索引节点插入到 bdev->bd_inodes 确立的块设备描述符的已打开索引节点链表中。
      f. 返回描述符 bdev 的地址。
    2. filp->i_mapping = inode->i_mapping
    3. 获取与这个块设备相关的 gendisk 描述符的地址
    // 在 kobject 映射域 bdev_map 上简单地调用 kobj_lookup() 传递设备的主设备号和次设备号
    // 如果被打开的块设备是一个分区,则返回的索引值存放在本地变量 part 中;否则,part 为 0
    disk = get_gendisk(bdev->bd_dev, &part);  
    
    1. 如果 bdev->bd_openers != 0,说明块设备已经被打开。检查 bdev->bd_contains 字段:
      a. 如果等于 bdev,那么块设备是一个整盘:调用块设备方法 bdev->bd_disk->fops->open(如果定义了),然后检查 bdev->bd_invalidated 的值,需要时调用 rescan_partitions()。
      b. 如果不等于 bdev,那么块设备是一个分区:bdev->bd_contains->bd_part_count++,跳到第 8 步。
    2. 这里的块设备是第一次被访问。初始化 bdev->bd_disk 为 gendisk 描述符的地址 disk。
    3. 如果块设备是一个整盘(part == 0),则执行下列子步骤:
      a. 如果定义了 disk->fops->open 块设备方法,就执行它:
      该方法由块设备驱动程序定义的定制函数,它执行任何特定的最后一分钟初始化。
      b. 从 disk->queue 请求队列的 hardsect_size 字段中获取扇区大小(字节数),用该值适当地设置 bdev->bd_block_size 和 bdev->bd_inode->i_blkbits。
      同时从 disk->capacity 中计算来的磁盘大小设置 bdev->bd_inode->i_size 字段。
      c. 如果设置了 bdev->bd_invalidated 标志,则调用 rescan_partitions() 扫描分区表并更新分区描述符。
      该标志是由 check_disk_change 块设备方法设置的,仅适用于可移动设备。
    4. 否则,如果块设备是一个分区,则执行下列子步骤:
      a. 再次调用 bdget(),这次是传递 disk->first_minor 次设备号,获取整盘的块描述符地址 whole。
      b. 对整盘的块设备描述符重复第 3 步 ~ 第 6 步,如果需要则初始化该描述符。
      c. bdev->bd_contains = 整盘描述符的地址。
      d. whole->bd_part_count++,从而说明磁盘分区上新的打开操作。
      e. bdev->bd_part = disk->part[part-1], disk->part[part-1] 是分区描述符 hd_struct 的地址。
      同样,执行 kobject_get(&bdev->bd_part->kobj) 增加分区引用计数器的值。
      f. 与第 6b 步中一样,设置索引节点中表示分区大小和扇区大小的字段。
    5. bdev->bd_openers++
    6. 如果块设备文件以独占方式被打开(设置了 filp->f_flags 中的 O_EXCL 标志),则调用 bd_claim(bdev, filp) 设置块设备的持有者。
      如果块设备已经有一个持有者,则释放该块设备描述符并返回要给错误码 -EBUSY。
    7. 返回 0(成功)终止。
    展开全文
  • 计算机网络——网络硬件和网络设备及其工作原理

    万次阅读 多人点赞 2018-10-09 01:26:36
    计算机网络——网络硬件和网络设备及其工作原理 常见的网络硬件有网卡、中继站、集线器、桥连接器、交换机、路由器。 一. 网卡: 网卡是工作在链路层的网络组件,是局域网中连接计算机和传输介质的接口,不仅能实现...
  • iostat 命令是 I/O statistics(输入/输出统计)的缩写,用来报告系统的 CPU 统计信息和块设备及其分区的 IO 统计信息。iostat 是 sysstat 工具集的一个工具,在 Ubuntu 系统中默认是不带 iostat 命令的,需要自行安装...
  • 闪存类块设备:使用闪存作为存储介质,里面的控制器运行固化的驱动程序,驱动 程序的功能之一是闪存转换层(Flash Translation Layer,FTL),把闪存转换为块设备, 外表现为块设备。常见的闪存类块设备是在个人...
  • 设备树学习(二、设备树dts/dtsi格式)

    万次阅读 多人点赞 2019-02-14 22:17:00
    说明:后续的博文参考自韦东山老师的设备树视屏,老师用的是2440的开发板,我用的是s5pv210的开发板。原理一样 一、前言 简单的说,如果要使用Device Tree,首先用户要了解自己的硬件配置和系统运行参数,并把...
  • 截断编码图像压缩技术

    千次阅读 2020-10-04 03:20:19
    论文先介绍了当前流行的图像压缩技术,重点介绍截断编码技术,先从理论上介绍截断编码原理,截断编码是一种有效、快速的数字图像压缩技术,作为一种经典的图像压缩编码,截断编码技术的实时性很强。...
  • 1. 云基础设施机制包括哪些主要构件?简要说明这些构件的概念。 逻辑网络边界:将一个网络环境与通信网络的其他部分分割开来,形成一个虚拟网络边界,包含并隔离了一组关于云的IT资源,且这些资源可能是分布式的。 ...
  • 它是在文件系统与块设备(例如:磁盘驱动器)之间。 2.文件级概念: 文件级是指文件系统,单个文件可能由于一个或多个逻辑块组成,且逻辑块之间是不连续分布。逻辑块大于或等于物理块整数倍, 3.物理块与文件系统...
  • 在园区部署数据存储云(必要时建立异地灾备中心),以数据或文件的形式通过在线或离线手段存储企业的各种加密或解密的业务数据,并建立数据回溯机制,可以规避如下事故导致的企业数据丢失或泄密风险:存储设备毁坏...
  • ARM-Linux驱动程序研究

    2018-04-16 17:38:27
    本文主要目的是研究Linux下的驱动程序移植。文章首先分析如何建立硬件和软件 平台,包括开发板介绍、建立交叉编译环境、BootLoader移植、Linux2.6内核...然后介绍典型的字符设备、块设备和网络设备驱动程序的写法。
  • 写一个linux设备驱动

    万次阅读 2012-04-26 11:17:25
    ----------------------- Page 1----------------------- 第 1章 +---------------------------------------------------+  | 写一个块设备驱动 |  +-----------------------------
  • 架构设计:系统存储(1)——存储方案(1)

    万次阅读 多人点赞 2016-08-18 20:19:21
    存储的知识中我们还将介绍磁盘阵列技术,包括磁盘阵列的组织方式和设备类型。最后存储知识中我们介绍操作系统中的文件系统,包括EXT系列文件系统和XFS文件系统(会顺带提到Windows操作系统使用的NTFS文件系统)...
  • 存储类型: https://www.bilibili.com/video/BV1WE41177Kp?from=search&seid=1887114199876544924 存储分类2: 存储: 文件存储: 对象存储:
  • 《 人机交互技术》第三章 交互设备

    千次阅读 2018-06-18 13:05:23
    第三章交互设备(输入、输出、虚拟现实中交互设备) 1.输入设备(文本输入、图像输入、三维信息输入、指点输入)   文本输入(键盘、手写设备、语音输入设备)(第一版P33)  文本输入是人与计算机交互的一个...
  • 计算机存储的发展(存储,文件存储,对象存储)

    万次阅读 多人点赞 2018-09-15 15:04:08
    2、对象存储设备 3、元数据服务器(Metadata Server,MDS) 4、对象存储系统的客户端Client 三者之间异同比较 参考文献 如果要实现一个计算机,那么这个计算机一定要有以下的三个部分构成:计算、存储和网络...
  • 传统磁带库不是块设备,有余机械的限制,相对于磁盘文件系统,也具备磁带文件系统,或者称磁带数据管理系统; 块级别备份可以看成是对底层lun的备份,不管lun上是否存在数据、或者原始有效容量;抛开了文件系统,...
  • 超详细|一篇搞定操作系统——设备管理

    万次阅读 多人点赞 2021-01-05 21:27:11
    主要功能包括:缓冲管理、设备分配与回收、设备处理和虚拟设备。 除了进行实际I/O操作的设备外,也包括设备控制器、DMA控制器、中断控制器、通道。 早期,计算机设计者没有将CPU的执行与I/O操作分开,甚至...
  • 超硬核!操作系统学霸笔记,考试复习面试全靠它

    万次阅读 多人点赞 2021-03-22 18:43:49
    再释放PCB 进程管理中的数据结构 操作系统中用于管理控制的数据结构:内存表,设备表,文件表,进程表(程序控制快PCB) 进程控制PCB的作用: 1)作为独立运行基本单位的标志 2)能实现间断性的运行方式 3)提供...
  • MAX485典型电路

    千次阅读 2020-12-29 03:00:02
    《MAX485典型电路》由会员分享,可在线阅读,更多相关《MAX485典型电路(60页珍藏版)》请在人人文库网上搜索。1、第9章 串行接口技术,单片机原理、接口及应用,内 容 提 要, RS-485总线扩展 IIC总线扩展接口及应用 SPI...
  • 局域网的组成及主要设备的作用

    千次阅读 2021-05-07 10:41:42
    局域网由网络硬件(包括网络服务器,网络工作站,网卡,网络互联设备等)和网络传输介质,以及网络软件所组成。 网络设备:即网络通信设备,它主要指为保证网络中的计算机互联、能稳定可靠地传输信息、实现底层网络...
  • 本文介绍AI模型适用于小型本地设备上的方法技术:压缩模型参数量,设计更小的模型结构,知识蒸馏,调整数据格式,数据复用等,并介绍移动小处理设备的类型、适用移动设备的模型框架等。...............
  • 块设备驱动程序

    千次阅读 2012-07-20 10:18:30
    第1章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊
  • 存储和文件存储是我们比较熟悉的两种主流的存储类型,而对象存储(Object-based Storage)是一种新的网络存储架构,基于对象存储技术的设备就是对象存储设备(Object-based Storage Device)简称OSD。 首先,...
  • Linux和windows访问设备的方式比较

    千次阅读 2018-07-12 16:12:35
    LInux一直秉承着一切皆文件的理念,但是如何把设备当做文件来处理呢?Windows又是如何处理设备的呢?参照前辈的译文 &nbsp; &nbsp; &nbsp; 毕业后一直在学操作系统, 有时候觉得什么都懂了,...
  • Zynq SOC学习笔记之设备

    千次阅读 2020-05-10 18:58:34
    以树状节点的方式描述一个设备的各种硬件信息细节:CPU、GPIO、时钟、中断、内存等,形成类似文本文件dts,直接透过它传递给Linux,使得驱动程序与硬件分离,只需要修改dts文件,便能实现需求。设备树易于扩展,硬件...
  • 查看设备挂载信息

    万次阅读 2017-09-19 16:48:46
    系统 # uname -a # 查看内核/操作系统/CPU信息  # head -n 1 /etc/issue # 查看操作系统版本 # cat /proc/cpuinfo # 查看CPU信息  # hostname # 查看计算机名 # lspci -tv # 列出所有PCI设备  # lsus
  • 设备树Device Tree详解

    千次阅读 2020-05-22 18:49:24
    1. 设备树(DeviceTree)基本概念及作用 2. 设备树的组成和使用 2.1. DTS和DTSI 2.2. DTC 2.3. DTB 2.4. Bootloader 3. 设备树中dts、dtsi文件的基本语法 3.1. chosennode 3.2. aliasesnode 3.3. ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 70,918
精华内容 28,367
热门标签
关键字:

典型的块设备包括