NVMe Linux驱动系列一:host端[pci.c]<43>

本文围绕NVMe驱动展开,详细介绍了多个关键函数。如queue_irq_offset用于计算队列中断偏移量,nvme_pci_map_queues将队列映射到CPU核心和中断,还有用于更新提交队列、拷贝命令、提交请求等函数,帮助开发者理解NVMe驱动的工作机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

queue_irq_offset


static int queue_irq_offset(struct nvme_dev *dev)
{
	/* if we have more than 1 vec, admin queue offsets us by 1 */
	if (dev->num_vecs > 1)
		return 1;

	return 0;
}

这个函数 queue_irq_offset 用于计算队列中断的偏移量,即确定每个队列的中断向量索引。在 NVMe 设备的驱动中,可能会使用多个中断向量来处理不同的队列,以提高并发性能。

函数逻辑如下:

  1. 参数 dev 是 NVMe 设备的指针,表示正在操作的 NVMe 设备。

  2. 如果设备拥有多个中断向量(dev->num_vecs > 1),则 admin 队列(队列索引为0)的中断向量会使其他队列的中断向量索引偏移1。

  3. 如果设备只有一个中断向量(dev->num_vecs <= 1),则不需要偏移,返回0。

  4. 返回计算得到的偏移量。

这个函数的作用是在设置队列中断时,根据设备的中断向量配置情况,确定每个队列应该使用的中断向量索引。这可以帮助在多队列情况下,将中断负载均衡到不同的 CPU 核心上,提高系统的性能。

nvme_pci_map_queues


static void nvme_pci_map_queues(struct blk_mq_tag_set *set)
{
	struct nvme_dev *dev = to_nvme_dev(set->driver_data);
	int i, qoff, offset;

	offset = queue_irq_offset(dev);
	for (i = 0, qoff = 0; i < set->nr_maps; i++) {
		struct blk_mq_queue_map *map = &set->map[i];

		map->nr_queues = dev->io_queues[i];
		if (!map->nr_queues) {
			BUG_ON(i == HCTX_TYPE_DEFAULT);
			continue;
		}

		/*
		 * The poll queue(s) doesn't have an IRQ (and hence IRQ
		 * affinity), so use the regular blk-mq cpu mapping
		 */
		map->queue_offset = qoff;
		if (i != HCTX_TYPE_POLL && offset)
			blk_mq_pci_map_queues(map, to_pci_dev(dev->dev), offset);
		else
			blk_mq_map_queues(map);
		qoff += map->nr_queues;
		offset += map->nr_queues;
	}
}

这个函数 nvme_pci_map_queues 用于映射 NVMe 设备的队列到 CPU 核心和中断上。在 NVMe 驱动中,队列映射可以帮助在多队列情况下将队列分配到不同的 CPU 核心上,以及将中断负载均衡到不同的 CPU 核心上,从而提高系统的性能。

函数逻辑如下:

  1. 参数 set 是一个 blk_mq_tag_set 结构,表示块多队列的标签集,包含有关队列的信息。

  2. 通过 setdriver_data 字段获取 NVMe 设备的指针 dev

  3. 初始化变量 iqoff,分别表示循环索引和队列的偏移。

  4. 对于每个映射(map)进行以下操作:

    • 设置 map 的队列数目为对应的队列数目(dev->io_queues[i]),如果队列数为0则跳过(poll 队列)。
    • 设置 map 的队列偏移为 qoff
    • 如果不是 poll 队列并且存在中断偏移(offset),则使用 blk_mq_pci_map_queues 函数将队列映射到指定的 CPU 核心和中断,否则使用 blk_mq_map_queues 函数进行默认的队列映射。
    • 更新 qoffoffset

通过这个函数,NVMe 驱动将设备的各个队列映射到不同的 CPU 核心上,以及将中断分配到不同的 CPU 核心上,以实现更好的性能和负载均衡。

nvme_write_sq_db


/*
 * Write sq tail if we are asked to, or if the next command would wrap.
 */
static inline void nvme_write_sq_db(struct nvme_queue *nvmeq, bool write_sq)
{
	if (!write_sq) {
		u16 next_tail = nvmeq->sq_tail + 1;

		if (next_tail == nvmeq->q_depth)
			next_tail = 0;
		if (next_tail != nvmeq->last_sq_tail)
			return;
	}

	if (nvme_dbbuf_update_and_check_event(nvmeq->sq_tail,
			nvmeq->dbbuf_sq_db, nvmeq->dbbuf_sq_ei))
		writel(nvmeq->sq_tail, nvmeq->q_db);
	nvmeq->last_sq_tail = nvmeq->sq_tail;
}

这个函数 nvme_write_sq_db 用于更新并写入提交队列(Submission Queue,SQ)的 doorbell,通知 NVMe 控制器已经准备好了新的命令。

函数逻辑如下:

  1. 参数 nvmeq 是一个指向 NVMe 队列的指针,这个队列是一个提交队列(SQ)。

  2. 参数 write_sq 是一个布尔值,表示是否要写入提交队列的 doorbell。如果为 true,则表示需要强制写入;如果为 false,则只在下一个命令将要进入队列时才会写入。

  3. 如果 write_sqfalse,说明只在下一个命令将要进入队列时才写入 doorbell。检查下一个尾指针是否等于队列深度,如果等于,则将下一个尾指针设置为0,然后检查下一个尾指针是否和上一个尾指针相同,如果相同,则直接返回,不进行写入。

  4. 如果需要写入 doorbell,调用 nvme_dbbuf_update_and_check_event 函数更新 doorbell 并检查是否需要触发事件。

  5. 如果需要更新 doorbell 并触发事件,则调用 writel 函数写入提交队列的尾指针值到队列的 doorbell 寄存器。

  6. 更新提交队列的 last_sq_tail,表示最近写入的尾指针值。

总的来说,这个函数用于根据条件决定是否更新提交队列的 doorbell,并写入尾指针值,通知 NVMe 控制器有新的命令需要处理。

nvme_sq_copy_cmd


static inline void nvme_sq_copy_cmd(struct nvme_queue *nvmeq,
				    struct nvme_command *cmd)
{
	memcpy(nvmeq->sq_cmds + (nvmeq->sq_tail << nvmeq->sqes),
		absolute_pointer(cmd), sizeof(*cmd));
	if (++nvmeq->sq_tail == nvmeq->q_depth)
		nvmeq->sq_tail = 0;
}

这个函数 nvme_sq_copy_cmd 用于将一个 NVMe 命令拷贝到提交队列(Submission Queue,SQ)中。

函数逻辑如下:

  1. 参数 nvmeq 是一个指向 NVMe 队列的指针,这个队列是一个提交队列(SQ)。

  2. 参数 cmd 是一个指向要拷贝的 NVMe 命令的指针。

  3. 使用 memcpy 函数将 cmd 指向的命令数据拷贝到提交队列中的特定位置。位置计算是根据当前队列尾指针 sq_tail 和队列元素大小 sqes 得到的。这个计算确保命令被拷贝到正确的位置。

  4. 增加 sq_tail 的值,然后检查是否需要回绕。如果 sq_tail 达到队列的最大深度 q_depth,则将其设置为 0,以实现循环队列的效果。

总的来说,这个函数用于将 NVMe 命令拷贝到提交队列中的合适位置,并更新队列的尾指针。这样,命令就被放置在了队列中等待进一步处理。

nvme_commit_rqs


static void nvme_commit_rqs(struct blk_mq_hw_ctx *hctx)
{
	struct nvme_queue *nvmeq = hctx->driver_data;

	spin_lock(&nvmeq->sq_lock);
	if (nvmeq->sq_tail != nvmeq->last_sq_tail)
		nvme_write_sq_db(nvmeq, true);
	spin_unlock(&nvmeq->sq_lock);
}

这个函数 nvme_commit_rqs 用于提交 NVMe 请求队列(Request Queue,RQ)中的请求,将它们添加到提交队列(Submission Queue,SQ)中等待处理。

函数的逻辑如下:

  1. 参数 hctx 是一个指向块多队列(Block Multi-Queue)硬件上下文的指针,它包含了有关队列的信息。

  2. 从硬件上下文中获取提交队列 nvmeq,它是一个指向 NVMe 队列的指针,这个队列是一个提交队列(SQ)。

  3. 获取提交队列的自旋锁,确保在更新队列状态时不会被其他线程干扰。

  4. 检查 nvmeq->sq_tail 是否等于 nvmeq->last_sq_tail。如果相等,说明队列中有新的请求需要提交到提交队列中。

  5. 调用 nvme_write_sq_db 函数,将提交队列的尾指针写入到门铃寄存器中,通知控制器有新的命令需要处理。参数 write_sq 被设置为 true,以确保门铃寄存器总是被更新。

  6. 解锁提交队列,允许其他线程访问队列。

总的来说,这个函数用于将请求从请求队列提交到提交队列,并触发门铃操作,通知 NVMe 控制器有新的命令需要处理。

nvme_pci_use_sgls


static inline bool nvme_pci_use_sgls(struct nvme_dev *dev, struct request *req,
				     int nseg)
{
	struct nvme_queue *nvmeq = req->mq_hctx->driver_data;
	unsigned int avg_seg_size;

	avg_seg_size = DIV_ROUND_UP(blk_rq_payload_bytes(req), nseg);

	if (!nvme_ctrl_sgl_supported(&dev->ctrl))
		return false;
	if (!nvmeq->qid)
		return false;
	if (!sgl_threshold || avg_seg_size < sgl_threshold)
		return false;
	return true;
}

这个函数 nvme_pci_use_sgls 用于判断是否应该在 NVMe 命令中使用散列段列表(Scatter-Gather Lists,SGLs)来传输数据。SGLs 可以在一个命令中传输多个散列段,以提高数据传输的效率。

函数的逻辑如下:

  1. 参数 dev 是一个指向 NVMe 设备的指针,它包含了设备的信息。

  2. 参数 req 是一个指向块请求的指针,它表示一个需要进行数据传输的请求。

  3. 参数 nseg 表示请求中的散列段数量。

  4. 计算平均散列段大小 avg_seg_size,它是请求的有效载荷字节数除以散列段数量。

  5. 使用函数 nvme_ctrl_sgl_supported 检查 NVMe 控制器是否支持散列段列表(SGLs)。

  6. 使用 nvmeq->qid 检查请求是否属于队列中的非管理员队列。如果请求属于管理员队列,返回 false,因为管理员队列通常不使用 SGLs。

  7. 使用 sgl_thresholdavg_seg_size 比较,如果 sgl_threshold 为 0 或平均散列段大小小于 sgl_threshold,则返回 false,表示不使用 SGLs。

  8. 如果以上所有条件都满足,返回 true,表示应该在命令中使用 SGLs 进行数据传输。

总的来说,这个函数用于判断是否应该在 NVMe 命令中使用散列段列表(SGLs)来传输数据,以提高数据传输的效率。

nvme_free_prps


static void nvme_free_prps(struct nvme_dev *dev, struct request *req)
{
	const int last_prp = NVME_CTRL_PAGE_SIZE / sizeof(__le64) - 1;
	struct nvme_iod *iod = blk_mq_rq_to_pdu(req);
	dma_addr_t dma_addr = iod->first_dma;
	int i;

	for (i = 0; i < iod->nr_allocations; i++) {
		__le64 *prp_list = iod->list[i].prp_list;
		dma_addr_t next_dma_addr = le64_to_cpu(prp_list[last_prp]);

		dma_pool_free(dev->prp_page_pool, prp_list, dma_addr);
		dma_addr = next_dma_addr;
	}
}

这个函数 nvme_free_prps 用于释放在 NVMe 请求中使用的物理页指针(PRP)列表。在 NVMe 命令中,PRP 列表用于指示数据的物理内存位置。

函数的逻辑如下:

  1. 参数 dev 是一个指向 NVMe 设备的指针,它包含了设备的信息。

  2. 参数 req 是一个指向块请求的指针,它表示一个需要进行数据传输的请求。

  3. 从请求的私有数据单元(pdu)中获取指向 nvme_iod 结构体的指针,该结构体描述了请求的数据信息。

  4. 使用循环遍历请求中的每个 PRP 列表,并释放它们占用的内存。

  5. 在循环中,首先获取 PRP 列表中的最后一个元素,这个元素保存了下一个 PRP 列表的物理地址。

  6. 使用 dma_pool_free 函数释放当前 PRP 列表占用的内存,并传递列表的起始物理地址作为参数。

  7. 更新 dma_addr 为下一个 PRP 列表的物理地址,以便在下一次迭代时使用。

总的来说,这个函数用于释放在 NVMe 请求中使用的物理页指针(PRP)列表所占用的内存,确保内存资源被正确释放。

nvme_unmap_data



static void nvme_unmap_data(struct nvme_dev *dev, struct request *req)
{
	struct nvme_iod *iod = blk_mq_rq_to_pdu(req);

	if (iod->dma_len) {
		dma_unmap_page(dev->dev, iod->first_dma, iod->dma_len,
			       rq_dma_dir(req));
		return;
	}

	WARN_ON_ONCE(!iod->sgt.nents);

	dma_unmap_sgtable(dev->dev, &iod->sgt, rq_dma_dir(req), 0);

	if (iod->nr_allocations == 0)
		dma_pool_free(dev->prp_small_pool, iod->list[0].sg_list,
			      iod->first_dma);
	else if (iod->nr_allocations == 1)
		dma_pool_free(dev->prp_page_pool, iod->list[0].sg_list,
			      iod->first_dma);
	else
		nvme_free_prps(dev, req);
	mempool_free(iod->sgt.sgl, dev->iod_mempool);
}

这个函数 nvme_unmap_data 用于取消映射 NVMe 请求中的数据缓冲区,以及释放相关的内存资源。

函数的逻辑如下:

  1. 参数 dev 是一个指向 NVMe 设备的指针,它包含了设备的信息。

  2. 参数 req 是一个指向块请求的指针,它表示一个需要进行数据传输的请求。

  3. 从请求的私有数据单元(pdu)中获取指向 nvme_iod 结构体的指针,该结构体描述了请求的数据信息。

  4. 如果 iod->dma_len 大于 0,表示请求使用了一块连续的内存块作为数据缓冲区,使用 dma_unmap_page 函数取消映射该内存块。

  5. 如果 iod->dma_len 为 0,表示请求使用了散射/聚集(scatter/gather)数据缓冲区,使用 dma_unmap_sgtable 函数取消映射请求中的散射/聚集列表。

  6. 如果 iod->nr_allocations 等于 0,表示只使用了一个 PRP 小内存块,使用 dma_pool_free 函数释放小内存块的内存,并传递对应的物理地址。

  7. 如果 iod->nr_allocations 等于 1,表示使用了一个 PRP 页面内存块,同样使用 dma_pool_free 函数释放页面内存块的内存,并传递对应的物理地址。

  8. 如果 iod->nr_allocations 大于 1,表示使用了多个 PRP 列表,调用函数 nvme_free_prps 来释放所有 PRP 列表的内存。

  9. 最后,使用 mempool_free 函数释放请求的散射/聚集列表(scatter/gather list),以及其他内存资源。

总的来说,这个函数用于取消映射 NVMe 请求中的数据缓冲区,并释放相关的内存资源,确保内存映射和资源的正确释放。

nvme_print_sgl


static void nvme_print_sgl(struct scatterlist *sgl, int nents)
{
	int i;
	struct scatterlist *sg;

	for_each_sg(sgl, sg, nents, i) {
		dma_addr_t phys = sg_phys(sg);
		pr_warn("sg[%d] phys_addr:%pad offset:%d length:%d "
			"dma_address:%pad dma_length:%d\n",
			i, &phys, sg->offset, sg->length, &sg_dma_address(sg),
			sg_dma_len(sg));
	}
}

这个函数 nvme_print_sgl 用于打印散射/聚集列表(scatter/gather list)的信息,以便于调试和查看列表中各个散射段的物理地址、偏移、长度等信息。

函数的逻辑如下:

  1. 参数 sgl 是一个指向散射/聚集列表的指针,表示一个包含多个散射段的列表。

  2. 参数 nents 表示散射/聚集列表中的散射段数量。

  3. 使用 for_each_sg 宏遍历散射/聚集列表中的每个散射段。

  4. 在循环内,获取当前散射段的物理地址(使用 sg_phys 函数)、偏移、长度,以及 DMA 地址和 DMA 长度(使用 sg_dma_addresssg_dma_len 函数)。

  5. 使用 pr_warn 函数打印当前散射段的各个信息,包括散射段的索引、物理地址、偏移、长度、DMA 地址和 DMA 长度。

这个函数的目的是帮助开发者在调试过程中查看和理解散射/聚集列表中各个散射段的属性,以便于定位和解决问题。

nvme_pci_setup_prps


static blk_status_t nvme_pci_setup_prps(struct nvme_dev *dev,
		struct request *req, struct nvme_rw_command *cmnd)
{
	struct nvme_iod *iod = blk_mq_rq_to_pdu(req);
	struct dma_pool *pool;
	int length = blk_rq_payload_bytes(req);
	struct scatterlist *sg = iod->sgt.sgl;
	int dma_len = sg_dma_len(sg);
	u64 dma_addr = sg_dma_address(sg);
	int offset = dma_addr & (NVME_CTRL_PAGE_SIZE - 1);
	__le64 *prp_list;
	dma_addr_t prp_dma;
	int nprps, i;

	length -= (NVME_CTRL_PAGE_SIZE - offset);
	if (length <= 0) {
		iod->first_dma = 0;
		goto done;
	}

	dma_len -= (NVME_CTRL_PAGE_SIZE - offset);
	if (dma_len) {
		dma_addr += (NVME_CTRL_PAGE_SIZE - offset);
	} else {
		sg = sg_next(sg);
		dma_addr = sg_dma_address(sg);
		dma_len = sg_dma_len(sg);
	}

	if (length <= NVME_CTRL_PAGE_SIZE) {
		iod->first_dma = dma_addr;
		goto done;
	}

	nprps = DIV_ROUND_UP(length, NVME_CTRL_PAGE_SIZE);
	if (nprps <= (256 / 8)) {
		pool = dev->prp_small_pool;
		iod->nr_allocations = 0;
	} else {
		pool = dev->prp_page_pool;
		iod->nr_allocations = 1;
	}

	prp_list = dma_pool_alloc(pool, GFP_ATOMIC, &prp_dma);
	if (!prp_list) {
		iod->nr_allocations = -1;
		return BLK_STS_RESOURCE;
	}
	iod->list[0].prp_list = prp_list;
	iod->first_dma = prp_dma;
	i = 0;
	for (;;) {
		if (i == NVME_CTRL_PAGE_SIZE >> 3) {
			__le64 *old_prp_list = prp_list;
			prp_list = dma_pool_alloc(pool, GFP_ATOMIC, &prp_dma);
			if (!prp_list)
				goto free_prps;
			iod->list[iod->nr_allocations++].prp_list = prp_list;
			prp_list[0] = old_prp_list[i - 1];
			old_prp_list[i - 1] = cpu_to_le64(prp_dma);
			i = 1;
		}
		prp_list[i++] = cpu_to_le64(dma_addr);
		dma_len -= NVME_CTRL_PAGE_SIZE;
		dma_addr += NVME_CTRL_PAGE_SIZE;
		length -= NVME_CTRL_PAGE_SIZE;
		if (length <= 0)
			break;
		if (dma_len > 0)
			continue;
		if (unlikely(dma_len < 0))
			goto bad_sgl;
		sg = sg_next(sg);
		dma_addr = sg_dma_address(sg);
		dma_len = sg_dma_len(sg);
	}
done:
	cmnd->dptr.prp1 = cpu_to_le64(sg_dma_address(iod->sgt.sgl));
	cmnd->dptr.prp2 = cpu_to_le64(iod->first_dma);
	return BLK_STS_OK;
free_prps:
	nvme_free_prps(dev, req);
	return BLK_STS_RESOURCE;
bad_sgl:
	WARN(DO_ONCE(nvme_print_sgl, iod->sgt.sgl, iod->sgt.nents),
			"Invalid SGL for payload:%d nents:%d\n",
			blk_rq_payload_bytes(req), iod->sgt.nents);
	return BLK_STS_IOERR;
}

这段代码是关于 NVMe 驱动中用于设置 PRP(Physical Region Page)的函数。PRP 是一种描述散射/聚集数据的方式,用于构建存储命令的数据传输描述符。

以下是代码的主要逻辑:

  1. 函数 nvme_pci_setup_prps 接受一个 NVMe 设备结构 dev、一个请求结构 req 以及一个 NVMe 读写命令结构 cmnd

  2. 通过 blk_mq_rq_to_pdu 函数将请求结构 req 转换为 struct nvme_iod 结构 iod,以访问请求的相关信息。

  3. iod->sgt.sgl 获取散射/聚集列表的第一个散射段,并从其中获取 DMA 地址和 DMA 长度。

  4. 计算请求数据的长度,并根据偏移对 DMA 地址进行调整,以便在 PRP 中指向数据的起始位置。

  5. 如果数据长度小于等于 NVMe 控制器页大小,将 iod->first_dma 设置为 DMA 地址,并返回。

  6. 计算需要多少个 PRP 来描述数据。如果 PRP 的数量小于等于 256/8(即 32),使用小型 DMA 池;否则,使用大型 DMA 池。

  7. 分配第一个 PRP 列表,并设置 iod->list[0].prp_listiod->first_dma

  8. 使用循环迭代来填充 PRP 列表。如果列表已满,分配一个新的 PRP 列表,将上一个列表的最后一个项设置为新列表的 DMA 地址,并将当前 DMA 地址添加到新列表中。

  9. 在循环结束后,根据最后一个 PRP 列表的情况,填写 cmnd->dptr.prp1cmnd->dptr.prp2,这些字段表示数据的 PRP 描述符。

  10. 如果出现分配资源失败或无效 SGL 等情况,函数会相应地返回适当的错误状态(BLK_STS_RESOURCEBLK_STS_IOERR),并在某些情况下触发警告。

该函数主要用于在 NVMe 驱动中构建 PRP 列表,以便于传输读写数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值