以下函数是prp相关的源码。
/*
* prp模型,除了第一个dma addr不是page_size对齐的
其余的dma addr都要求是page_size对齐的
*/
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;
__le64 *prp_list;
dma_addr_t prp_dma;
int nprps, i;
int length = blk_rq_payload_bytes(req);//request的数据总长度, 假设 5000
struct scatterlist *sg = iod->sg; //数据都映射在这里了
int dma_len = sg_dma_len(sg); //dma mapping以后的数据长度 假设 4096
u64 dma_addr = sg_dma_address(sg); //假设4096对齐或者不对齐,也就是有offset的情况
u32 page_size = dev->ctrl.page_size; //这个假设是4096
int offset = dma_addr & (page_size - 1);//求offset的方法,比较巧妙,根据这个offset可以知道一个sge要传输的数据长度(在确定page大小的情况下)
void **list = nvme_pci_iod_list(req);
length -= (page_size - offset);//判断1个prp是否满足要求 length = length - (page_size - offset) = 5000 - 4096
if (length <= 0) {
iod->first_dma = 0;
goto done;
}
dma_len -= (page_size - offset); //dma_len = dma_len - (page_size - offset) = 4096 - 4096 = 0
if (dma_len) {
dma_addr += (page_size - offset); //dma_addr = dma_addr + (page_size - offset)
} else {
sg = sg_next(sg); //如果走这个分支说明 dma_len == (page_size - offset)
dma_addr = sg_dma_address(sg);
dma_len = sg_dma_len(sg);
}
//这个成立说明只需要两个prp就可以了 不需要list
if (length <= page_size) {
iod->first_dma = dma_addr;
goto done;
}
nprps = DIV_ROUND_UP(length, page_size); // nprps = int(length / page_size) + 1
if (nprps <= (256 / 8)) { //nprps < 32
pool = dev->prp_small_pool; //256 / 8 = 32
iod->npages = 0;
} else {
pool = dev->prp_page_pool; //4096 / 8 = 512
iod->npages = 1; //标记是使用哪一个pool
}
prp_list = dma_pool_alloc(pool, GFP_ATOMIC, &prp_dma);
if (!prp_list) {
iod->first_dma = dma_addr;
iod->npages = -1;
return BLK_STS_RESOURCE;
}
list[0] = prp_list; //这个类似 nvme_pci_iod_list(req)[0] = prp_list 记录的是虚拟地址;
iod->first_dma = prp_dma; //记录物理地址
i = 0;
for (;;) {
if (i == page_size >> 3) { //4096 >> 3 == 512, 只有pool是prp_page_pool才会进入这个分支,当然还要保证iod->sg映射的物理段足够多
__le64 *old_prp_list = prp_list;
prp_list = dma_pool_alloc(pool, GFP_ATOMIC, &prp_dma);//之前的记录满了,所以再次申请
if (!prp_list)
return BLK_STS_RESOURCE;
list[iod->npages++] = 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); //prp_list里面记录的都是地址,记录一个需要8个字节
dma_len -= page_size;
dma_addr += page_size;
length -= page_size;
//这个成立说明数据已经全部转换完毕了
if (length <= 0)
break;
if (dma_len > 0)
continue;
/*说明了sg_dma_address(sg)转换出来的地址不一定就是page_size对齐的,
但是dma_len一定要是page_size的倍数。也就是offset为0
*/
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->sg)); //主要就是填这两个prp
cmnd->dptr.prp2 = cpu_to_le64(iod->first_dma);//第二个prp记录数据或者下一个dma pool的起始地址
return BLK_STS_OK;
bad_sgl:
WARN(DO_ONCE(nvme_print_sgl, iod->sg, iod->nents), "Invalid SGL for payload:%d nents:%d\n", blk_rq_payload_bytes(req), iod->nents);
return BLK_STS_IOERR;
}
一般来说,如果sge只有1个时,那么只需要在下发的nvme rw命令时填写prp1字段的值,这个值是64bit的,所以这个64bit的字段即需要有dma的地址,也需要有传输的长度,这个64bit所以它需要一些特定的bit记录地址,一些特定的bit记录长度,同理prp2也是。
但是如果sge段比较多,靠prp1和prp2就不够了,所以需要更多的prp,这个时候prp2里记录的地址就是prp_list的起始地址了,但是,prp_list有个限制条件,就是它的每个64位的bit记录的都是地址,而没有长度,长度都是nvme 的page_size大小。
从上面的分析当中,有两个疑问?
1:怎么知道prp2记录的是数据的地址还是prp_list的起始地址?
2:需要多个prp时,因为prp_list是只记录地址的,但是有时候一个IO请求时,数据量没那么巧怎么办?也就是prp_list最后元素记录的地址传输的数据量不是一个page_size?
关于第二点,在nvme_map_data函数里有这么一段代码,metadata有特殊用途?
//这个if语句是为metadata做map操作,然后把dma地址给到cmnd->rw.metadata成员 看起来数据量应该不是太大
if (blk_integrity_rq(req)) {
if (blk_rq_count_integrity_sg(q, req->bio) != 1)
goto out_unmap;
sg_init_table(&iod->meta_sg, 1);
if (blk_rq_map_integrity_sg(q, req->bio, &iod->meta_sg) != 1)
goto out_unmap;
if (!dma_map_sg(dev->dev, &iod->meta_sg, 1, dma_dir))
goto out_unmap;
}
if (blk_integrity_rq(req))
cmnd->rw.metadata = cpu_to_le64(sg_dma_address(&iod->meta_sg));
return BLK_STS_OK;
第一点,如果有知道的同仁可以留给言。
最后看一下,关于选择sgl还是prp的依据。
static inline bool nvme_pci_use_sgls(struct nvme_dev *dev, struct request *req)
{
struct nvme_iod *iod = blk_mq_rq_to_pdu(req);
int nseg = blk_rq_nr_phys_segments(req);
unsigned int avg_seg_size;
if (nseg == 0)
return false;
avg_seg_size = DIV_ROUND_UP(blk_rq_payload_bytes(req), nseg); //int(blk_rq_payload_bytes(req)/nseg) + 1
if (!(dev->ctrl.sgls & ((1 << 0) | (1 << 1)))) //判断sgls的第0第1个bit是否为1
return false;
if (!iod->nvmeq->qid) //这个是管理队列
return false;
if (!sgl_threshold || avg_seg_size < sgl_threshold)//数据量比较小时使用prp
return false;
return true;
}