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

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

3天内不再提示

Linux块层架构介绍 块层IO流程与块层IO调度器详解

Linux爱好者 ? 来源:奇伢云存储 ? 作者:奇伢 ? 2022-05-16 12:12 ? 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

67d3d51e-d4cc-11ec-bce3-dac502259ad0.png

块存储没那么神秘!

之前一直跟大家聊文件系统,文件系统提供一层文件到物理块层的映射转换。这层逻辑可能非常复杂,依赖于文件系统的实现。今天则跟大家聊聊块层,块层位于 fs 层之下,大家可能平时不怎么接触,看不见摸不着。其实没有那么神秘,来看下块设备的使用姿势, Linux 经常看见的 sda,sdb 这样的盘符文件其实就是块设备。

我们来试一下对一个块设备进行读写(注意,千万找一个不用的块设备,有写操作):

funcmain(){
  //千万注意:/dev/sdx1 盘,注意一定是要是不用的新盘哦。这里有写操作,可千万不要写坏数据哦
  f,err:=os.OpenFile("/dev/sdx1",os.O_RDWR,0666)
  iferr!=nil{
    log.Fatalf("err:%v
",err)
  }
  //写
  content:="helloworld"
  _,err=f.WriteAt([]byte(content),0)
  iferr!=nil{
    log.Fatalf("err:%v
",err)
  }
  //读
  buffer:=make([]byte,len(content))
  n,err:=f.ReadAt(buffer,0)
  iferr!=nil{
    log.Fatalf("err:%v
",err)
  }

  fmt.Printf("content=%s,n=%v
",buffer,n)
}

以上就完成了对块设备的一次读写,大家看用户态操作一下块设备是不是非常简单呀。说白了,就是把它当一个线性的文件来使用即可。用户态的文件系统经常喜欢这样去管理块设备,来看一下块层的架构。

块层架构

块设备的逻辑集中在 Linux 的块层子系统中。由于块设备有多种的类型,并且内部关于不同的场景可能会有不同的调度需求,所以它的设计也必须满足一定的抽象,块层内一般分为三层

  • 通用层 :抽象不同的块设备,为上层建立统一的块设备模型;
  • IO 调度层 :IO 请求入队、出队,并且按照特定的策略去调度(可配置);
  • 块设备驱动层 :针对不同的块设备类型有着不同驱动程序。比如 SCSI 设备,逻辑设备等;

聊聊这三层:

  • 通用层:给上层一个叫做 submit_bio 的函数,并且和上层对齐的 IO 类型叫做 bio ,所有的块 IO 请求封装成 bio ,然后通过 submit_bio 递交即可。这是统一的抽象界面;
  • IO 调度层:无非就是上层请求怎么入队然后怎么派发给底层。整体的块 IO 调度器叫做电梯,它的架子也是电梯算法的架子,调度算法允许配置,常见的就是 cfq,deadline,noop 这三种算法;
  • 块设备驱动层:针对不同块设备的封装,比如有顺序访问的块设备,还有随机访问的,还有逻辑栈式设备;

块层 IO 流程

今天走一小段代码,省略复杂逻辑,只撸主干逻辑。

下面涉及到代码,配合 Linux 3.10.1

1抽象界面 submit_bio

块层的入口是 submit_bio 函数,上层填装 bio 结构之后,调用 submit_bio 递交到块层。一切的开始都是从这个函数。bio 就是块层和上层约定好的参数结构。块层的核心工作就是对这个结构做处理,做变形,最终做转发。

块设备的读写都是通过这个接口和参数来表达。来看一下 submit_bio 的逻辑:

voidsubmit_bio(intrw,structbio*bio)
{
  //打上读写标记
  bio->bi_rw|=rw;
  //...
  //构造request
  generic_make_request(bio);
}

一个小信息:FS 和 块层交互的是 bio 类型,但是块层内部用的是 request 结构,由 bio 转换而来。

2IO 请求入队 make_request_fn

来看下 generic_make_request 中 实现:

voidgeneric_make_request(structbio*bio)
{
  //保证make_requst核心的逻辑在同一个进城内是串行之行的
  if(current->bio_list){
    bio_list_add(current->bio_list,bio);
    return;
  }

  bio_list_init(&bio_list_on_stack);
  current->bio_list=&bio_list_on_stack;
  //循环处理bio_list里的内容
  do{
    //获取到该块设备的队列
    structrequest_queue*q=bdev_get_queue(bio->bi_dev);
    //执行make_request_fn回调
    q->make_request_fn(q,bio);
    bio=bio_list_pop(current->bio_list);
  }while(bio);
  current->bio_list=NULL;
}

块设备都会有个 request_queue 结构体。队列这个很容易理解,用来挂请求的。排序、合并之后 IO 请求总得放在一个地方。

上面的核心是 request queue 的 make_request_fn 函数回调。合并和排序就是在该函数中实现。

思考:make_request_fn 又是什么呢?什么时候赋值的呢?

其实是设备创建之初,初始化结构体的时候赋值好的。比如 SCSI 设备,那么 make_request_fn 就是 blk_queue_bio 。

注意:这里我们看到还有一个 request_fn 的回调,SCSI 设备是初始化成 scsi_request_fn ,记住这个,后面会讲。

//分配并初始化一个scsi设备
scsi_alloc_sdev
scsi_alloc_queue
//创建、初始化请求队列,回调request_fn
__scsi_alloc_queue(sdev->host,scsi_request_fn);
blk_init_queue
blk_init_queue_node
blk_init_allocated_queue
//回调make_request_fn
blk_queue_make_request(q,blk_queue_bio);

排序、合并的逻辑则是在 make_request_fn 中完成,也就是 IO 请求入队的过程就完成了。

思考:合并、排序究竟是什么意思?

  • 合并:将磁盘上扇区连续的多个请求合并成一个,然后作为一个 SCSI 请求命令下发
  • 排序:将多个 IO 请求按照磁盘扇区的位置进行排序,以便磁头能够尽可能的往一个方向摆动

合并和排序在 make_request_fn 中完成。来看一看调度器的逻辑:

voidblk_queue_bio(structrequest_queue*q,structbio*bio)
{
  el_ret=elv_merge(q,&req,bio);
  if(el_ret==ELEVATOR_BACK_MERGE){
    //往后合并
  }elseif(el_ret==ELEVATOR_FRONT_MERGE){
    //往前合并
  }

get_rq:
  //没合并的新请求走这里

  plug=current->plug
  if(plug){
    list_add_tail(&req->queuelist,&plug->list);
  }else{
    add_acct_request(q,req,where);
    __blk_run_queue(q);
  }
}

elv_merge 会返回一个常量来标识向前或者向后合并。如果是向前或者向后合并,那么返回对应的已经存在的 request 结构体。如果是不能合并,那么就创建的一个新的 request 请求。

思考:IO 请求在 make_request_fn 入队,那什么时候派发呢?

是通过 request_fn 回调函数(往前看,设备初始化的时候赋值的)来处理。SCSI 设备的 request_fn 回调是 scsi_request_fn 函数。函数中有个 for 循环,会按照策略逐个取出请求,封装处理一下,然后丢到底层去执行 ( 使用 scsi_dispatch_cmd )。

思考:有没有想过能够合并和排序的基础条件是啥?

基础是:有足够多的请求。这个依赖于 Linux 的一个叫做蓄流/泄流的机制(Plugging / Unplugging)。蓄流/泄流什么意思?

Linux 块层为了增加吞吐能力,对于来的请求,并不是立即下发,而是 hold 一段时间,攒一会儿,超时了或者超阈值了,再一次性处理请求下发。类似于用塞子堵住一下(蓄流)池子,水满了再拔掉塞子(泄流)。

很容易理解,块层之所以不将每个请求都立马下发,就是为了实现 IO 的调度。这种短暂的 hold 请求本质上是优化的基础。如果每一个请求都不聚合,不把大家聚在一起,那么是无法找到优化的时机的。

3IO 请求派发 request_fn

request 的派发逻辑则是在 request_fn 中完成,SCSI 是 scsi_request_fn 函数。Unplug 的时候就会走到这里来。

staticvoidscsi_request_fn(structrequest_queue*q)
{
  for(;;){
    //按照IO调度策略取请求出来
    req=blk_peek_request(q);

    //派发命令到SCSI驱动层
    rtn=scsi_dispatch_cmd(cmd);
  }
}

这里的逻辑则是集中在 ”怎么取请求出来?“ 。

IO 调度算法的主要发挥时机就是在这里!(还有一个时机就是入队的时候)

把 request 选出来之后,封装成 scsi cmd 命令,通过 scsi_dispatch_cmd 函数下发到 SCSI 驱动层即可。这样一个请求的派发就算完成了。

块层 IO 调度器

接着上面,聊聊块层 IO 调度器。首先块层整体的 IO 调度器是一个电梯算法的架子,你会发现 elevator 的缩写到处都是。随着 Linux 系统的持续发展还是衍生了不同于真正简单电梯算法的调度算法。这个就是我们常见的 noop,deadline,cfq 等算法。这部分则是被抽象成一个界面,可以让用户来配置选择。这个统一界面类型叫做 elevator_type :

structelevator_type{
  //最重要的是这个
  structelevator_opsops;
}

最重要就是 ops 字段,以 noop 算法为例:

staticstructelevator_typeelevator_noop={
  .ops={
    .elevator_merge_req_fn=noop_merged_requests,
    .elevator_dispatch_fn=noop_dispatch,
    .elevator_add_req_fn=noop_add_request,
    .elevator_former_req_fn=noop_former_request,
    .elevator_latter_req_fn=noop_latter_request,
    .elevator_init_fn=noop_init_queue,
    .elevator_exit_fn=noop_exit_queue,
  }
  .elevator_name="noop",
  .elevator_owner=THIS_MODULE,
}

也就是说,不同的调度算法只要实现了这个 elevator_type ,就可以在"电梯"的大框架里运行了。所以,其实“电梯”已经不是“电梯”了

调度算法主要做好两个事情:

  1. bio 请求入队:合并、排序?随你。比如 noop 就没有合并和排序
  2. request 请求出队:电梯不电梯?随你,目前就看 deadline 是变形的“电梯”。

调度算法的实现在:

  • noop 的实现在 block/noop-iosched.c ,及其简单
  • deadline 的实现在 block/deadline-iosched.c ,主要加了一个超时,饥饿的考虑
  • cfq 的实现在 block/cfq-iosched.c ,号称绝对公平算法,考虑到不同进程的性能公平,逻辑是最复杂的

今天先不给大家展开调度策略的实现,只简要梳理其作用。说白了,无论哪种调度策略,都只是选出一个 request 出来而已。

思考:linux 怎么查看调度策略?

cat/sys/block/{块设备符}/queue/scheduler

1noop

它自身不带任何逻辑。没有合并、排序。请求按照 fifo 的方式入队出队。谁先进的谁先出,不区分请求。

这种简单的算法,反而适用于超高性能的介质,比如 ssd ,因为 ssd 的性能已经足够高了,自身提供的并发能力也是够的。不需要上层做顺序化,批量化,它没有磁头摆动的问题。合并和排序等多余的逻辑对它来讲就是累赘。

2deadline

最像电梯算法的 IO 调度算法。在电梯算法的基础上,再加上请求超时的考虑,起到防止饥饿请求的作用。它内部有两部电梯:读写。读请求优先,但也会考虑写饥饿,同一时间只有一部电梯在运行。

3cfq

完全公平队列,为每个进程单独创建一个队列来管理该进程所产生的请求,试图给不同的进程分配相同的块设备使用时间片,以此来保证每个进程都能被很好的分配到 IO 带宽。该算法就属于万金油,通用场景用它是最保险的,是最通用的 IO 调度算法。它的逻辑实现也是最复杂的。noop 100 行代码,deadline 460 行,cfq 4600 行代码,由此可见。

总结

  • submit_bio 是块层的统一接口函数,bio 是统一 IO 结构体
  • request 是块层内部的 IO 请求的结构体,bio 到 request 有一层转换,这层转换就是排序、合并的时候;
  • make_request_fn 就是 bio 转换成 request 的时机,IO 的排序、合并时机就在于此
  • request_fn 是 request 派发的时机,IO 的调度算法也在此发挥作用。从 request 队列中按照策略选一个 req 出来,封装成底层想要的样子,下发下去
  • IO 调度器合并、排序,包括按照电梯直上直下派发请求的目的是减少磁盘寻址时间,从而提高整体性能;
  • noop,cfq,deadline 三种派发请求的算法都有适用自己的场景。noop 是几乎 bypass 请求,没有重拍、合并。deadline 是电梯算法衍生了一下。cfq 是全新的一种公平算法,跟电梯没啥关系;
  • 块层最核心的逻辑是:bio 请求怎么入队,request 请求怎么出队而已,它远比文件系统要简单;

原文标题:Linux 块层 IO 子系统|最核心的逻辑是什么?

文章出处:【微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

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

    关注

    87

    文章

    11522

    浏览量

    214309
  • 文件系统
    +关注

    关注

    0

    文章

    297

    浏览量

    20430
  • 调度器
    +关注

    关注

    0

    文章

    98

    浏览量

    5516

原文标题:Linux 块层 IO 子系统|最核心的逻辑是什么?

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    内电分割时,能不能分割成多个不连续

    在进行内电分割时,同一网络必须是一个连续的整体,而不能分割成多个不连续么?
    发表于 09-29 08:37

    liunx下文件系统,都会与设备交互吗?

    下面这张图大家应该都很熟悉了,看起来linux下文件系统都要走设备。然后再与具体的存储器件交互。那么,请问下,是否有例外呢?
    发表于 01-17 11:29

    EPA功能及用户技术研究

    EPA功能及用户技术研究 Research on EPA Functional Block and User Layer Technology
    发表于 03-17 09:14 ?16次下载

    基于三架构流程模拟系统的设计与应用_刘红霞

    基于三架构流程模拟系统的设计与应用_刘红霞
    发表于 01-19 21:54 ?0次下载

    Linux设备驱动开发详解》第13章、Linux设备驱动

    Linux设备驱动开发详解》第13章、Linux设备驱动
    发表于 10-27 11:24 ?18次下载
    《<b class='flag-5'>Linux</b>设备驱动开发<b class='flag-5'>详解</b>》第13章、<b class='flag-5'>Linux</b><b class='flag-5'>块</b>设备驱动

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

    在深挖bio之前,很有必要先了解点背景知识,看看之上的天地。这里“之上”意思是靠近用户空间(the top),远离硬件(the bottom),包括所有使用
    的头像 发表于 02-03 16:23 ?4429次阅读
    基于<b class='flag-5'>块</b><b class='flag-5'>层</b>的组成“bio<b class='flag-5'>层</b>”的详细解析

    深度剖析基于的组成“request

    现在,"request"并存着两种模型:单队列(single-queue) 和 多队列(multi-queue)。多队列的出现也就是近几年的事情,也许总有一天会完全取代单队列的,但是目前来看两者
    的头像 发表于 02-03 16:29 ?4594次阅读
    深度剖析基于<b class='flag-5'>块</b><b class='flag-5'>层</b>的组成“request<b class='flag-5'>层</b>”

    Linux通用之deadline简介及通用调度框架

    接口很简单,首先判断request的IO方向,根据IO方向通过deadline_add_rq_rb将request添加到读/写红黑树中,request在红黑树中以请求的起始扇区作为节点的key value,可以直接认为红黑树中的request是按扇区增加的方向排好了序。
    的头像 发表于 04-29 16:40 ?6186次阅读
    <b class='flag-5'>Linux</b>通用<b class='flag-5'>块</b><b class='flag-5'>层</b>之deadline简介及通用<b class='flag-5'>块</b><b class='flag-5'>层</b><b class='flag-5'>调度</b><b class='flag-5'>器</b>框架

    Linux IO系统简介和调度的工作流程详细概述

    Linux内核组件要读写一些数据时,并不是请求一发出,内核便立即执行该请求,而是将其推迟执行。当传输一个新数据时,内核需要检查它能否通过。Linux IO
    的头像 发表于 05-27 10:41 ?5351次阅读
    <b class='flag-5'>Linux</b> <b class='flag-5'>IO</b>系统简介和<b class='flag-5'>调度</b><b class='flag-5'>器</b>的工作<b class='flag-5'>流程</b>详细概述

    pcb板各层画什么?丝印 机械 阻焊 助焊 信号 钻孔数据作用详解

    pcb板在画图的时候大家都知道,电路板会有很多层,那么首先我们要知道都是PCB板子的哪些。通过对PCB的各个图层的详细解答,希望能够对大家进一步了解一PCB的组成与设计有帮助。下面我们多以
    的头像 发表于 08-17 11:25 ?2.3w次阅读

    逻辑接口的IO口如何使用

    上篇博文:【FPGA】SRIO IP核系统总览以及端口介绍(一)(User Interfaces 之 I/O Port)根据数据手册PG007,介绍到了逻辑接口的IO口,今天想研究下
    的头像 发表于 08-18 09:35 ?5325次阅读
    逻辑<b class='flag-5'>层</b>接口的<b class='flag-5'>IO</b>口如何使用

    CXL事务详解

    CXL.ioIO设备提供非一致性的load/strore接口。事务类型、事务数据包格式、信用流量控制、虚拟通道管理、事务顺序的规则等遵循PCIe协议。CXL.io的事务如下图中的黄
    的头像 发表于 10-10 16:02 ?3514次阅读

    PCB阻焊与助焊的区别

    标准的印刷电路板 (PCB) 通常需要两种不同类型的,即“罩 (mask)”。
    发表于 06-01 16:58 ?3607次阅读
    PCB阻焊<b class='flag-5'>层</b>与助焊<b class='flag-5'>层</b>的区别

    查看linux系统磁盘io情况的办法是什么

    谈到 Linux 磁盘 I/O 的工作原理,我们了解到 Linux 存储系统 I/O 栈由文件系统(file system layer)、通用
    发表于 08-01 10:14 ?2606次阅读

    SCP基本构建介绍

    1-什么是SCP ? 将电源和系统管理任务从应用处理(AP)中抽象出来。 ? 符合ARM系统控制和管理接口(SCMI)规范。 ? 执行环境不固定。可以在RTOS或裸机环境中运行。 2-基本构建
    的头像 发表于 11-02 16:52 ?1415次阅读
    SCP基本构建<b class='flag-5'>块</b><b class='flag-5'>介绍</b>