内核IO栈 | IO缓冲区的向下传递过程

news2025/1/16 5:16:40

最近遇到个这么一个事,查阅SCSI手册,对于READ(10)命令,似乎在采用DMA的情况下,下发的SCSI命令中,并没有内存空间的地址和长度:

34400f6909019c176f1e2856c3add998.png

也就是说,单纯从这一个命令中,我们并不知道将数据从硬盘的LBA+size传输到内存的哪个位置。

这似乎和我一开始对整个IO栈的理解是不一样的,这一部分内容的缺省直觉告诉我,SCSI层的底层驱动下发到设备的并不仅仅是一个简单的标准SCSI命令,而是另有其他的东西,或者存在某种机制完成了LBA和内存地址间的映射。

今天我们就来探究这个问题。bio转化为request进入到SCSI子系统之后,又被包装成scsi_cmnd调用设备提供的queuecommand函数被排入设备硬件队列中,那么,在进入到块IO子系统层之后,内存和磁盘上的数据是怎样建立了映射关系,确保能传输到正确的位置上呢?

要弄清楚这个问题,我们依然得从整个IO链路入手,重点关注相关缓冲区被传递或配置的部分。

bio的数据缓冲区

当一个bio到达块IO子系统时,它是带着数据缓冲区来的,即,这个bio已经包含了准备好的内存区域,用来存放本次读/写的数据,这段区域被保存在bio结构体的指向bio_vec结构的指针bi_io_vec中。

struct bio {
  ...
  struct bio    *bi_next;  /* request queue link */
  // 迭代器
  struct bvec_iter  bi_iter;
  // 指向bio_vec数组的第一个元素
  struct bio_vec    *bi_io_vec;  /* the actual vec list */
  ...
}


// bio_vec描述了一段连续的内存区域
struct bio_vec {
  struct page  *bv_page; // 这段连续内存区域的第一页
  unsigned int  bv_len; // 本段内存区域长度 bytes
  unsigned int  bv_offset; // 相较于页起始的偏移
};


// bvec_iter 用来协助遍历bio_io_vec数组,并记录了lba和size
struct bvec_iter {
  sector_t    bi_sector;  /* bio在磁盘上的起始扇区编号 */
  unsigned int    bi_size;  /* 还没有传输的字节数 */


  unsigned int    bi_idx;    /* current index into bvl_vec */


  unsigned int            bi_bvec_done;  /* number of bytes completed in
               current bvec */
};

以上各个结构间的关系如下图:

47f52a01938f4ed679dcf898c47ac1d8.jpeg

单个bio在物理盘上表示为bi_sector~bi_sector+bi_size这一段区域,但是在内存空间中,可以是离散的,由bi_io_vec起始的bio_vec描述。

bio是一个动态的过程,bvec_iter描述了当前bio执行的某一个瞬间的情况,表示它当前还剩余多少没有传输,当前处理到哪一个bio_vec(segment),已经传输了多少数据等。

显然,我们可以发现,bio的设计天然就是可以合并的,如果两个bio在物理上相邻,那么,它们可以合并后下发,链接件为bi_next字段。合并过程在块IO子系统层进行,因此,上层下发的bio,bi_next字段都为NULL。

bio向request的转化

在块IO子系统层收到bio后,需要将其转化为request后向下传递,这个过程中一定会有对所分配的内存区域的处理过程。

回顾之前已经发表过的内核IO栈知识,让我们重新以更细致的视角分析blk_mq_submit_bio函数,这个函数将创建并发送一个request到块设备,并且在函数中考虑合并、蓄流及IO调度。

blk_qc_t blk_mq_submit_bio(struct bio *bio)
{
  struct request_queue *q = bio->bi_bdev->bd_disk->queue;
  const int is_sync = op_is_sync(bio->bi_opf);
  const int is_flush_fua = op_is_flush(bio->bi_opf);
  struct blk_mq_alloc_data data = {
    .q    = q,
  };
  struct request *rq;
  struct blk_plug *plug;
  struct request *same_queue_rq = NULL;
  unsigned int nr_segs;
  blk_qc_t cookie;
  blk_status_t ret;
  bool hipri;


  blk_queue_bounce(q, &bio); // 反弹缓冲区 ※1
  __blk_queue_split(&bio, &nr_segs); // bio切分 ※2


  if (!bio_integrity_prep(bio)) // 完整性保护 ※3
    goto queue_exit;


  if (!is_flush_fua && !blk_queue_nomerges(q) &&
      blk_attempt_plug_merge(q, bio, nr_segs, &same_queue_rq)) // plug蓄流 ※4
    goto queue_exit;


  if (blk_mq_sched_bio_merge(q, bio, nr_segs)) // 调度合并
    goto queue_exit;


  rq_qos_throttle(q, bio);
  hipri = bio->bi_opf & REQ_HIPRI;


  data.cmd_flags = bio->bi_opf; // 操作码
  rq = __blk_mq_alloc_request(&data); // 分配一个request
  if (unlikely(!rq)) {
    rq_qos_cleanup(q, bio);
    if (bio->bi_opf & REQ_NOWAIT)
      bio_wouldblock_error(bio);
    goto queue_exit;
  }


  trace_block_getrq(bio);


  rq_qos_track(q, rq, bio);


  cookie = request_to_qc_t(data.hctx, rq);


  blk_mq_bio_to_request(rq, bio, nr_segs); // bio->req


  ret = blk_crypto_init_request(rq);
  if (ret != BLK_STS_OK) {
    bio->bi_status = ret;
    bio_endio(bio);
    blk_mq_free_request(rq);
    return BLK_QC_T_NONE;
  }
  // blk_mq的多条路径
  plug = blk_mq_plug(q, bio);
  if (unlikely(is_flush_fua)) {
    /* Bypass scheduler for flush requests */
    blk_insert_flush(rq);
    blk_mq_run_hw_queue(data.hctx, true);
  } else if (plug && (q->nr_hw_queues == 1 ||
       blk_mq_is_sbitmap_shared(rq->mq_hctx->flags) ||
       q->mq_ops->commit_rqs || !blk_queue_nonrot(q))) {
    /*
     * Use plugging if we have a ->commit_rqs() hook as well, as
     * we know the driver uses bd->last in a smart fashion.
     *
     * Use normal plugging if this disk is slow HDD, as sequential
     * IO may benefit a lot from plug merging.
     */
    unsigned int request_count = plug->rq_count;
    struct request *last = NULL;


    if (!request_count)
      trace_block_plug(q);
    else
      last = list_entry_rq(plug->mq_list.prev);


    if (request_count >= blk_plug_max_rq_count(plug) || (last &&
        blk_rq_bytes(last) >= BLK_PLUG_FLUSH_SIZE)) {
      blk_flush_plug_list(plug, false);
      trace_block_plug(q);
    }


    blk_add_rq_to_plug(plug, rq);
  } else if (q->elevator) {
    /* Insert the request at the IO scheduler queue */
    blk_mq_sched_insert_request(rq, false, true, true);
  } else if (plug && !blk_queue_nomerges(q)) {
    /*
     * We do limited plugging. If the bio can be merged, do that.
     * Otherwise the existing request in the plug list will be
     * issued. So the plug list will have one request at most
     * The plug list might get flushed before this. If that happens,
     * the plug list is empty, and same_queue_rq is invalid.
     */
    if (list_empty(&plug->mq_list))
      same_queue_rq = NULL;
    if (same_queue_rq) {
      list_del_init(&same_queue_rq->queuelist);
      plug->rq_count--;
    }
    blk_add_rq_to_plug(plug, rq);
    trace_block_plug(q);


    if (same_queue_rq) {
      data.hctx = same_queue_rq->mq_hctx;
      trace_block_unplug(q, 1, true);
      blk_mq_try_issue_directly(data.hctx, same_queue_rq,
          &cookie);
    }
  } else if ((q->nr_hw_queues > 1 && is_sync) ||
      !data.hctx->dispatch_busy) {
    /*
     * There is no scheduler and we can try to send directly
     * to the hardware.
     */
    blk_mq_try_issue_directly(data.hctx, rq, &cookie);
  } else {
    /* Default case. */
    blk_mq_sched_insert_request(rq, false, true, true);
  }


  if (!hipri)
    return BLK_QC_T_NONE;
  return cookie;
queue_exit:
  blk_queue_exit(q);
  return BLK_QC_T_NONE;
}

为了看得懂这段代码,可能需要补充以下4个知识点(分别对应上面代码中标记※的部分):反弹缓冲区、bio切分、完整性保护和蓄流,为了保证文章整体脉络,我将这四个知识点的整理放在了本文的副推,感兴趣的读者可以去阅读。

在上面代码的34行,我们成功获取了一个request结构,并在48行调用blk_mq_bio_to_request函数使用bio填充了request,观察这个过程:

static void blk_mq_bio_to_request(struct request *rq, struct bio *bio,
    unsigned int nr_segs)
{
  int err;


  if (bio->bi_opf & REQ_RAHEAD)
    rq->cmd_flags |= REQ_FAILFAST_MASK;


  rq->__sector = bio->bi_iter.bi_sector; // 记录了lba
  rq->write_hint = bio->bi_write_hint;
  blk_rq_bio_prep(rq, bio, nr_segs);


  /* This can't fail, since GFP_NOIO includes __GFP_DIRECT_RECLAIM. */
  err = blk_crypto_rq_bio_prep(rq, bio, GFP_NOIO);
  WARN_ON_ONCE(err);


  blk_account_io_start(rq);
}


static inline void blk_rq_bio_prep(struct request *rq, struct bio *bio,
    unsigned int nr_segs)
{
  rq->nr_phys_segments = nr_segs;
  rq->__data_len = bio->bi_iter.bi_size; // 记录了初始size
  rq->bio = rq->biotail = bio;
  rq->ioprio = bio_prio(bio);


  if (bio->bi_bdev)
    rq->rq_disk = bio->bi_bdev->bd_disk;
}

在初始化request的过程中,记录了lba和size的信息以及bio的关联,但是似乎并没有记录下内存中的地址关系。

聚散列表sg

SCSI数据缓冲区组织成聚散列表的形式。聚散列表的基本结构为scatterlist:

struct scatterlist {
  unsigned long  page_link; // 表示地址
  unsigned int  offset; // 如果表示映射页面,此项表示在映射页面内的偏移
  unsigned int  length; // 如果表示映射页面,此项表示在映射页面内的长度
  dma_addr_t  dma_address; // DMA地址
#ifdef CONFIG_NEED_SG_DMA_LENGTH
  unsigned int  dma_length; // DMA长度
#endif
};

一个scatterlist结构对应了一个内存缓冲区或另一个scatterlist地址,聚散列表就是多个scatterlist的组合。这种组合并不是单纯以链表方式来表示,也不是单纯以数组形式来表示,而是数组与链表的结合。scatterlist使用的内存以页面为基本单位分配,每个页面相当于一个scatterlist数组,具有固定项数,因此,由于内存缓冲区和scatterlist数组都是页对齐的,page_link字段的低4字节(页大小)均为0,这就使得我们可以在page_link字段上添加一些特殊的mask来区分page_link字段的含义,于是规定了:

  • 位0表示该scatterlist是否为链接件

  • 位1表示该scatterlist是否为聚散列表的最后一项

通过这样的方式,我们就可以构建出聚散列表,Linux中设计了一个特殊的表头sg_table来表示聚散列表:

struct sg_table {
  struct scatterlist *sgl;  /* the list */
  unsigned int nents;    /* number of mapped entries */
  unsigned int orig_nents;  /* original size of list */
};

它们之间的关系如下:

421271ac29dddfc35ec0cb66a9974531.jpeg

可以猜测,接下来,会有一个环节将bio中的bvec数组映射为sg_table的过程。查证后,这个过程在sd_init_command时被处理:

static blk_status_t sd_init_command(struct scsi_cmnd *cmd)
{
  struct request *rq = scsi_cmd_to_rq(cmd);


  switch (req_op(rq)) {
...
  case REQ_OP_READ:
  case REQ_OP_WRITE:
  case REQ_OP_ZONE_APPEND:
    return sd_setup_read_write_cmnd(cmd);
...
}
static blk_status_t sd_setup_read_write_cmnd(struct scsi_cmnd *cmd)
{
...
  ret = scsi_alloc_sgtables(cmd);
  if (ret != BLK_STS_OK)
    return ret;
...
fail:
  scsi_free_sgtables(cmd);
  return ret;
}
重点关注此函数:
blk_status_t scsi_alloc_sgtables(struct scsi_cmnd *cmd)
{
  struct scsi_device *sdev = cmd->device;
  struct request *rq = scsi_cmd_to_rq(cmd);
  unsigned short nr_segs = blk_rq_nr_phys_segments(rq);
  struct scatterlist *last_sg = NULL;
  blk_status_t ret;
  bool need_drain = scsi_cmd_needs_dma_drain(sdev, rq);
  int count;


  if (WARN_ON_ONCE(!nr_segs))
    return BLK_STS_IOERR;


  /*
   * Make sure there is space for the drain.  The driver must adjust
   * max_hw_segments to be prepared for this.
   * 如果设备有“过剩DMA”问题,需要追加一个抽干缓冲区
   */
  if (need_drain)
    nr_segs++;


  /*
   * If sg table allocation fails, requeue request later.
   * 分配sg_table
   */
  if (unlikely(sg_alloc_table_chained(&cmd->sdb.table, nr_segs,
      cmd->sdb.table.sgl, SCSI_INLINE_SG_CNT)))
    return BLK_STS_RESOURCE;


  /*
   * Next, walk the list, and fill in the addresses and sizes of
   * each segment. 为bvec->sg建立映射
   */
  count = __blk_rq_map_sg(rq->q, rq, cmd->sdb.table.sgl, &last_sg);


  if (blk_rq_bytes(rq) & rq->q->dma_pad_mask) {
    unsigned int pad_len =
      (rq->q->dma_pad_mask & ~blk_rq_bytes(rq)) + 1;


    last_sg->length += pad_len;
    cmd->extra_len += pad_len;
  }


  if (need_drain) {
    sg_unmark_end(last_sg);
    last_sg = sg_next(last_sg);
    sg_set_buf(last_sg, sdev->dma_drain_buf, sdev->dma_drain_len);
    sg_mark_end(last_sg);


    cmd->extra_len += sdev->dma_drain_len;
    count++;
  }


  BUG_ON(count > cmd->sdb.table.nents);
  cmd->sdb.table.nents = count;
  cmd->sdb.length = blk_rq_payload_bytes(rq);


  // 如果包含完整性数据,完整性数据对应一个新的缓冲区,需要为其分配空间并建立映射
  if (blk_integrity_rq(rq)) {
    struct scsi_data_buffer *prot_sdb = cmd->prot_sdb;
    int ivecs;


    if (WARN_ON_ONCE(!prot_sdb)) {
      /*
       * This can happen if someone (e.g. multipath)
       * queues a command to a device on an adapter
       * that does not support DIX.
       */
      ret = BLK_STS_IOERR;
      goto out_free_sgtables;
    }


    ivecs = blk_rq_count_integrity_sg(rq->q, rq->bio);


    if (sg_alloc_table_chained(&prot_sdb->table, ivecs,
        prot_sdb->table.sgl,
        SCSI_INLINE_PROT_SG_CNT)) {
      ret = BLK_STS_RESOURCE;
      goto out_free_sgtables;
    }


    count = blk_rq_map_integrity_sg(rq->q, rq->bio,
            prot_sdb->table.sgl);
    BUG_ON(count > ivecs);
    BUG_ON(count > queue_max_integrity_segments(rq->q));


    cmd->prot_sdb = prot_sdb;
    cmd->prot_sdb->table.nents = count;
  }


  return BLK_STS_OK;
out_free_sgtables:
  scsi_free_sgtables(cmd);
  return ret;
}

在这个函数中,根据不同的情况,将所有的缓冲区准备好,并建立映射。我们主要关注映射过程:

/*
 * map a request to scatterlist, return number of sg entries setup. Caller
 * must make sure sg can hold rq->nr_phys_segments entries
 */
int __blk_rq_map_sg(struct request_queue *q, struct request *rq,
    struct scatterlist *sglist, struct scatterlist **last_sg)
{
  int nsegs = 0;


  if (rq->rq_flags & RQF_SPECIAL_PAYLOAD)
    nsegs = __blk_bvec_map_sg(rq->special_vec, sglist, last_sg);
  else if (rq->bio && bio_op(rq->bio) == REQ_OP_WRITE_SAME)
    nsegs = __blk_bvec_map_sg(bio_iovec(rq->bio), sglist, last_sg);
  else if (rq->bio)
    nsegs = __blk_bios_map_sg(q, rq->bio, sglist, last_sg);


  if (*last_sg)
    sg_mark_end(*last_sg);


  /*
   * Something must have been wrong if the figured number of
   * segment is bigger than number of req's physical segments
   */
  WARN_ON(nsegs > blk_rq_nr_phys_segments(rq));


  return nsegs;
}


static int __blk_bios_map_sg(struct request_queue *q, struct bio *bio,
           struct scatterlist *sglist,
           struct scatterlist **sg)
{
  struct bio_vec bvec, bvprv = { NULL };
  struct bvec_iter iter;
  int nsegs = 0;
  bool new_bio = false;


  for_each_bio(bio) {
    bio_for_each_bvec(bvec, bio, iter) {
      /*
       * Only try to merge bvecs from two bios given we
       * have done bio internal merge when adding pages
       * to bio
       */
      if (new_bio &&
          __blk_segment_map_sg_merge(q, &bvec, &bvprv, sg))
        goto next_bvec;


      if (bvec.bv_offset + bvec.bv_len <= PAGE_SIZE)
        nsegs += __blk_bvec_map_sg(bvec, sglist, sg);
      else
        nsegs += blk_bvec_map_sg(q, &bvec, sglist, sg);
 next_bvec:
      new_bio = false;
    }
    if (likely(bio->bi_iter.bi_size)) {
      bvprv = bvec;
      new_bio = true;
    }
  }


  return nsegs;
}

上面的代码不难理解,将连接在一起的bio的所有bvec映射到sg_table中,观察到第46行对不同bio之间的bvec做了一次尝试合并,这是bio执行过程中第二次合并过程,第一次合并是bio间的合并,将lba连续的bio采用bi_next字段进行了链接,此处是对连续的内存区域的合并,对于不同bio间相邻的内存区域进行合并,可以节省一项scatterlist。

static inline bool
__blk_segment_map_sg_merge(struct request_queue *q, struct bio_vec *bvec,
         struct bio_vec *bvprv, struct scatterlist **sg)
{


  int nbytes = bvec->bv_len;
  // sg为空 无法合并
  if (!*sg)
    return false;
  // sg长度超出最大限制 无法合并
  if ((*sg)->length + nbytes > queue_max_segment_size(q))
    return false;
  // 物理上无法合并 1.不相邻 2.跨越硬件边界 
  if (!biovec_phys_mergeable(q, bvprv, bvec))
    return false;
  // 合并,也就是将长度延长
  (*sg)->length += nbytes;


  return true;
}

两次合并过程如图:

0de29dfb6828cb6159ec64936a666baa.jpeg

在以上过程完成后,cmd->sdb.table中就存放了以sg_table给定的内存区域的位置,接着将会随着scsi_cmnd结构下发到底层驱动。

以mpt3sas底层驱动为例,它的queuecommand函数为scsih_qcmd:

static int
scsih_qcmd(struct Scsi_Host *shost, struct scsi_cmnd *scmd)
{
  struct MPT3SAS_ADAPTER *ioc = shost_priv(shost);
  struct MPT3SAS_DEVICE *sas_device_priv_data;
  struct MPT3SAS_TARGET *sas_target_priv_data;
  struct _raid_device *raid_device;
  struct request *rq = scsi_cmd_to_rq(scmd);
  int class;
  Mpi25SCSIIORequest_t *mpi_request;
  struct _pcie_device *pcie_device = NULL;
  u32 mpi_control;
  u16 smid;
  u16 handle;


  if (ioc->logging_level & MPT_DEBUG_SCSI)
    scsi_print_command(scmd);


  sas_device_priv_data = scmd->device->hostdata;
  if (!sas_device_priv_data || !sas_device_priv_data->sas_target) {
    scmd->result = DID_NO_CONNECT << 16;
    scmd->scsi_done(scmd);
    return 0;
  }


  if (!(_scsih_allow_scmd_to_device(ioc, scmd))) {
    scmd->result = DID_NO_CONNECT << 16;
    scmd->scsi_done(scmd);
    return 0;
  }


  sas_target_priv_data = sas_device_priv_data->sas_target;


  /* invalid device handle */
  handle = sas_target_priv_data->handle;
  if (handle == MPT3SAS_INVALID_DEVICE_HANDLE) {
    scmd->result = DID_NO_CONNECT << 16;
    scmd->scsi_done(scmd);
    return 0;
  }




  if (ioc->shost_recovery || ioc->ioc_link_reset_in_progress) {
    /* host recovery or link resets sent via IOCTLs */
    return SCSI_MLQUEUE_HOST_BUSY;
  } else if (sas_target_priv_data->deleted) {
    /* device has been deleted */
    scmd->result = DID_NO_CONNECT << 16;
    scmd->scsi_done(scmd);
    return 0;
  } else if (sas_target_priv_data->tm_busy ||
       sas_device_priv_data->block) {
    /* device busy with task management */
    return SCSI_MLQUEUE_DEVICE_BUSY;
  }


  /*
   * Bug work around for firmware SATL handling.  The loop
   * is based on atomic operations and ensures consistency
   * since we're lockless at this point
   */
  do {
    if (test_bit(0, &sas_device_priv_data->ata_command_pending))
      return SCSI_MLQUEUE_DEVICE_BUSY;
  } while (_scsih_set_satl_pending(scmd, true));
  // 流式DMA数据传输方向
  if (scmd->sc_data_direction == DMA_FROM_DEVICE)
    mpi_control = MPI2_SCSIIO_CONTROL_READ;
  else if (scmd->sc_data_direction == DMA_TO_DEVICE)
    mpi_control = MPI2_SCSIIO_CONTROL_WRITE;
  else
    mpi_control = MPI2_SCSIIO_CONTROL_NODATATRANSFER;


  /* set tags */
  mpi_control |= MPI2_SCSIIO_CONTROL_SIMPLEQ;
  /* NCQ Prio supported, make sure control indicated high priority */
  if (sas_device_priv_data->ncq_prio_enable) {
    class = IOPRIO_PRIO_CLASS(req_get_ioprio(rq));
    if (class == IOPRIO_CLASS_RT)
      mpi_control |= 1 << MPI2_SCSIIO_CONTROL_CMDPRI_SHIFT;
  }
  /* Make sure Device is not raid volume.
   * We do not expose raid functionality to upper layer for warpdrive.
   */
  if (((!ioc->is_warpdrive && !scsih_is_raid(&scmd->device->sdev_gendev))
    && !scsih_is_nvme(&scmd->device->sdev_gendev))
    && sas_is_tlr_enabled(scmd->device) && scmd->cmd_len != 32)
    mpi_control |= MPI2_SCSIIO_CONTROL_TLR_ON;
  // 取一个号
  smid = mpt3sas_base_get_smid_scsiio(ioc, ioc->scsi_io_cb_idx, scmd);
  if (!smid) {
    ioc_err(ioc, "%s: failed obtaining a smid\n", __func__);
    _scsih_set_satl_pending(scmd, false);
    goto out;
  }
  mpi_request = mpt3sas_base_get_msg_frame(ioc, smid); // 取一帧
  memset(mpi_request, 0, ioc->request_sz);
  _scsih_setup_eedp(ioc, scmd, mpi_request);
  // 填充相关字段
  if (scmd->cmd_len == 32)
    mpi_control |= 4 << MPI2_SCSIIO_CONTROL_ADDCDBLEN_SHIFT;
  mpi_request->Function = MPI2_FUNCTION_SCSI_IO_REQUEST;
  if (sas_device_priv_data->sas_target->flags &
      MPT_TARGET_FLAGS_RAID_COMPONENT)
    mpi_request->Function = MPI2_FUNCTION_RAID_SCSI_IO_PASSTHROUGH;
  else
    mpi_request->Function = MPI2_FUNCTION_SCSI_IO_REQUEST;
  mpi_request->DevHandle = cpu_to_le16(handle);
  mpi_request->DataLength = cpu_to_le32(scsi_bufflen(scmd));
  mpi_request->Control = cpu_to_le32(mpi_control);
  mpi_request->IoFlags = cpu_to_le16(scmd->cmd_len);
  mpi_request->MsgFlags = MPI2_SCSIIO_MSGFLAGS_SYSTEM_SENSE_ADDR;
  mpi_request->SenseBufferLength = SCSI_SENSE_BUFFERSIZE;
  mpi_request->SenseBufferLowAddress =
      mpt3sas_base_get_sense_buffer_dma(ioc, smid);
  mpi_request->SGLOffset0 = offsetof(Mpi25SCSIIORequest_t, SGL) / 4;
  int_to_scsilun(sas_device_priv_data->lun, (struct scsi_lun *)
      mpi_request->LUN);
  memcpy(mpi_request->CDB.CDB32, scmd->cmnd, scmd->cmd_len);


  if (mpi_request->DataLength) {
    pcie_device = sas_target_priv_data->pcie_dev;
    if (ioc->build_sg_scmd(ioc, scmd, smid, pcie_device)) {
      mpt3sas_base_free_smid(ioc, smid);
      _scsih_set_satl_pending(scmd, false);
      goto out;
    }
  } else
    ioc->build_zero_len_sge(ioc, &mpi_request->SGL);


  raid_device = sas_target_priv_data->raid_device;
  if (raid_device && raid_device->direct_io_enabled)
    mpt3sas_setup_direct_io(ioc, scmd,
      raid_device, mpi_request);


  if (likely(mpi_request->Function == MPI2_FUNCTION_SCSI_IO_REQUEST)) {
    if (sas_target_priv_data->flags & MPT_TARGET_FASTPATH_IO) {
      mpi_request->IoFlags = cpu_to_le16(scmd->cmd_len |
          MPI25_SCSIIO_IOFLAGS_FAST_PATH);
      ioc->put_smid_fast_path(ioc, smid, handle);
    } else
      ioc->put_smid_scsi_io(ioc, smid,
          le16_to_cpu(mpi_request->DevHandle));
  } else
    ioc->put_smid_default(ioc, smid);
  return 0;


 out:
  return SCSI_MLQUEUE_HOST_BUSY;
}

要能够读懂这段代码,需要了解MPI标准,咱们这里主要大致了解MPI及其对scatterlist的支持。

MPI Overview

Fusion-MPT MPI定义了一个寄存器级传输机制和消息协议。MPI传输机制包括消息队列和系统门铃。消息队列是主机和I/O控制器(IOC)之间的主要通信机制。主机内存中分配的请求消息帧用于标识由IOC执行的I/O操作。这些操作由主机驱动程序排队在请求描述符发送队列中。主机内存中还分配了回复消息帧,用于跟踪I/O操作的完成情况。IOC将这些完成情况排队在回复描述符发送队列中。系统门铃用于配置消息队列、跟踪IOC状态和进行复位管理。目前,传输接口定义为PCIe,但该机制可移植到未来的传输接口。MPI为SCSI Initiator和SCSI Target定义了一个消息协议。消息集中还定义了IOC配置、事件管理、集成RAID管理、固件下载和上传以及诊断等内容。还有用于执行SAS非I/O操作的消息定义。所有请求消息由主机驱动程序在基于主机内存的缓冲区中构建,并通过请求描述符发送队列传输到IOC。所有消息都有关联的回复,由IOC放置在回复描述符发送队列中。

门铃:系统接口门铃寄存器提供了主机系统和IOC之间发送和接收信息的机制。该门铃必须作为一个32位值进行访问。当主机系统向门铃写入数据时,它会中断IOC,以便IOC可以读取该值。系统不能读取它自己写入的相同值。当IOC向门铃写入一个值时,IOC会向主机系统生成可屏蔽中断,然后系统可以读取IOC写入的值。

系统请求消息帧池:在初始化过程中,主机驱动程序必须分配一块系统内存区域作为系统请求消息帧的池。当主机需要向IOC发送请求消息时,它会分配其中一个帧,并在帧中构建请求消息。每个系统请求消息帧由其关联的系统消息标识符(SMID)标识,该标识符是池中的索引。SMID是一个16位值,范围从零到小于系统请求消息帧数量的值。保留了SMID值为零,主机驱动程序不能使用它来发送请求消息。

当主机需要向IOC发送请求消息时,它会分配一个系统请求消息帧,并在该帧中构建请求消息。然后,主机将包含帧的系统消息标识符(SMID)的请求描述符写入IOC的请求描述符发送寄存器。内部中断会在写入新的请求描述符时通知IOC,并且IOC将请求消息传输到本地(Local)请求消息帧。

当主机驱动程序将请求描述符写入请求描述符发送队列时,系统请求消息帧的所有权暂时转移到IOC。在IOC将控制权返还给主机驱动程序之前,主机驱动程序不得修改帧的内容(除了主机上下文区域)或释放相关内存。

结合下图,一次请求处理过程的消息队列模型大致为如下流程,其中蓝色的线表示Host端动作,灰色的线表示IOC端动作:

480d6ef2d2029223a4f6d1077601ef03.png

  1. 主机从未使用的帧池中分配一个系统请求消息帧,并在帧中构建一个MPI请求消息。然后,主机将包含帧的SMID的请求描述符写入请求描述符发送寄存器。(门铃)

  2. IOC收到新的请求消息通知,处理该请求,并准备回复。如果IOC需要发送回复消息,则执行步骤3。如果只需要发布回复描述符,则执行步骤6。

  3. 如果回复空闲队列为空,IOC等待主机将系统回复消息帧地址(SRMFA)放置在队列上。

  4. 当回复空闲队列上有可用的SRMFA时,IOC从队列中读取32位的值。IOC通过将SRMFA与初始化期间提供的SystemReplyAddressHigh值组合,形成系统回复消息帧的地址。

  5. IOC使用DMA将回复消息传输到系统回复消息帧。

  6. IOC将包含请求的SMID的回复描述符发布到回复描述符后置队列。

  7. IOC更新其内部的Reply Post IOC Index。

  8. 因为IOC的Reply Post IOC Index不再等于Reply Post Host Index,IOC在主机中断状态寄存器中设置Reply Descriptor Interrupt位,通知主机有一个回复描述符可用。除非主机选择屏蔽这些中断,否则会向主机驱动程序生成一个中断。(门铃)

  9. 主机从回复描述符后置队列中读取新的条目,并在检查Reply Descriptor Type字段不等于MPI2_RPY_DESCRIPT_FLAGS_UNUSED(0xF)且第二个字不等于0xFFFFFFFF后对其进行处理。如果使用系统回复消息帧发送完整的回复消息,则主机将SRMFA写入回复空闲队列上的下一个空闲条目,并将新的索引值写入回复空闲主机索引寄存器,以便再次将帧提供给IOC。

  10. 主机通过将0xFFFFFFFF覆盖描述符来将刚处理的回复描述符标记为未使用。然后,主机将其新的索引值写入回复提交主机索引寄存器。如果新的Reply Post Host Index值与IOC的Reply Post IOC Index匹配,IOC将在主机中断状态寄存器中清除回复描述符中断位。

61e4f1550f064a2de6de2063a48f0747.png

630276682978ad143d40db2494cdaf0e.png

 每个Descriptor都是两个字长,请求内容包含在SMID关联的帧中。

MPI Scatter Gather List

IOC支持两种聚散列表(Scatter Gather List,SGL)格式:MPI和IEEE兼容格式。通过Fusion-MPT PCI消息单元从主机传递给IOC的聚散列表必须始终使用MPI聚散列表格式。IEEE兼容格式仅用于IOC内部使用。聚散列表描述了一个或多个用于DMA操作的内存缓冲区。SGL由一个或多个段组成。段是一个结构化列表,包含一个或多个散射收集元素(Scatter Gather Element,SGE)。MPI格式的SGL有三种元素类型:Simple、Chain和Transaction Context。IEEE格式有两种元素类型:Simple和Chain。

简单元素(Simple Element)是一个地址和长度对,用于描述物理连续的内存块,是最常见的SGE。地址部分可以是32位或64位。如果使用32位地址,则内存块必须位于系统地址空间的最低4 GB内。简单SGE描述的内存块通常是数据缓冲区的一部分。

链式元素(Chain Element)是一个地址和长度对,用于链接分段为多个物理内存位置的SGL。地址部分可以是32位或64位。如果使用32位地址,则内存块必须位于系统地址空间的最低4 GB内。链式元素描述的内存块包含了作为SGL连续部分的其他SGE。SGL中物理连续的每个部分都称为一个段。

段包含一个或多个元素,通常是一个或多个连续的简单元素,后跟一个链式元素。简单元素必须按顺序连续且位于链式元素之前的段内。如果存在链式元素,它必须始终是段的最后一个元素。段不能跨越4 GB边界。SGL中的所有简单元素的地址字段大小必须相同,都是32位地址或64位地址。SGL可以描述多个内存缓冲区。多个简单元素用于描述非物理连续的单个内存缓冲区。

地址字段中的所有位均设置为1的简单元素(例如,低地址=0xFFFFFFFF,高地址=0xFFFFFFFF)表示该元素数据的位存储。此特殊的位存储情况仅适用于用于处理从协议总线传入的数据的简单元素。

对以上的对象MPI均规定了相应的消息格式,这里不罗列,最后一个聚散列表将由若干条消息描述,被组织为如下形式:

bfa28194c0f5936c6870a61f0a5e1b86.png

我们可以看到上述代码最终在123行调用 ioc->build_sg_scmd 方法,实际调用 _base_build_sg_scmd 函数,将聚散列表按照类似上面的格式描述在对应smid帧的对应区域,至此,设备就实际上拥有了聚散列表和lba的映射关系,完成了数据的映射。

ed8ec36a4a91c7f1b90807c5f432a3ad.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1104648.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MyBatis源码初始

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; MyBatis源码解读 ✨特色专栏&#xff…

首饰上亚马逊合规认证RSL报告

首饰 首饰是一种古老而又流行的装饰品&#xff0c;它们不仅可以点缀女性的美丽&#xff0c;还可以表达个人的品味和风格。首饰的种类繁多&#xff0c;有耳环、项链、手镯、戒指等&#xff0c;每一种都有自己的特点。 随着人们对珠宝首饰的要求越来越高&#xff0c;为了确保珠宝…

uni-app小程序使用DCloud(插件市场)流程

一、DCloud&#xff08;插件市场&#xff09; DCloud 是uni-app官方插件市场&#xff0c;里面有官方、团队、个人发布的众多插件&#xff0c;包括uni-ui、uni-pay 等。而像uni-ui这种大型组件库都有官方文档可参考&#xff0c;但一些团队或个人发布的小型插件没有文档&#xf…

Python调用c++生成的dll

Python调用c生成的dll 1.简单例子1.1 vs2019 c生成dll1.2 Python端调用 2.调用c类生成的dll2.1 vs cpp端生成dll2.2 Python端调用 参考文献 1.简单例子 1.1 vs2019 c生成dll 项目中添加add.cpp文件 extern "C" int __declspec(dllexport) add(int x, int y) {retu…

SpringBoot + Disruptor 实现特快高并发处理,支撑每秒 600 万订单无压力!

背景 工作中遇到项目使用Disruptor做消息队列&#xff0c;对你没看错&#xff0c;不是Kafka也不是rabbitmq。Disruptor有个最大的优点就是快&#xff0c;还有一点它是开源的哦&#xff0c;下面做个简单的记录。 Disruptor介绍 Disruptor 是英国外汇交易公司LMAX开发的一个高…

中国式复杂报表制作工具及技巧,解决90%效率问题

最大的数据杀手——中国式复杂报表 作为资料人&#xff0c;在日常生活和工作之中&#xff0c;我们是否经常被要求制作如下图所示的一些数据统计表格&#xff1a; 总的来看&#xff0c;很多人经常发现自己虽然有数据&#xff0c;却不知道用什么图表来进行数据最佳形式的价值表…

「我在淘天做技术」一篇文章告诉你商品团队在做哪些有意思的事?

作者:许令波(君山) 近期淘天集团秋季 2024 届校园招聘正式启动&#xff0c;预计将发放 2000 多个 offer&#xff0c;其中技术类岗位占比超过 50%。为了方便大家更真实地了解淘天技术的布局和现状&#xff0c;我们策划了「我在淘天做技术」系列&#xff0c;首次全面分享淘天技术…

【限时优惠】RHCE9.0培训考证-红帽官方授权中心

【微|信|公|众|号&#xff1a;厦门微思网络】 官网&#xff1a; www.xmws.cn 相信关注红帽认证的小伙伴都已经知道了&#xff1a;2022 年 5 月 18 日&#xff0c;红帽公司宣布推出红帽企业 Linux 9 (RHCE 9)&#xff0c;这是世界领先的企业 Linux 平台的最新版本。 特别提醒的是…

怎样成功部署CRM销售管理系统?

部署CRM销售管理系统可以是自上而下的落实&#xff0c;也可以自下而上让基层员工提出他们的建议&#xff0c;毕竟他们才是系统的使用者。成功部署CRM销售管理系统离不开以下几点要素&#xff1a; 1、全渠道沟通 在通讯技术发达的今天&#xff0c;人们可以在任何地方进行视频通…

Unity3D Shader新手入门教程:3D溶解与腐蚀特效详解

引言 在游戏开发中&#xff0c;特效是非常重要的一部分&#xff0c;它能够增加游戏的趣味性和可玩性。其中&#xff0c;Shader特效是一种非常常见和常用的特效&#xff0c;它能够通过改变物体表面的渲染方式来实现各种各样的特效效果。本文将详细介绍Unity3D中的Shader 3D溶解与…

华为云应用中间件DCS系列—Redis实现(电商网站)秒杀抢购示例

云服务、API、SDK&#xff0c;调试&#xff0c;查看&#xff0c;我都行 阅读短文您可以学习到&#xff1a;应用中间件系列之Redis实现&#xff08;电商网站&#xff09;秒杀抢购示例 1 什么是DEVKIT 华为云开发者插件&#xff08;Huawei Cloud Toolkit&#xff09;&…

游戏设计模式专栏(十二):在Cocos游戏开发中运用代理模式

点击上方亿元程序员关注和★星标 引言 大家好&#xff0c;我是亿元程序员&#xff0c;一位有着8年游戏行业经验的主程。 本系列是《和8年游戏主程一起学习设计模式》&#xff0c;让糟糕的代码在潜移默化中升华&#xff0c;欢迎大家关注分享收藏订阅。 代理模式&#xff08…

【Tomcat】为Tomcat服务配置本地Apr库以提升性能

关于 apr 和 apr-util 对 Tomcat 服务的性能提升的说明&#xff1a; 要测APR给tomcat带来的好处最好的方法是在慢速网络上&#xff08;模拟Internet&#xff09;&#xff0c;将Tomcat线程数开到300以上的水平&#xff0c;然后模拟一大堆并发请求。如果不配APR&#xff0c;基本…

el-pagination怎么修改样式,分页修改样式

/* 分页距离右边20&#xff0c;距离底边20 */ .pagination-container .el-pagination{position:absolute;right:20px;bottom:20px;} 自己写一个分页组件&#xff0c;用到绝对定位和相对定位

Cornerstone for Mac:高效SVN管理的黄金标准

在当今的软件开发领域&#xff0c;版本控制系统是不可或缺的一部分。其中&#xff0c;Subversion&#xff08;SVN&#xff09;是一个广泛使用的版本控制系统&#xff0c;有助于团队协同工作&#xff0c;实现代码的版本管理和追踪。对于Mac用户来说&#xff0c;Cornerstone是一款…

PLC寄存器基础知识

这篇博客介绍的内容其实是微机原理的相关知识&#xff0c;如果没有修过微机原理&#xff0c;可以找相关书籍看一看&#xff0c;众所知周PLC也是属于微控制器。下面我们看下西门子PLC常用的寄存器地址关系。 1、西门子寄存器地址关系 待续......

togaf入门介绍

TOGAF标准是一个开放的、行业共识的企业架构框架。 它是一个基础框架&#xff0c;这意味着它适用于任何环境下的任何类型的架构的开发。这一基础框架是由The Open Group TOGAF补充的库&#xff0c;该库是一个广泛和不断增长的指导材料组合&#xff0c;为在具体情况下应用TOGAF …

SAP S4 BAPI更新BP税号类型CN0自动覆盖CN5

BAPI更新BP税号类型CN0自动覆盖CN5 使用cl_md_bp_maintain>maintain更新BP税号CN0的数据&#xff0c;更新结果都会变成CN5类型&#xff0c;CN1类型一切正常。 1、BP税号 2、跟踪方法中代码 查看底层逻辑&#xff0c;发现CN0都被强制替换成CN5了&#xff0c;BP GUI界面还能…

MultiPlayerShoot----C++学习记录01打包测试项目

首先将多人游戏插件plug文件夹移至项目目录 打开config/DefaultEngine.ini&#xff08;5.0的虚幻引擎内容略不一样&#xff09;和Game.ini对里边的内容进行编辑。 DefaultEngine.ini [/Script/Engine.GameEngine] NetDriverDefinitions(DefName"GameNetDriver",Driv…

win 10怎么录屏?教你轻松捕捉屏幕活动

在当今科技快速发展的时代&#xff0c;录屏已成为信息分享、教学、游戏直播等方面的重要工具。无论是为了制作教程、分享游戏过程还是保存重要信息&#xff0c;录屏功能都发挥着举足轻重的作用。可是很多人不知道win 10怎么录屏&#xff0c;本文将详细介绍win10的三种常用录屏方…