NVMe驱动解析-DMA传输

DMA技术是一项比较古老的技术,大部分的处理器都附带这个功能。通过DMA引擎,在CPU不用参与的情况下,数据就能够从一个地址传输到另一个地址。这在进行大量数据搬移的情况下,能够大大降低CPU的使用率。

PCIe有个寄存器位Bus Master Enable。这个bit置1后,PCIe设备就可以向Host发送DMA Read Memory和DMA Write Memory请求了。当Host收到请求后,根据请求中包含的内存地址,通过DMA引擎对该地址进行读写操作,再通过TLP发送或者接收数据。

当Host的driver需要跟PCIe设备传输数据的时候,只需要告诉PCIe设备存放数据的地址就可以了,下面将介绍NVMe是如何使用DMA传输NVMe Command的。

先回顾下之前文章提到的内容,一是NVMe Command占用64个字节,二是NVMe的PCIe BAR空间被映射到虚拟内存空间(其中包括用来通知NVMe SSD Controller读取Command的Doorbell寄存器)。另外,提一下NVMe数据传输的方式,NVMe的数据传输都是通过NVMe Command,而NVMe Command则存放在NVMe Queue中,NVMe Queue一般按照下图方法配置。

NVMe Command的DMA地址分配

NVMe驱动中分配NVMe queue的函数nvme_alloc_queue(),其中用来存放Completion Command( nvmeq->cqes)和Submit Command ( nvmeq->sq_cmds )的地址都是通过内核函数dma_alloc_coherent()分配的。这里有必要介绍下,DMA传输地址必须是物理连续的,通过dma_alloc_coherent()分配的内存能够满足这个要求,而kmalloc()则不能。dma_alloc_coherent()的第二个参数是指定分配的空间,Submit Command 指定的是SQ_SIZE(depth),意思是分配depth个Submit Command的连续空间,所以一个Queue只能放depth个Command。第三个参数是存放实际的DMA地址,这个地址就是需要告诉PCIe设备的;与其对应的是函数的返回值nvmeq->sq_cmds ,这个值是DMA地址转换成内核线程空间的地址值,驱动会向这个地址写数据。那么整个过程是这样:驱动获得地址nvmeq->sq_cmds ,当上层传入Command后,将Command写入nvmeq->sq_cmds[i*64Bytes](i表示第n个Command,n不大于depth),然后通过Doorbell告诉SSD Controller 这个i值,之后Controller通过i就可以算出要取得数据的DMA地址了(nvmeq->cq_dma_addr[i*64Bytes])。

1023 static struct nvme_queue *nvme_alloc_queue(struct nvme_dev *dev, int qid,
1024                                                         int depth, int vector)
1025 {
1026         struct device *dmadev = &dev->pci_dev->dev;
1027         unsigned extra = DIV_ROUND_UP(depth, 8) + (depth *
1028                                                 sizeof(struct nvme_cmd_info));
1029         struct nvme_queue *nvmeq = kzalloc(sizeof(*nvmeq) + extra, GFP_KERNEL);
1030         if (!nvmeq)
1031                 return NULL;
1032 
1033         nvmeq->cqes = dma_alloc_coherent(dmadev, CQ_SIZE(depth),
1034                                         &nvmeq->cq_dma_addr, GFP_KERNEL);
1035         if (!nvmeq->cqes)
1036                 goto free_nvmeq;
1037         memset((void *)nvmeq->cqes, 0, CQ_SIZE(depth));
1038 
1039         nvmeq->sq_cmds = dma_alloc_coherent(dmadev, SQ_SIZE(depth),
1040                                         &nvmeq->sq_dma_addr, GFP_KERNEL);
1041         if (!nvmeq->sq_cmds)
1042                 goto free_cqdma;
1043 
1044         nvmeq->q_dmadev = dmadev;
1045         nvmeq->dev = dev;
1046         spin_lock_init(&nvmeq->q_lock);
1047         nvmeq->cq_head = 0;
1048         nvmeq->cq_phase = 1;
1049         init_waitqueue_head(&nvmeq->sq_full);
1050         init_waitqueue_entry(&nvmeq->sq_cong_wait, nvme_thread);
1051         bio_list_init(&nvmeq->sq_cong);
1052         nvmeq->q_db = &dev->dbs[qid << (dev->db_stride + 1)];
1053         nvmeq->q_depth = depth;
1054         nvmeq->cq_vector = vector;
1055 
1056         return nvmeq;
1057 
1058  free_cqdma:
1059         dma_free_coherent(dmadev, CQ_SIZE(depth), (void *)nvmeq->cqes,
1060                                                         nvmeq->cq_dma_addr);
1061  free_nvmeq:
1062         kfree(nvmeq);
1063         return NULL;
1064 }

其实,NVMe并不是完全按照上面说的那样,而是使用一种tail, head来表示。Submit Queue中用tail来表示最后一个入队的Command index,而CompletionQueue中用head表示。这样通过比较tail和head就可以知道队列中哪些地址是有Command的。

Host如何告诉SSD Controller DMA地址

Controller需要知道DMA基地址后,才能算出某个index对应的Command地址。那么,Host是怎么告诉Controller这个地址的呢?

NVMe协议规定了一个create_sq的Admin Command,Host就是通过向Controller发送这个命令告诉的,其中prp1的值就是前面讲到的nvmeq->sq_dma_addr。Controller收到这个命令后,存下prp1的值即可。同理,competition queue也有一个Admin Command为create_cq。

866 static int adapter_alloc_cq(struct nvme_dev *dev, u16 qid,
867                                                 struct nvme_queue *nvmeq)
868 {
869         int status;
870         struct nvme_command c;
871         int flags = NVME_QUEUE_PHYS_CONTIG | NVME_CQ_IRQ_ENABLED;
872 
873         memset(&c, 0, sizeof(c));
874         c.create_cq.opcode = nvme_admin_create_cq;
875         c.create_cq.prp1 = cpu_to_le64(nvmeq->cq_dma_addr);
876         c.create_cq.cqid = cpu_to_le16(qid);
877         c.create_cq.qsize = cpu_to_le16(nvmeq->q_depth - 1);
878         c.create_cq.cq_flags = cpu_to_le16(flags);
879         c.create_cq.irq_vector = cpu_to_le16(nvmeq->cq_vector);
880 
881         status = nvme_submit_admin_cmd(dev, &c, NULL);
882         if (status)
883                 return -EIO;
884         return 0;
885 }
886 
887 static int adapter_alloc_sq(struct nvme_dev *dev, u16 qid,
888                                                 struct nvme_queue *nvmeq)
889 {
890         int status;
891         struct nvme_command c;
892         int flags = NVME_QUEUE_PHYS_CONTIG | NVME_SQ_PRIO_MEDIUM;
893 
894         memset(&c, 0, sizeof(c));
895         c.create_sq.opcode = nvme_admin_create_sq;
896         c.create_sq.prp1 = cpu_to_le64(nvmeq->sq_dma_addr);
897         c.create_sq.sqid = cpu_to_le16(qid);
898         c.create_sq.qsize = cpu_to_le16(nvmeq->q_depth - 1);
899         c.create_sq.sq_flags = cpu_to_le16(flags);
900         c.create_sq.cqid = cpu_to_le16(qid);
901 
902         status = nvme_submit_admin_cmd(dev, &c, NULL);
903         if (status)
904                 return -EIO;
905         return 0;
906 }
907 

但是,还有一个问题,这个Admin Command是怎么传过去的呢?还是要看NVMe Spec。之前提到的NVMe的BAR空间中就有这么两个寄存器,它们用来存储Admin Queue 的 Command DMA基地址。
如下,在创建Admin Queue的时候就向Controller写入DMA地址:

1155 static int nvme_configure_admin_queue(struct nvme_dev *dev)
1156 {
	...
1182         writeq(nvmeq->sq_dma_addr, &dev->bar->asq);
1183         writeq(nvmeq->cq_dma_addr, &dev->bar->acq);
1184         writel(dev->ctrl_config, &dev->bar->cc);
	...
1200 }

总结

这篇文章主要讲解了NVMe 通过DMA传输NVMe Command的机制,DMA并不是一项新技术,在InfiniBand中也使用。NVMe的优势其实是DMA加上Multi-Queue,并且绕过了Linux Kernel庞大的Block层,下一篇文章将着重介绍NVMe是如何响应I/O Request。

本文作者

张元元是Memblaze SSD事业部应用工程师,研究方向涉及PCIe SSD在VSAN、Docker等环境中的应用及优化。对于服务器虚拟化、NVMe驱动的实现、Linux内核及容器技术有深入的研究。本系列文章为张元元对于NVMe驱动及相关技术的全面解读,更多张元元的文章请关注他的微信公众号:yuan_memblaze


  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值