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
總結
以上是生活随笔為你收集整理的NVMe驱动解析-DMA传输的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JAVA命令符找不到符号_[转]Java
- 下一篇: 记事本安卓软件代码设计_用轻量级工具 N