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 设备的驱动中,可能会使用多个中断向量来处理不同的队列,以提高并发性能。
函数逻辑如下:
-
参数
dev
是 NVMe 设备的指针,表示正在操作的 NVMe 设备。 -
如果设备拥有多个中断向量(
dev->num_vecs > 1
),则 admin 队列(队列索引为0)的中断向量会使其他队列的中断向量索引偏移1。 -
如果设备只有一个中断向量(
dev->num_vecs <= 1
),则不需要偏移,返回0。 -
返回计算得到的偏移量。
这个函数的作用是在设置队列中断时,根据设备的中断向量配置情况,确定每个队列应该使用的中断向量索引。这可以帮助在多队列情况下,将中断负载均衡到不同的 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 核心上,从而提高系统的性能。
函数逻辑如下:
-
参数
set
是一个blk_mq_tag_set
结构,表示块多队列的标签集,包含有关队列的信息。 -
通过
set
的driver_data
字段获取 NVMe 设备的指针dev
。 -
初始化变量
i
和qoff
,分别表示循环索引和队列的偏移。 -
对于每个映射(
map
)进行以下操作:- 设置
map
的队列数目为对应的队列数目(dev->io_queues[i]
),如果队列数为0则跳过(poll 队列)。 - 设置
map
的队列偏移为qoff
。 - 如果不是 poll 队列并且存在中断偏移(
offset
),则使用blk_mq_pci_map_queues
函数将队列映射到指定的 CPU 核心和中断,否则使用blk_mq_map_queues
函数进行默认的队列映射。 - 更新
qoff
和offset
。
- 设置
通过这个函数,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 控制器已经准备好了新的命令。
函数逻辑如下:
-
参数
nvmeq
是一个指向 NVMe 队列的指针,这个队列是一个提交队列(SQ)。 -
参数
write_sq
是一个布尔值,表示是否要写入提交队列的 doorbell。如果为true
,则表示需要强制写入;如果为false
,则只在下一个命令将要进入队列时才会写入。 -
如果
write_sq
为false
,说明只在下一个命令将要进入队列时才写入 doorbell。检查下一个尾指针是否等于队列深度,如果等于,则将下一个尾指针设置为0,然后检查下一个尾指针是否和上一个尾指针相同,如果相同,则直接返回,不进行写入。 -
如果需要写入 doorbell,调用
nvme_dbbuf_update_and_check_event
函数更新 doorbell 并检查是否需要触发事件。 -
如果需要更新 doorbell 并触发事件,则调用
writel
函数写入提交队列的尾指针值到队列的 doorbell 寄存器。 -
更新提交队列的
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)中。
函数逻辑如下:
-
参数
nvmeq
是一个指向 NVMe 队列的指针,这个队列是一个提交队列(SQ)。 -
参数
cmd
是一个指向要拷贝的 NVMe 命令的指针。 -
使用
memcpy
函数将cmd
指向的命令数据拷贝到提交队列中的特定位置。位置计算是根据当前队列尾指针sq_tail
和队列元素大小sqes
得到的。这个计算确保命令被拷贝到正确的位置。 -
增加
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)中等待处理。
函数的逻辑如下:
-
参数
hctx
是一个指向块多队列(Block Multi-Queue)硬件上下文的指针,它包含了有关队列的信息。 -
从硬件上下文中获取提交队列
nvmeq
,它是一个指向 NVMe 队列的指针,这个队列是一个提交队列(SQ)。 -
获取提交队列的自旋锁,确保在更新队列状态时不会被其他线程干扰。
-
检查
nvmeq->sq_tail
是否等于nvmeq->last_sq_tail
。如果相等,说明队列中有新的请求需要提交到提交队列中。 -
调用
nvme_write_sq_db
函数,将提交队列的尾指针写入到门铃寄存器中,通知控制器有新的命令需要处理。参数write_sq
被设置为true
,以确保门铃寄存器总是被更新。 -
解锁提交队列,允许其他线程访问队列。
总的来说,这个函数用于将请求从请求队列提交到提交队列,并触发门铃操作,通知 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 可以在一个命令中传输多个散列段,以提高数据传输的效率。
函数的逻辑如下:
-
参数
dev
是一个指向 NVMe 设备的指针,它包含了设备的信息。 -
参数
req
是一个指向块请求的指针,它表示一个需要进行数据传输的请求。 -
参数
nseg
表示请求中的散列段数量。 -
计算平均散列段大小
avg_seg_size
,它是请求的有效载荷字节数除以散列段数量。 -
使用函数
nvme_ctrl_sgl_supported
检查 NVMe 控制器是否支持散列段列表(SGLs)。 -
使用
nvmeq->qid
检查请求是否属于队列中的非管理员队列。如果请求属于管理员队列,返回false
,因为管理员队列通常不使用 SGLs。 -
使用
sgl_threshold
和avg_seg_size
比较,如果sgl_threshold
为 0 或平均散列段大小小于sgl_threshold
,则返回false
,表示不使用 SGLs。 -
如果以上所有条件都满足,返回
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 列表用于指示数据的物理内存位置。
函数的逻辑如下:
-
参数
dev
是一个指向 NVMe 设备的指针,它包含了设备的信息。 -
参数
req
是一个指向块请求的指针,它表示一个需要进行数据传输的请求。 -
从请求的私有数据单元(pdu)中获取指向
nvme_iod
结构体的指针,该结构体描述了请求的数据信息。 -
使用循环遍历请求中的每个 PRP 列表,并释放它们占用的内存。
-
在循环中,首先获取 PRP 列表中的最后一个元素,这个元素保存了下一个 PRP 列表的物理地址。
-
使用
dma_pool_free
函数释放当前 PRP 列表占用的内存,并传递列表的起始物理地址作为参数。 -
更新
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 请求中的数据缓冲区,以及释放相关的内存资源。
函数的逻辑如下:
-
参数
dev
是一个指向 NVMe 设备的指针,它包含了设备的信息。 -
参数
req
是一个指向块请求的指针,它表示一个需要进行数据传输的请求。 -
从请求的私有数据单元(pdu)中获取指向
nvme_iod
结构体的指针,该结构体描述了请求的数据信息。 -
如果
iod->dma_len
大于 0,表示请求使用了一块连续的内存块作为数据缓冲区,使用dma_unmap_page
函数取消映射该内存块。 -
如果
iod->dma_len
为 0,表示请求使用了散射/聚集(scatter/gather)数据缓冲区,使用dma_unmap_sgtable
函数取消映射请求中的散射/聚集列表。 -
如果
iod->nr_allocations
等于 0,表示只使用了一个 PRP 小内存块,使用dma_pool_free
函数释放小内存块的内存,并传递对应的物理地址。 -
如果
iod->nr_allocations
等于 1,表示使用了一个 PRP 页面内存块,同样使用dma_pool_free
函数释放页面内存块的内存,并传递对应的物理地址。 -
如果
iod->nr_allocations
大于 1,表示使用了多个 PRP 列表,调用函数nvme_free_prps
来释放所有 PRP 列表的内存。 -
最后,使用
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)的信息,以便于调试和查看列表中各个散射段的物理地址、偏移、长度等信息。
函数的逻辑如下:
-
参数
sgl
是一个指向散射/聚集列表的指针,表示一个包含多个散射段的列表。 -
参数
nents
表示散射/聚集列表中的散射段数量。 -
使用
for_each_sg
宏遍历散射/聚集列表中的每个散射段。 -
在循环内,获取当前散射段的物理地址(使用
sg_phys
函数)、偏移、长度,以及 DMA 地址和 DMA 长度(使用sg_dma_address
和sg_dma_len
函数)。 -
使用
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 是一种描述散射/聚集数据的方式,用于构建存储命令的数据传输描述符。
以下是代码的主要逻辑:
-
函数
nvme_pci_setup_prps
接受一个 NVMe 设备结构dev
、一个请求结构req
以及一个 NVMe 读写命令结构cmnd
。 -
通过
blk_mq_rq_to_pdu
函数将请求结构req
转换为struct nvme_iod
结构iod
,以访问请求的相关信息。 -
从
iod->sgt.sgl
获取散射/聚集列表的第一个散射段,并从其中获取 DMA 地址和 DMA 长度。 -
计算请求数据的长度,并根据偏移对 DMA 地址进行调整,以便在 PRP 中指向数据的起始位置。
-
如果数据长度小于等于 NVMe 控制器页大小,将
iod->first_dma
设置为 DMA 地址,并返回。 -
计算需要多少个 PRP 来描述数据。如果 PRP 的数量小于等于 256/8(即 32),使用小型 DMA 池;否则,使用大型 DMA 池。
-
分配第一个 PRP 列表,并设置
iod->list[0].prp_list
和iod->first_dma
。 -
使用循环迭代来填充 PRP 列表。如果列表已满,分配一个新的 PRP 列表,将上一个列表的最后一个项设置为新列表的 DMA 地址,并将当前 DMA 地址添加到新列表中。
-
在循环结束后,根据最后一个 PRP 列表的情况,填写
cmnd->dptr.prp1
和cmnd->dptr.prp2
,这些字段表示数据的 PRP 描述符。 -
如果出现分配资源失败或无效 SGL 等情况,函数会相应地返回适当的错误状态(
BLK_STS_RESOURCE
或BLK_STS_IOERR
),并在某些情况下触发警告。
该函数主要用于在 NVMe 驱动中构建 PRP 列表,以便于传输读写数据。