操作系统学习-3.Linux文件系统学习4-IO调度算法

电梯算法简介

电梯调度算法主要适用于LINUX I/O磁盘请求调度。

磁盘结构如下图所示,磁盘主要由盘面和磁头组成。

磁盘每次进读写请求时,需要给磁盘驱动器一个地址,磁盘驱动器根据给定地址计算出相应的扇区,然后将磁头移动到需要访问的扇区,开始进行读写。

读写磁盘时,转动磁头实际上很耗费时间,如果不采用调度策略,磁头直接根据请求进行直接访问,则必定会造成时间的浪费。

比如以下这种访问方式,访问序列为 盘面1的10,70,20,30,50,如果顺序访问,则是10-》70,70-》20,20-》30,30-》50,这样磁头实际上转了2圈,势必造成一种浪费。

采用电梯调度策略,访问顺序变成这样10->20->30->50->70,磁头转动一圈就可以满足所有的访问请求,从而节约I/O访问时间,提高效率。

基础原理

电梯调度策略实际上提供了一种思路,在进行数据访问的同时,如果有新的请求到来,直接将其按照升序插入到等待队列中。

磁盘每次直接从等待队列中取出相应的逻辑地址,直接进行磁盘访问。当然这种调度策略也存在缺陷,如果在某一块地址区域磁盘访问特别频繁,可能会造成处在等待序列末尾的请求“饥饿”,也就是队尾的请求需要等待很长时间才能满足。

调度策略的本质是一种排序算法,对排序队列的一种操作,这种队列采用链表实现比较容易。

基础函数梳理

elevator_init
elv_merge
__elv_add_request
elv_next_request/blk_peek_request

数据结构

struct elevator_type
{
  /* managed by elevator core */
  struct kmem_cache *icq_cache;

  /* fields provided by elevator implementation */
  struct elevator_ops ops;
  size_t icq_size;  /* see iocontext.h */
  size_t icq_align;  /* ditto */
  struct elv_fs_entry *elevator_attrs;
  char elevator_name[ELV_NAME_MAX];
  struct module *elevator_owner;

  /* managed by elevator core */
  char icq_cache_name[ELV_NAME_MAX + 5];  /* elvname + "_io_cq" */
  struct list_head list;
};




struct elevator_ops
{
  elevator_merge_fn *elevator_merge_fn;
  elevator_merged_fn *elevator_merged_fn;
  elevator_merge_req_fn *elevator_merge_req_fn;
  elevator_allow_merge_fn *elevator_allow_merge_fn;
  elevator_bio_merged_fn *elevator_bio_merged_fn;

  elevator_dispatch_fn *elevator_dispatch_fn;
  elevator_add_req_fn *elevator_add_req_fn;
  elevator_activate_req_fn *elevator_activate_req_fn;
  elevator_deactivate_req_fn *elevator_deactivate_req_fn;

  elevator_completed_req_fn *elevator_completed_req_fn;

  elevator_request_list_fn *elevator_former_req_fn;
  elevator_request_list_fn *elevator_latter_req_fn;

  elevator_init_icq_fn *elevator_init_icq_fn;  /* see iocontext.h */
  elevator_exit_icq_fn *elevator_exit_icq_fn;  /* ditto */

  elevator_set_req_fn *elevator_set_req_fn;
  elevator_put_req_fn *elevator_put_req_fn;

  elevator_may_queue_fn *elevator_may_queue_fn;

  elevator_init_fn *elevator_init_fn;
  elevator_exit_fn *elevator_exit_fn;
  elevator_registered_fn *elevator_registered_fn;
};

电梯初始化过程

blk_init_queue–》blk_init_queue_node–》blk_init_allocated_queue–》elevator_init

电梯算法noop

static struct elevator_type elevator_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,
};

直接看源码,整套看下来没问题,前提是要知道,电梯上层是什么时候调用什么接口,然后这些接口是做什么用的。
然后就没有然后了,算法就是一个list,没有其他任何杂质。

电梯算法deadline

static struct elevator_type iosched_deadline = {
  .ops = {
    .elevator_merge_fn =     deadline_merge,
    .elevator_merged_fn =    deadline_merged_request,
    .elevator_merge_req_fn =  deadline_merged_requests,
    .elevator_dispatch_fn =    deadline_dispatch_requests,
    .elevator_add_req_fn =    deadline_add_request,
    .elevator_former_req_fn =  elv_rb_former_request,
    .elevator_latter_req_fn =           elv_rb_latter_request,
    .elevator_init_fn =    deadline_init_queue,
    .elevator_exit_fn =    deadline_exit_queue,
  },

  .elevator_attrs = deadline_attrs,
  .elevator_name = "deadline",
  .elevator_owner = THIS_MODULE,
};

这个算法是很有意思的,
按照我的看法,应该是造成read和write相互饥饿,导致切换。
deadline的理论原理是,某一个操作的饥饿,导致累积,这样可以成块的处理这一操作。

初始化

static int deadline_init_queue(struct request_queue *q, struct elevator_type *e)
{
  struct deadline_data *dd;
  struct elevator_queue *eq;

  eq = elevator_alloc(q, e);
  if (!eq)
    return -ENOMEM;

  dd = kzalloc_node(sizeof(*dd), GFP_KERNEL, q->node);
  if (!dd) {
    kobject_put(&eq->kobj);
    return -ENOMEM;
  }
  eq->elevator_data = dd;

  INIT_LIST_HEAD(&dd->fifo_list[READ]);
  INIT_LIST_HEAD(&dd->fifo_list[WRITE]);
  dd->sort_list[READ] = RB_ROOT;
  dd->sort_list[WRITE] = RB_ROOT;
  dd->fifo_expire[READ] = read_expire;
  dd->fifo_expire[WRITE] = write_expire;
  dd->writes_starved = writes_starved;
  dd->front_merges = 1;
  dd->fifo_batch = fifo_batch;

  spin_lock_irq(q->queue_lock);
  q->elevator = eq;
  spin_unlock_irq(q->queue_lock);
  return 0;
}

前面的开辟节点空间的,等等的就不说了。主要看下参数的意义。

1、sort_list 有两个,一个读一个写,这就意味着,有两个红黑树。

2、fifo_expire 也哟两个,表示到期的时间

static const int read_expire = HZ / 2; /* max time before a read is submitted. */

static const int write_expire = 5 * HZ; /* ditto for writes, these limits are SOFT! */

HZ 不知道是多少,如果一秒一次来说的话,就是500ms 和 5s。但总之写的时间是读的10倍

3、writes_starved 表示读这个操作可以超过写几次。

static const int writes_starved = 2; /* max times reads can starve a write */

这里赋值两次后面再运用的时候是这样的

if (writes && (dd->starved++ >= dd->writes_starved))
goto dispatch_writes;
所以这边还有个starved表示读取的次数,是从0开始的。这个条件判断就是,读两次,写一次。
4、front_merges 前置合并,这个前置和后置一直没有疏通是什么意思。
5、fifo_batch 批量处理的个数,
static const int fifo_batch = 16; /* # of sequential requests treated as one
这边是16个。后面我们会看到,具体算法实现中是怎么运用这个参数的。

算法详解

1、从add开始

static void
deadline_add_request(struct request_queue *q, struct request *rq)
{
  struct deadline_data *dd = q->elevator->elevator_data;
  const int data_dir = rq_data_dir(rq);

  deadline_add_rq_rb(dd, rq);

  /*
   * set expire time and add to fifo list
   */
  rq->fifo_time = jiffies + dd->fifo_expire[data_dir];
  list_add_tail(&rq->queuelist, &dd->fifo_list[data_dir]);
}

加入红黑树

加入fifo队列,是有时间参数的。

so,很简单

2、merge请求

static int
deadline_merge(struct request_queue *q, struct request **req, struct bio *bio)
{
  struct deadline_data *dd = q->elevator->elevator_data;
  struct request *__rq;
  int ret;

  /*
   * check for front merge
   */
  if (dd->front_merges) {
    sector_t sector = bio_end_sector(bio);

    __rq = elv_rb_find(&dd->sort_list[bio_data_dir(bio)], sector);
    if (__rq) {
      BUG_ON(sector != blk_rq_pos(__rq));

      if (elv_rq_merge_ok(__rq, bio)) {
        ret = ELEVATOR_FRONT_MERGE;
        goto out;
      }
    }
  }

  return ELEVATOR_NO_MERGE;
out:
  *req = __rq;
  return ret;
}

根据bio 的sector 从rb里面找到一个相应位置的rq。

struct request *elv_rb_find(struct rb_root *root, sector_t sector)
{
  struct rb_node *n = root->rb_node;
  struct request *rq;

  while (n) {
    rq = rb_entry(n, struct request, rb_node);

    if (sector < blk_rq_pos(rq))
      n = n->rb_left;
    else if (sector > blk_rq_pos(rq))
      n = n->rb_right;
    else
      return rq;
  }

  return NULL;
}

那么看这个逻辑应该返回的是最后叶子的父节点,我们用图示来表示。
在这里插入图片描述
对照这个图,如果9是根节点,然后来了个3,最终它会到4的左边节点,所以返回的是rq。

static void
deadline_merged_requests(struct request_queue *q, struct request *req,
       struct request *next)
{
  if (!list_empty(&req->queuelist) && !list_empty(&next->queuelist)) {
    if (time_before(next->fifo_time, req->fifo_time)) {
      list_move(&req->queuelist, &next->queuelist);
      req->fifo_time = next->fifo_time;
    }
  }
  deadline_remove_request(q, next);
}

这merged个有点意思了, req是刚才返回的4,而next是我们自己生成的rq,也就是将来入住菱形的部分。

merged太难研究

3.分发流程

/*
 * deadline_dispatch_requests selects the best request according to
 * read/write expire, fifo_batch, etc
 */
static int deadline_dispatch_requests(struct request_queue *q, int force)
{
  struct deadline_data *dd = q->elevator->elevator_data;
  const int reads = !list_empty(&dd->fifo_list[READ]);
  const int writes = !list_empty(&dd->fifo_list[WRITE]);
  struct request *rq;
  int data_dir;
  if (dd->next_rq[WRITE])
    rq = dd->next_rq[WRITE];
  else
    rq = dd->next_rq[READ];

  if (rq && dd->batching < dd->fifo_batch)
    goto dispatch_request;

  if (reads) {
    BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[READ]));

    if (writes && (dd->starved++ >= dd->writes_starved))
      goto dispatch_writes;

    data_dir = READ;

    goto dispatch_find_request;
  }

  if (writes) {
dispatch_writes:
    BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE]));

    dd->starved = 0;

    data_dir = WRITE;

    goto dispatch_find_request;
  }

  return 0;

dispatch_find_request:

  if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) {
    rq = rq_entry_fifo(dd->fifo_list[data_dir].next);
  } else {

    rq = dd->next_rq[data_dir];
  }

  dd->batching = 0;

dispatch_request:

  dd->batching++;
  deadline_move_request(dd, rq);

  return 1;
}

分发流程是整个算法的核心,我们分两种情况看:

第一种批量饥饿

流程我们简化代码

static int deadline_dispatch_requests(struct request_queue *q, int force)
{
  if (dd->next_rq[WRITE])
    rq = dd->next_rq[WRITE];
  else
    rq = dd->next_rq[READ];

  if (rq && dd->batching < dd->fifo_batch)
    goto dispatch_request;
dispatch_request:

  dd->batching++;
  deadline_move_request(dd, rq);

  return 1;
}

就是很简单了,dispatch_request 然后到deadline_move_request,batching++,会到16.

但是这里前面的,精髓还不是这样,你单单看这些是看不出来的,要真正的理解必须结合整个算法的逻辑。

这里再if判断 是会保持方向的,也就是说如果是read,就会联系发16个read,之前我也一直看不懂这个函数的逻辑,

为什么说会联系16个保持方向呢?要看最后一个函数

static void
deadline_move_request(struct deadline_data *dd, struct request *rq)
{
  const int data_dir = rq_data_dir(rq);

  dd->next_rq[READ] = NULL;
  dd->next_rq[WRITE] = NULL;
  dd->next_rq[data_dir] = deadline_latter_request(rq);

  dd->last_sector = rq_end_sector(rq);
  deadline_move_to_dispatch(dd, rq);
}

看后面两个是分发的,没什么好说的,看前面这里把制空,这个时候

dd->next_rq[data_dir] = deadline_latter_request(rq);

再通过rq获取一个,所以这个就是保持,deadline_latter_request(rq);这个会是同一个类型的rq吗?后面看了红黑树,因为初始化的时候,

我们看到是有两种红黑树,他们是分开的。所以是保持一致的。

第二种切换饥饿

如第一种批量到16 就要下来判断是否进行reads或者write切换

if (reads) {
    if (writes && (dd->starved++ >= dd->writes_starved))
      goto dispatch_writes;
    data_dir = READ;
    goto dispatch_find_request;
  }
  if (writes) {
dispatch_writes:
    BUG_ON(RB_EMPTY_ROOT(&dd->sort_list[WRITE]));
    dd->starved = 0;
    data_dir = WRITE;
    goto dispatch_find_request;
  }
dispatch_find_request:
  if (deadline_check_fifo(dd, data_dir) || !dd->next_rq[data_dir]) {
    rq = rq_entry_fifo(dd->fifo_list[data_dir].next);
  } else {
    rq = dd->next_rq[data_dir];
  }

如果确定某一种,这边判断fifo是否有时间线是饥饿的,如果有就用fifo里面的。

还有一种情况,就是刚才dd->next_rq 不是制空了吗?那就也是用fifo里面的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沈万三djh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值