支持nvme的linux_Linux nvme驱动初探
本篇研究的nvme驅動基于Linux 3.10.73 ,為什么選擇這個版本呢,因為這個版本之后Linux 塊層馬上就換成支持多隊列(可以參考Linux塊層多隊列之引入內核),小編的SUSE 11.3也正好能編譯這個相對比較低的版本。(隨后再看最新版本內核上nvme驅動的實現)
通過nvme_alloc_ns可知,nvme設備通過nvme_make_request()函數進入快層:
nvme_alloc_ns()blk_queue_make_request(ns->queue, nvme_make_request);
快速看一下nvme_make_request()這個函數,就會發現nvme設備有多么任性,都沒有申請request, 合并bio這種常規操作(因為它支持隨機寫,而且速度快,不需要),直接把提交過來的bio送到塊設備驅動進行處理。(小編第一次看到這個函數時就小激動了一把)。
714 static void nvme_make_request(struct request_queue *q, struct bio *bio)715 {716 struct nvme_ns *ns = q->queuedata;717 struct nvme_queue *nvmeq = get_nvmeq(ns->dev);718 int result = -EBUSY;719720 spin_lock_irq(&nvmeq->q_lock);721 if (bio_list_empty(&nvmeq->sq_cong))722 result = nvme_submit_bio_queue(nvmeq, ns, bio);723 if (unlikely(result)) {724 if (bio_list_empty(&nvmeq->sq_cong))725 add_wait_queue(&nvmeq->sq_full, &nvmeq->sq_cong_wait);726 bio_list_add(&nvmeq->sq_cong, bio);727 }728729 spin_unlock_irq(&nvmeq->q_lock);730 put_nvmeq(nvmeq);731 }
雖說在這個linux版本上塊設備層只支持單隊列,但是nvme設備有自己的多隊列,每個cpu上綁著一個隊列(自己玩)。
每個隊列是一個先進先出的FIFO管道,用于連通主機端(Host)和設備端(Device)。其中從主機端發送到設備端的命令管道稱之為發送隊列.從設備端發送到主機端的命令完成管道稱之為完成隊列。對于一個IO請求,在主機端組裝完成后,通過發送隊列發到設備端,然后在設備中進行處理并把相應的完成結果組裝成IO完成請求,最后通過完成隊列返還給主機端。
不管是發送隊列還是完成隊列,都是一段內存,通常位于主機端的DDR空間(申請的DMA內存)里。這段內存劃分成若干等長的內存塊,每一塊用于存儲一個定常的消息(nvme的發送消息和完成消息都是定常的)。在使用的時候,對于這個隊列,有一個頭指針和一個尾指針。當兩者相等時,隊列是空的。見下圖。
隨著新的消息加入到隊列中來,尾指針不停向前移動。因為內存是定常的,因此指針一旦移動到內存的最后一個存儲空間,之后再移動的話需要環回到內存的起始位置。因此內存在使用上實際上當作一個環來循環使用。當尾指針的下一個指針就是頭指針的時候,這個隊列不能再接收新的消息,即隊列已經滿了,如下圖所示。
隨著隊列的使用者不斷取出消息并修改頭指針,隊列中的元素不斷釋放,一直到頭指針再次追上尾指針時,隊列完全變空。
那么主機端將數據寫入隊列后,設備端是怎么知道該隊列所在的內存已經更新了呢?這就需要利用門鈴機制(Doorbell)。每個隊列都有一個門鈴指針。對于發送隊列來說,這個指針表示的是發送隊列的尾指針。主機端將數據寫入到發送隊列后,更新映射到位于設備寄存器空間中的門鈴的尾指針。實現在SoC控制器芯片上的尾指針一旦被更新,設備就知道新數據到了。
這里并未涉及到主機端如何知道數據已經取走并且設備已經更新了頭指針了。nvme協議并沒有采用傳統的查詢寄存器的方式來讓主機獲得這個信息,因為這樣勢必造成CPU與硬件寄存器的交互。對于x86來說,每一次與硬件的交互都會帶來性能的損失,因此降低硬件交互尤為重要。NVMe的方案是對于這個發送消息,在當它完成的時候會將完成的結果通過DMA的方式寫入到內存中,主機根據每個IO請求及其完成請求中的Command Identifier (CID)字段來匹配相應的發送請求和完成請求。其中完成結果中攜帶有信息表明最新的該請求所對應的發送隊列的當前頭指針。
nvme_alloc_queue()nvmeq->cqes = dma_alloc_coherent(dmadev, CQ_SIZE(depth),…nvme_process_cq(){/* 檢查dma完成請求中 strcut nvme_completion數組,確定發送隊列的head指針 */struct nvme_completion cqe = nvmeq->cqes[head];free_cmdid(nvmeq, cqe.command_id, &fn)}
反過來,當設備端完成一個nvme請求時,也需要通過完成隊列來把完成的結果告知主機端,這是通過完成隊列來實現的。與發送隊列不同,完成隊列是通過中斷機制告訴接收端(主機CPU)收到了新的完成消息并安排后續處理。同樣的,為了確定完成隊列里到底有多少是新的完成消息,在每一個完成請求中,有一個標志位phase,這個標志位每次寫入的數值都會發生改變,并據此確定每一個完成請求是否是新的完成請求(比如一次完成請求phase為1,第二次完成請求phase值為0,從代碼來看phase值初始值為1,說明設備發送給主機端phase的值第一次為1)。通過這種機制,雖然主機端不能一下子確定到底有多少新的完成請求,但是可以逐漸的、一步步完成所有的完成請求,并將完成隊列用空。隨著主機逐漸從完成隊列里取出完成消息,主機會更新位于設備上的完成隊列頭指針寄存器,告訴設備完成隊列的實施狀況。
nvme_process_cq()/*?檢查dma完成請求中?strcut?nvme_completion數組,確定完成*?隊列的head?,通過門鈴通知設備?db(doorbeel)?*/head?=?nvmeq->cq_head;writel(head,?nvmeq->q_db?+?(1?<dev->db_stride));
在最新的nvme1.2A中,每一個NVMeController允許最多65535個IO隊列和一個Admin隊列。Admin隊列在設備初始化之后隨即創建,包括一個發送隊列和一個完成隊列。其他的IO隊列則是由Admin隊列中發送的控制命令來產生的。nvme規定的IO隊列的關系比較靈活,既可以一個發送隊列對應一個完成隊列,也可以幾個發送隊列共同對應一個完成隊列。在主流的實現中,較多采用了一對一的方式。下圖列舉了兩種方式的示意。
nvme_setup_io_queues()負責初始化隊列,它先查詢了到底有多少個CPU,然后再調用set_queue_count發命令給設備,讓設備按照CPU的個數來設置隊列的個數。
nvme_setup_io_queues()set_queue_count(dev, nr_io_queues);nvme_set_features(dev,NVME_FEAT_NUM_QUEUES, q_count, 0,&result);
在set_queue_count中,按照之前傳入的CPU數量來設置設備的能力。其中NVME_FEAT_NUM_QUEUES對應于NVMe協議的Number of Queues (Feature Identifier 07h)。當然,根據設備能力不同,如果不巧設備剛好沒辦法支持這么多隊列的話,驅動程序也會做一些取舍,選取設備的能力和CPU數量中較小的值。
參考: 晶格思維微信公眾號
--The end--
總結
以上是生活随笔為你收集整理的支持nvme的linux_Linux nvme驱动初探的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安川机器人编程加电弧_安川AR2010机
- 下一篇: ddos攻击的实现方法(ddos攻击实现