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
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