最近遇到个这么一个事,查阅SCSI手册,对于READ(10)命令,似乎在采用DMA的情况下,下发的SCSI命令中,并没有内存空间的地址和长度:
也就是说,单纯从这一个命令中,我们并不知道将数据从硬盘的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 */
};
以上各个结构间的关系如下图:
单个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 */
};
它们之间的关系如下:
可以猜测,接下来,会有一个环节将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;
}
两次合并过程如图:
在以上过程完成后,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端动作:
主机从未使用的帧池中分配一个系统请求消息帧,并在帧中构建一个MPI请求消息。然后,主机将包含帧的SMID的请求描述符写入请求描述符发送寄存器。(门铃)
IOC收到新的请求消息通知,处理该请求,并准备回复。如果IOC需要发送回复消息,则执行步骤3。如果只需要发布回复描述符,则执行步骤6。
如果回复空闲队列为空,IOC等待主机将系统回复消息帧地址(SRMFA)放置在队列上。
当回复空闲队列上有可用的SRMFA时,IOC从队列中读取32位的值。IOC通过将SRMFA与初始化期间提供的SystemReplyAddressHigh值组合,形成系统回复消息帧的地址。
IOC使用DMA将回复消息传输到系统回复消息帧。
IOC将包含请求的SMID的回复描述符发布到回复描述符后置队列。
IOC更新其内部的Reply Post IOC Index。
因为IOC的Reply Post IOC Index不再等于Reply Post Host Index,IOC在主机中断状态寄存器中设置Reply Descriptor Interrupt位,通知主机有一个回复描述符可用。除非主机选择屏蔽这些中断,否则会向主机驱动程序生成一个中断。(门铃)
主机从回复描述符后置队列中读取新的条目,并在检查Reply Descriptor Type字段不等于MPI2_RPY_DESCRIPT_FLAGS_UNUSED(0xF)且第二个字不等于0xFFFFFFFF后对其进行处理。如果使用系统回复消息帧发送完整的回复消息,则主机将SRMFA写入回复空闲队列上的下一个空闲条目,并将新的索引值写入回复空闲主机索引寄存器,以便再次将帧提供给IOC。
主机通过将0xFFFFFFFF覆盖描述符来将刚处理的回复描述符标记为未使用。然后,主机将其新的索引值写入回复提交主机索引寄存器。如果新的Reply Post Host Index值与IOC的Reply Post IOC Index匹配,IOC将在主机中断状态寄存器中清除回复描述符中断位。
每个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均规定了相应的消息格式,这里不罗列,最后一个聚散列表将由若干条消息描述,被组织为如下形式:
我们可以看到上述代码最终在123行调用 ioc->build_sg_scmd 方法,实际调用 _base_build_sg_scmd 函数,将聚散列表按照类似上面的格式描述在对应smid帧的对应区域,至此,设备就实际上拥有了聚散列表和lba的映射关系,完成了数据的映射。