0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

基于块层的组成“bio层”的详细解析

Linux阅码场 2018-02-03 16:23 次阅读

操作系统比如Linux关键的价值之一,就是为具体的设备提供了抽象接口。虽然后来出现了各种其它抽象模型比如“网络设备”和“位图显示(bitmap display)”,但是最初的“字符设备”和“块设备”两种类型的设备抽象依然地位显赫。近几年持久化内存(persistent memory)炙手可热,[与非易失性存储NVRAM概念不同, persistent memory强调以内存访问方式读写持久存储,完全不同与块设备层], 但在将来很长一段时间内,块设备接口仍然是持久存储(persistent storage)的主角。这两篇文章的目的就是去揭开这位主角的面纱。

术语“块层”常指Linux内核中非常重要的一部分 - 这部分实现了应用程序和文件系统访问存储设备的接口。 块层是由哪些代码组成的呢? 这个问题没有准确的答案。一个最简单的答案是在"block"子目录下的所有源码。这些代码又可被看作两层,这两层之间紧密联系但有明显的区别。我知道这两个子层次还没有公认的命名,因此这里就称作“bio层”和“request 层”吧。本文将带我们先了解"bio层",而在下一篇文章中讨论“request层”。

块层之上

在深挖bio层之前,很有必要先了解点背景知识,看看块层之上的天地。这里“之上”意思是靠近用户空间(the top),远离硬件(the bottom),包括所有使用块层服务的代码。

基于块层的组成“bio层”的详细解析

通常,我们可以通过/dev目录下的块设备文件来访问块设备,在内核中块设备文件会映射到一个有S_IFBLK标记的inode。这些inode有点像符号链接,本身不代表一个块设备,而是一个指向块设备的指针。更细地说,inode结构体的i_bdev域会指向一个代表目标设备的struct block_device对象。 struct block_device包含一个指向第二个inode的域:block_device->bd_inode, 这个inode会在块设备IO中起作用,而/dev目录下的inode只是一个指针而已。

第二个inode所起的主要作用(实现代码主要在fs/block_dev.c, fs/buffer.c,等)就是提供page cache。如果设备文件打开时没有加O_DIRECT标志,与inode关联的page cache用来缓存预读数据,或缓存写数据直到回写(writeback)过程将脏页刷到块设备上。如果用了O_DIRECT,读和写绕过page cache直接向块设备发请求。相似地,当一个块设备格式化并挂载成文件系统时,读和写操作通常会直接作用在块设备上 [作者写错了?],尽管一些文件系统(尤其是ext*家族)能够访问相同的page cache(过去称为buffer cache)来管理一些文件系统数据。

open()另一个与块设备相关的标志是O_EXCL。块设备有个简单的劝告锁(advisory-locking)模型,每个块设备最多只能有个“持有者”(holder)。在激活一个块设备时,[激活泛指驱动一个块设备的过程,包括向内核添加代表块设备的对象,注册请求队列等],可用blkdev_get()函数为块设备指定一个"持有者"。[ blkdev_get()的原型: int blkdev_get(struct block_device *bdev, fmode_t mode, void *holder), holder可以是一个文件系统的超级块, 也可以是一个挂载点等]。一旦块设备有了“持有者”,随后再试图激活该设备就会失败。通常在挂载时,文件系统会为块设备指定一个“持有者”,来保证互斥使用块设备。当一个应用程序试图以O_EXCL方式打开块设备时,内核会新建一个struct file对象并把它作为块设备的“持有者”,假如这个块设备作为文件系统已经被挂载,打开操作就会失败。如果open()操作成功并且还没有关上,尝试挂载操作就会阻塞。但是,如果块设备不是以O_EXCL打开的,那么O_EXCL就不能阻止块设备被同时打开,O_EXCL只是便于应用程序测试块设备是否正在使用中。

无论以什么方式访问块设备,主要接口都是发送读写请求,或其它特殊请求比如discard操作, 最终接收处理结果。bio层就是要提供这样的服务。

bio层

Linux中块设备用struct gendisk表示,即 一个通用磁盘 (generic disk)。这个结构体也没包含太多信息,主要起承上启下的作用,上承文件系统,下启块层。往上层走,一个gendisk对象会关联到block_device对象,如我们上文所述,block_device对象被链接到/dev目录下的inode中。如果一个物理块设备包含多个分区,也就说有个分区表,那么这个gendisk对象就对应多个block_device对象。其中,有一个block_device对象代表着整个物理磁盘gendisk,而其它block_device各代表gendisk中的一个分区。

struct bio是bio层一个重要的数据结构,用来表示来自block_device对象的读写请求,以及各种其它的控制类请求,然后把这些请求传达到驱动层。一个bio对象包括的信息有目标设备,设备地址空间上的偏移量,请求类型(通常是读或写),读写大小,和用来存放数据的内存区域。在Linux 4.14之前,bio对象是用block_device来表示目标设备的。而现在bio对象包含一个指向gendisk结构体的指针和分区号,这些可通过bio_set_dev()函数设置。这样做突出了gendisk结构体的核心地位,更自然一些。

一个bio一旦构造好,上层代码就可以调用generic_make_request()或submit_bio()提交给bio层处理。[submit_bio()只是generic_make_request()的一个简单封装]。 通常,上层代码不会等待请求处理完成,而是把请求放到块设备队列上就返回了。generic_make_request()有时可能阻塞一小会,比如在等待内存分配的时候,这样想可能更容易理解,它也许要等待一些已经在队列上的请求处理完成,然后腾出空间。如果bi_opf域上设置了REQ_NOWAIT标志,generic_make_request()在任何情况下都不应该阻塞,而应该把这个bio的返回状态设置成BLK_STS_AGAIN或BLK_STS_NOTSUPP,然后立即返回。截至写作时,这个功能还没有完全实现。

bio层和request层间的接口需要设备驱动调用blk_queue_make_request()来注册一个make_request_fn()函数,这样generic_make_request()就可以通过回调这个函数来处理提交个这个块设备的bio请求了。make_request_fn()函数负责如何处理bio请求,当IO请求完成时,调用bio_endio()设置bi_status域的状态来表示请求是否处理成功,并回调保存在bio结构体里的bi_end_io函数。

除了上述对bio请求的简单处理,bio层最有意思的两个功能就是:避免递归调用(recursion avoidance)和队列激活(queue plugging)。

避免递归(recursion avoidance)

在存储方案里,经常用到"md" [mutiple device] (软RAID就是md的一个实例)和"dm" [device mapper] (用于multipath和LVM2)这两种虚拟设备,也常叫做栈式设备,由多个块设备按树的形式组织起来,它们会沿着设备树往下一层一层对bio请求作修改和传递。如果采用递归的简单的实现,在设备树很深的情况下,会占用大量的内核栈空间。很久以前 (Linux 2.6.22),这个问题时不时会发生,在使用一些本身就因递归调用占用大量内核栈空间的文件系统时,情况更加糟糕。

为了避免递归,generic_make_request()会进行检测,如果发现递归,就不会把bio请求发送到下一层设备上。这种情况下,generic_make_request()会把bio请求放到进程内部的一个队列上(currect->bio_list, struct task_struct的一个域), 等到上一次的bio请求处理完以后,然后再提交这一层的请求。由于generic_make_request()不会阻塞以等待bio处理完成,即使延迟一会再处理请求都是没问题的。

通常,这个避免递归的方法都工作得很完美,但有时候可能发生死锁。理解死锁如何发生的关键就是上文我们对bio提交方式的观察: 当递归发生时,bio要排队等待之前已经提交的bio处理完成。如果要等的bio一直在current->bio_list队列上而得不到处理,它就会一直等下去。

引起bio互相等待而产生死锁的原因,不太容易发现,通常都是在测试中发现的,而不是分析代码发现的。以bio拆分 (bio split)为例,当一个bio的目标设备在大小或对齐上有限制时,make_request_fn()可能会把bio拆成两部分,然后再分别处理。bio层提供了两个函数(bio_split()和bio_chain()),使得bio拆分很容易,但是bio拆分需要给第二个bio结构体分配空间。在块层代码里分配内存要特别小心,尤其当内存紧张时,Linux在回收内存时,需要把脏页通过块层写出去。如果在内存写出的时候,又需要分配内存,那就麻烦了。一个标准的机制就是使用mempool,为一个某种关键目的预留一些内存。从mempool分配内存需要等待其它mempool的使用者归还一些内存,而不用等待整个内存回收算法完成。当使用mempool分配bio内存时,这种等待可能会导致generic_make_request()死锁。

社区已经有多次尝试提供一个简单的方式来避免死锁。一个是引入了"bioset" 进程,你可以用ps命令在电脑上查看。这个机制主要关注的就是解决上面描述的死锁问题,为每一个分配bio的"mempool"分配一个"rescuer"线程。如果发现bio分配不出来,所有在currect->bio_list的bio就会被取下来,交个相应的bioset线程来处理。这个方法相当复杂,导致创建了很多bioset线程,但是大多时候派不上用场,只是为了解决一个特殊的死锁情况,代价太高了。通常,死锁跟bio拆分有关系,但是它们不总是要等待mempool分配。[最后这句话,有些突兀]

最新的内核通常不会创建bioset线程了,而只是在几种个别情况下才会创建。Linux 4.11内核,引入了另一个解决方案,对generic_make_request()做了改动,好处是更通用,代价小,但是却对驱动程序提出了一点要求。主要的要求是在发生bio拆分时,其中一个bio要直接提交给generic_make_request()来安排最合适的时间处理,另一个bio可以用任何合适的方式处理,这样generic_make_request()就有了更强的控制力。 根据bio在提交时在设备栈中的深度,对bio进行排序后,总是先处理更低层设备的bio, 再处理较高层设备的bio。这个简单的策略避免了所有恼人的死锁问题。

块队列激活(queue plugging)

存储设备处理单个IO请求的代价通常挺高的,因此提高处理效率的一个办法就是把多个请求聚集起来,然后做一次批量提交。对于慢速设备来说,队列上积攒的请求通常会多一些,那么做批处理的机会就多。但是,对于快速设备,或经常处于空闲状态的慢速设备来说,做批处理的机会就显然少了很多。为了解决这个问题,Linux块层提出了一个机制叫"plugging"。[plugging, 即堵上塞子,队列就像水池,请求就像水,堵上塞子就可以蓄水了]

原来,plugging仅仅在队列为空的时候才使用。在向一个空队列提交请求前,这个队列就会被“堵塞”上一会时间,好让请求积蓄起来,暂时不往底层设备提交。文件系统提交的bio就会排起队来,以便做批处理。文件系统可以主动请求,或着定时器周期性超时,来拔开塞子。我们预期的是在一定时间内聚集一批请求,然后在一点延迟后就开始真正处理IO,而不是一直聚积特别多的请求。从Linux 2.6.30开始,有了一个新的plugging机制,把积蓄请求的对象,从面向每个设备,改成了面向每个进程。这个改进在多处理器上扩张性很好。

当文件系统,或其它块设备的使用者在提交请求时,通常会在调用generic_make_request()前后加上blk_start_plug()和blk_finish_plug()。 blk_start_plug()会初始化一个struct blk_plug结构体,让current->plug指向它,这个结构体里面包含一个请求列表(我们会在下一篇文章细说这个)。因为这个请求列表是每个进程就有一个,所以在往列表里添加请求时不用上锁。如果可以更高效率的处理请求,make_request_fn()就会把bio添加到这个列表上。

当blk_finish_plug()被调用时,或调用schedule()进行进程切换时(比如,等待mutex锁,等待内存分配等),保存在current->plug列表上的所有请求就要往底层设备提交,就是说进程不能身负IO请求去睡觉。

调用schedule()进行进程切换时,积蓄的bio会被全部处理,这个事实意为着bio处理的延迟只会发生在新的bio请求不断产生期间。假如进程因等待要进入睡眠,那么积蓄起来的bio就会被立即处理。这样可以避免出现循环等待的问题,试想一个进程在等待一个bio请求处理完成而进入睡眠,但是这个bio请求还在plug列表上并没有下发给底层设备。

像这样进程级别的plugging机制,主要的好处一是相关性最强的bio会更容易聚集起来,以便批量处理,二是这样很大程度上减少了队列锁的竞争。如果没有进程级别的plugging处理,那么每一个bio请求到来时,都要进行一次spinlock或原子操作。有了这样的机制,每一个进程就有一个bio列表,把进程bio列表往设备队列里合并时,只需要上一次锁就够了。

bio层及以下(bio layer and below)

总之,bio层不是很复杂,它将IO请求以bio结构体的方式直接传递给相应的make_request_fn() [具体的实现有通用块层的blk_queue_bio(), DM设备的dm_make_request(), MD设备的md_make_request()]。bio层实现了各种通用的函数,来帮助设备驱动层处理bio拆分,scheduling the sub-bios [不会翻译这个,意思应该是安排拆分后的bio如何处理], "plugging"请求等。 bio层也会做一些简单操作,比如更新/proc/vmstat中的pgpgin和pgpgout的计数,然后把IO请求的大部分操作交给下一层处理 [request层]。

有时候,bio层的下一层就是最终的驱动,比如说DRBD(The Distributed Replicated Block Device)或 BRD (a RAM based block device). 更常见的下一层有MD和DM提供这种虚拟设备的中间层。不可或缺的一层,就是除bio层之外剩下的部分了,我称之为"request 层",这将是我们在下一篇讨论的话题

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    87

    文章

    10979

    浏览量

    206673

原文标题:块层介绍 第一篇: bio层

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    8PCB 布铜问题

    请教各位大师:FPGA 设计的一8板,2电源,2地,4sig。 FPGA 信号有80M. 想问一下怎么叠合适? 电源
    发表于 07-18 11:07

    4板手动布线问题

    各位老师,一已用过一段时间的4板( 别人画的),现想用手动布线的方式做一些小的修改,但在布线时,有一些新放置的过孔和焊盘,不知如何使它从顶层连接的底层?或顶层连接到电源?或连接到地线
    发表于 09-24 21:04

    芯片去Delayer

    (Delayer)。透过芯片研磨(Polishing)与去(Delayer)可逐检视是否有缺陷,并可提供后续实验,清楚解析出每一电路布线结构。以干、湿蚀刻及研磨(Polishin
    发表于 08-21 09:44

    AD10画4板内电一般都设计成什么

    各位大侠们,这里两天我画了一4板,由于第一次画4板,有几个问题不清楚,还望赐教!(本人是学生,问的问题低级了,还望各位不要嘲笑。) (1)我的板子是双电源(正负5V),内电(p
    发表于 01-23 06:36

    板各层的厚度怎么设置

    我第一次画四板,四依次是top,gnd,pwr,bottom,但是每两之间都有介质,请问一般应该给各个信号,内电
    发表于 05-13 07:35

    网络互联协议基础

    Protocol/Internet Protocol)已成为一个事实上的工业标准。 TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。 TCP/IP协议簇分为四,IP位于
    发表于 05-29 06:25

    关于四板差分的参考是电源平面的问题

    1.四板叠:S-G-P-S因差分线序交叉需打孔换走BOTTOM,现在第四差分对应的参考平面是电源平面。一直有个疑惑:第四差分对应的
    发表于 08-06 09:45

    哪些可以当作阻抗参考

    那些可以当作参考,还是需要地层作为参考?例如板子是四板,WIFI天线为L1-L4是怎么设定
    发表于 09-19 05:35

    为什么DFM解析出来的文件有些2D线(所有)的方孔不能挖空

    请问,在DFM解析出来的文件中,仿真显示,在板框里,使用了所有的方孔不能挖空呢?
    发表于 07-04 16:57

    基于三前馈BP神经网络的图像压缩算法解析

    本文介绍了基于三前馈BP神经网络的图像压缩算法,提出了基于FPGA的实现验证方案,详细讨论了实现该压缩网络组成的重要模块MAC电路的流水线设计。
    发表于 05-06 07:01

    浅析串口通讯协议的物理和协议

    什么是串口通讯?串口通讯协议物理的结构是由哪些部分组成的?串口通讯协议的协议的主要标准是什么?
    发表于 10-22 09:30

    IIC物理是由哪些部分组成

    在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核和片上外设;STM32 标准库则是在寄存器与用户代码之间的软件。对于通讯协议,我们也以分层的方式来理解,最基本的是把它分为
    发表于 12-13 08:09

    AUTOSAR基础软件是由哪些部分组成

    基础软件主要是用于提供基础软件服务,包括标准化的系统功能以及功能接口,并且由一系列的基础服务软件组成,包括系统服务、内存服务、通信服务等。一、基础软件模块按照类型可以分为驱动模块、接口模块、处理模块以及管理器。驱动模块:包含
    发表于 02-17 08:00

    BERT中的嵌入组成以及实现方式介绍

    介绍在本文中,我将解释BERT中嵌入的实现细节,即token嵌入、Segment嵌入和Position嵌入。简介这是一张来自论文的图,它恰当地描述了BERT中每一个嵌入的功能:与大多数旨在
    发表于 11-02 15:14

    PCB叠的几种不同变体

    走线组成,而不是大型连续面积。对这些进行详细的实验设计显然是不可行的,因此采用了“百分比覆盖率”方法。采用这种方法,计算出结构的平均电导率,以铜厚度占总面积的百分比计算。这些实验设
    发表于 04-20 17:10