編者按
軟件和硬件,既相互依存又需要某種程度上的相互獨立。通過軟件和硬件之間的接口把兩者連接在一起。軟硬件接口,有很多含義:比如指令集是CPU軟件和硬件之間的接口;比如一些硬件模塊(包括IO接口模塊、GPU、各種加速引擎等)暴露出來的可讀寫寄存器,則為控制接口;再比如,CPU和GPU或其他硬件模塊之間通過DMA進行數據交互的(軟硬件間的)數據傳輸接口。 軟硬件接口,是個非常龐大的命題。本文是《軟硬件融合》圖書內容的節選,聚焦在軟件和硬件之間的數據交互接口。
軟件和硬件之間數據交互接口
我們在計算機的基礎課程里一般都學過IO交互的四種模式:寄存器模式、中斷模式、DMA模式和通道模式。隨著計算機技術的發展,除了IO設備,還有很多獨立的硬件組件通過各種類型總線跟CPU連接在一起。接口已經不僅僅是用于IO數據傳輸場景,也用于軟件(運行于CPU的程序)和其他硬件之間的數據交互場景。 注:本文用“軟硬件接口”特指軟件和硬件之間數據交互的接口。
1 軟硬件接口定義
傳統的非硬件緩存一致性總線,是需要軟件驅動顯式的控制設備來進行數據交互的。通過梳理軟硬件接口的演進,逐步給出軟硬件接口的定義。 a. 軟硬件接口演進
(a) CPU輪詢? ? ? ? ? ? ? ? ? ?(b) CPU中斷???? ?? ? ? ???? ? (c) DMA方式
(d) 共享隊列??? ? ? ???? ???
(e) 用戶態輪詢???? ?? ? ? ??? ?
(f) 多隊列 圖1 軟硬件接口的演進 軟硬件接口是在IO接口基礎上的擴展,如圖1,我們結合IO交互的四種模式,重新梳理一下軟硬件接口的演進:
第一階段,使用軟件輪詢硬件狀態。如圖1(a),最開始是通過軟件輪詢,這時候軟件和硬件的交互非常簡單。發送的時候,軟件會定期的去查詢硬件的狀態,當發送緩沖為空的時候,就把數據寫入到硬件的緩存寄存器;接收的時候,軟件會定期的查詢硬件的狀態,當接收緩沖區有數據的時候,就把數據讀取到軟件。
第二階段,使用中斷模式。如圖1(b),隨著CPU的性能快速提升,統計發現,輪詢的失敗次數很高,大量的CPU時間被浪費在硬件狀態查詢而不是數據傳輸,因此引入中斷模式。只有當發送緩沖存在空閑區域可以讓軟件存放一定量待發送數據的時候,或者接收緩沖已經有一定量數據待軟件接收的時候,硬件會發起中斷,CPU收到中斷后進入中斷服務程序,在中斷服務程序里處理數據的發送和接收。
第三階段,引入DMA。如圖1(c),前面的兩種情況下,都需要CPU來完成數據的傳輸,依然會有大量的CPU消耗。因此引入了專用的數據搬運模塊DMA來完成CPU和硬件之間的數據傳輸,某種程度上,DMA可以看做是用于代替CPU進行數據搬運的加速器。發送的時候,當數據在CPU內存準備好,CPU告訴DMA源地址和數據的大小,DMA收到這些信息后主動把數據從CPU內存搬到硬件內部。同樣的,接收的時候,CPU開辟好一片內存空間并告知DMA目標地址和空間的長度,DMA負責把硬件內部的數據搬運到CPU內存。
第四階段,專門的共享隊列。如圖1(d),引入了DMA之后,如果只有一片空間用于軟件和硬件之間的數據交換,則軟件和硬件之間的數據交換則是同步的。例如在接收的時候,當DMA把數據搬運到CPU內存之后,CPU需要馬上進行處理并釋放內存,CPU處理的時候DMA則只能停止工作。后來引入了乒乓緩沖的機制,當一個內存緩沖區用于DMA傳輸數據的時候,另一個緩沖區的數據由CPU進行處理,實現DMA傳輸和CPU處理的并行。更進一步的,演變成更多緩沖區組成的循環緩沖隊列。這樣,CPU的數據處理和DMA的數據傳輸則完全異步的完成,并且CPU對數據的處理以及DMA對數據的搬運都可以批量操作完成后,再同步狀態信息給對方。
第五階段,用戶態的軟件輪詢共享隊列驅動。如圖1(e),進一步的,隨著帶寬和內存的增加,導致數據頻繁的在用戶態應用程序、內核的堆棧、驅動以及硬件之間交互,并且緩沖區也越來越大,這些都不可避免的增加系統消耗,并且帶來更多的延遲;而且,數據交互頻繁,導致的中斷的開銷也是非常龐大的。因此,通過用戶態的PMD(Polling Mode Driver,輪詢模式驅動)可以高效的在硬件和用戶態的應用程序直接傳遞數據,不需要中斷,完全繞開內核,以此來提升性能和降低延遲。
第六階段,支持多隊列。如圖1(f),隨著硬件設計規模擴大,硬件資源越來越多,在單個設備里,可以通過多隊列的支持,來提高并行性。驅動也需要加入對多隊列的支持,這樣我們甚至可以為每個應用程序配置專用的隊列或隊列組,通過多隊列的并行性來提升性能和應用數據的安全性。
說明:本小節所講的內容,主要是基于傳統的非緩存一致性總線的數據交互演進。隨著跨芯片的緩存數據一致性總線開始流行,通過硬件完成數據交互,在提升性能的同時,也簡化了軟件設計。 b. 軟硬件接口的組成部分 粗略的說,軟硬件接口是由驅動(Driver)和設備(Device)組成,驅動和設備的交互也即軟件和硬件的交互。更確切一些的說,軟硬件接口包括交互的驅動軟件、硬件設備的接口部分邏輯,也包括內存中的共享隊列,還包括傳輸控制和數據信息的總線。
圖2軟硬件接口硬件架構示意模型 如圖2,是軟硬件接口硬件架構的示意模型。軟硬件接口的組件詳細介紹如下:
驅動軟件。驅動是提供一定的接口API,讓上層的軟件能夠更加方便的與硬件交互。驅動負責硬件設備控制面的配置、運行控制以及設備數據面的數據傳輸控制等。驅動屏蔽硬件的接口細節,對上層軟件提供標準的API函數接口。驅動屏蔽硬件細節,提供標準API給上層軟件,在單機系統是非常有價值的。通過不同版本的驅動,既可以屏蔽硬件細節,又可以跟不同的操作系統平臺兼容。在云計算場景,要求要更嚴格一些,云場景期望是完全標準的硬件接口。驅動是代表軟件與硬件交互的接口,但依然是軟件的一部分,在云計算虛擬機驅動也會遷移到新的環境,這就要求新的運行環境和原始環境一致。也就是說,在IO直通模式下,需要雙方的硬件接口本身就是一致的。
設備硬件接口子模塊,包括DMA和內部緩沖。高速的設備一般都有專用的DMA,專門負責數據搬運。驅動會通知DMA共享隊列狀態信息,然后DMA會讀取內存中的共享隊列描述符,并根據描述符信息負責在CPU內存和內部緩沖之間搬運數據。
共享隊列。特定的跟硬件DMA格式兼容的共享隊列數據結構,軟件和硬件通過共享隊列交互數據。每個共享隊列包括隊列的頭和尾指針、組成隊列的各個描述符項以及每個描述符項所指向的實際的數據塊。共享隊列位于軟件側的CPU內存里,由軟件驅動負責管理。
傳輸的總線:軟硬件交互可以說是上層功能,需要底層接口總線的承載。例如,在片內通常是通過AXI-Lite總線來實現軟件對硬件的寄存器讀寫控制,而數據總線則是通過AXI實現硬件DMA對軟件的CPU內存的讀寫訪問。芯片間的總線常見的主要是PCIe,通過PCIe的TLP包來承載上層的各種類型的讀寫訪問。
2 生產者消費者模型
生產者消費者問題(Producer-Consumer Problem)是多進程同步問題的經典案例之一,描述了共享固定大小緩沖區的兩個進程,即所謂的“生產者”和“消費者”,在實際運行時如何處理交互的問題。 如圖3所示,生產者的主要作用是生成一定量的數據放到緩沖區中,然后重復此過程。與此同時,消費者也在緩沖區消耗這些數據。問題的關鍵就是要保證生產者不會在緩沖區滿時加入數據,消費者不會在緩沖區中空時消耗數據。
圖3 經典生產者消費者模型 解決問題的基本辦法是:讓生產者在緩沖區滿時休眠,等到消費者消耗緩沖區中的數據,從而緩沖區有了空閑區域的時候,生產者才能被喚醒,開始繼續往緩沖區添加數據;同樣,也需要讓消費者在緩沖區空時進入休眠,等到生產者往緩沖區添加數據之后,再喚醒消費者繼續消耗數據。 a. 進程間通信 一個生產者進程,一個消費者進程,生產者進程通過共享緩沖傳遞數據給消費者進程。如果程序員不夠小心,沒有考慮多進程間相互影響的話,很可能寫出下面這段會導致“死鎖”的代碼。
// 該算法使用了兩個系統庫函數:sleep 和 wakeup。 // 調用 sleep的進程會被阻斷,直到有另一個進程用wakeup喚醒之。 // 代碼中的itemCount用于記錄緩沖區中的數據項數。 int?itemCount?=?0; procedure producer() { while (true) { item = produceItem(); if (itemCount == BUFFER_SIZE) { sleep(); } putItemIntoBuffer(item); itemCount = itemCount + 1; if (itemCount == 1) { wakeup(consumer); } } } procedure consumer() { while (true) { if (itemCount == 0) { sleep(); } item = removeItemFromBuffer(); itemCount = itemCount - 1; if (itemCount == BUFFER_SIZE - 1) { wakeup(producer); } consumeItem(item); } }上面代碼中的問題在于它可能導致競爭條件,進而引發死鎖??紤]下面的情形:
消費者進程把最后一個itemCount的內容讀出來(注意它現在是零),消費者進程返回到while的起始處,現在進入if塊。
就在調用sleep之前,OS調度,決定將CPU時間片讓給生產者進程,于是消費者進程在執行sleep之前就被中斷了,生產者進程開始執行。
生產者進程生產出一項數據后將其放入緩沖區,然后在itemCount上加 1;由于緩沖區在上一步加1之前為空,生產者嘗試喚醒消費者。
遺憾的是,消費者并沒有在休眠,喚醒指令不起作用。當消費者恢復執行的時候,執行 sleep,一覺不醒(出現這種情況的原因在于,消費者只能被生產者在itemCount為1的情況下喚醒)。
生產者不停地循環執行,直到緩沖區滿,隨后進入休眠。
由于兩個進程都進入了永遠的休眠,死鎖情況出現了。因此,該算法是不完善的。我們可以通過引入信號量(Semaphore)的方式來完善這個算法。信號量能夠實現對某個特定資源的互斥訪問。
// 該方法使用了兩個信號燈,fillCount和emptyCount; // fillCount用于記錄緩沖區中存在的數據項數量; // emptyCount用于記錄緩沖區中空閑空間數量; // 當有新數據項被放入緩沖區時,fillCount增加,emptyCount減少; // 當有新數據項被取出緩沖區時,fillCount減少,emptyCount增加; // 如果在生產者嘗試減少emptyCount的時候發現其值為零,那么生產者就進入休眠。 // 等到有數據項被消耗,emptyCount增加的時候,生產者才被喚醒。 //?消費者的行為類似。 semaphore fillCount = 0; // 生產的項目 semaphore emptyCount = BUFFER_SIZE; // 剩余空間 procedure producer() { while (true) { item = produceItem(); down(emptyCount); putItemIntoBuffer(item); up(fillCount); } } procedure consumer() { while (true) { down(fillCount); item = removeItemFromBuffer(); up(emptyCount); consumeItem(item); } }b.分布式消息隊列服務 消息隊列中間件是分布式系統中重要的組件,主要解決應用耦合、異步消息、流量削峰等問題。消息(Message)是指在應用之間傳送的數據,消息可以非常簡單,比如只包含文本字符串,也可以更復雜,可能包含嵌入對象。消息隊列(Message Queue)是一種應用間的通信方式,消息發送后可以立即返回,有消息系統來確保信息的可靠傳遞,消息發布者只管把消息發布到MQ中而不管誰來取,消息使用者只管從MQ中取消息而不管誰發布的,這樣發布者和使用者都不用知道對方的存在。 如圖4,消息隊列一般由三部分組成:
Producer:消息生產者,負責產生和發送消息到 Broker。
Broker:消息處理中心。負責消息存儲、確認、重試等,一般其中會包含多個Queue。
Consumer:消息消費者,負責從 Broker 中獲取消息,并進行相應處理。
圖4 消息隊列模型 消息隊列具有如下特性:
異步性。將耗時的同步操作,通過發送消息的方式,進行了異步化處理。減少了同步等待的時間。
松耦合。消息隊列減少了服務之間的耦合性,不同的服務可以通過消息隊列進行通信,而不用關心彼此的實現細節,只要定義好消息的格式就行。
分布式。通過對消費者的橫向擴展,降低了消息隊列阻塞的風險,以及單個消費者產生單點故障的可能性(當然消息隊列本身也可以做成分布式集群)。
可靠性。消息隊列一般會把接收到的消息存儲到本地硬盤上(當消息被處理完之后,存儲信息根據不同的消息隊列實現,有可能將其刪除),這樣即使應用掛掉或者消息隊列本身掛掉,消息也能夠重新加載。
互聯網場景使用較多的消息隊列有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等。 c. 驅動和設備通信 NIC(Network Interface Adapter,網絡接口卡)是典型的IO設備,網絡數據包的傳輸有發送Tx和接收Rx兩個方向。通過貢獻的Tx Queue和Rx Queue來交互數據傳輸。我們以網絡Tx的傳輸為例,介紹基于生產者消費者模型的驅動和設備數據交互。 如圖5,給出了網絡包處理Tx發送的基本原理示意圖(Rx接收跟Tx發送類似,控制流程一致,數據方向相反)。可以看到,在Tx的時候,驅動是生產者,設備是消費者,他們通過內存中共享的環形隊列傳輸數據。一般在環形隊列中的是用于描述數據的描述符,通過指針指向實際的數據塊。當上層應用通過驅動把數據寫到環形隊列以后,驅動會把環形隊列相關的狀態信息告知設備端。設備端接收到信息后DMA開始工作,首先讀取環形隊列中的相應描述符,然后通過描述符信息搬運實際的數據塊到硬件內部。搬運完成后硬件通過中斷告知驅動,然后驅動會釋放此塊緩沖。
圖5 網絡驅動和設備通信示意圖
3 用戶態輪詢驅動:DPDK和SPDK
DPDK和SPDK是當前主流的開源高速接口框架,核心的技術是用戶態的輪詢驅動。DPDK/SPDK支持兩個核心的設備類型:DPDK聚焦高性能網絡處理,SPDK聚焦高性能存儲處理。 a. DPDK介紹
DPDK(Data Plane Development Kit,數據平面開發套件)是在用戶態中運行的一組軟件庫和驅動程序,可加速在CPU架構上運行的數據包處理工作負載。DPDK由英特爾大約在2010年創建,現在作為Linux基金會下的一個開源項目提供,在拓展通用CPU的應用方面發揮了重要作用。
?
? |
? | ? |
? | |
(a) 基于Linux內核的包處理 | (b) 基于DPDK的包處理 |
圖6 基于DPDK的包處理 如圖6(a),傳統Linux網絡驅動存在如下一些問題:
中斷開銷大,大量數據傳輸會頻繁觸發中斷,中斷開銷系統無法承受;
數據包從內核緩沖區拷貝到用戶緩沖區,帶來系統調用和數據包復制的開銷;
對于很多網絡功能來說,TCP/IP協議并非數據轉發必需;
操作系統調度帶來的緩存替換也會對性能產生負面影響。
如圖6(b),DPDK最核心的功能是提供了用戶態的輪詢模式驅動,為了加速網絡IO,DPDK允許傳入的網絡數據包直通到用戶空間而沒有內存復制的開銷,不需要用戶空間和內核空間切換時的上下文處理。DPDK可在高吞吐量和低延遲敏感的場景加速特定的網絡功能,如無線核心、無線訪問、有線基礎設施、路由器、負載均衡器、防火墻、視頻流、VoIP等。DPDK所使用的優化技術主要有:
用戶態驅動,減少內核態用戶態切換開銷,減少緩沖區拷貝;
輪詢模式驅動(PMD, Polling Mode Driver),不需要中斷,沒有中斷開銷,并且對隊列及數據及時處理,降低延遲;
固定處理器核,減少線程切換的開銷,減少緩存失效,同時要考慮NUMA特性,確保內存和處理器核在同一個NUMA域中;
大頁機制,減少TLB未命中幾率;
非鎖定的同步,避免等待;
內存對齊和緩存對齊,有利于內存到緩存的加載效率;
DDIO機制,從IO設備把數據直接送到L3緩存,而不是送到內存。
b. SPDK介紹 在數據中心中,固態存儲介質正在逐漸替換機械HDD,NVMe在性能、功耗和機架密度方面具有明顯的優勢。因為固態存儲吞吐量提升,存儲軟件需要花費更多的CPU資源;因為固態存儲延遲性能的大幅提升,存儲軟件的處理延遲則開始凸顯??偨Y來說,隨著存儲介質性能的進一步提升,存儲軟件棧的性能和效率越來越成為存儲系統的瓶頸。 如圖7,SPDK(Storage Performance Development Kit,存儲性能開發套件)利用了很多DPDK的組件,在DPDK的基礎上,加入了存儲的相關組件。SPDK的核心技術依然是用戶態的PMD。SPDK已經證明,使用一些處理器內核和一些NVMe驅動器進行存儲,而無需額外的卸載硬件,可以輕松實現每秒數百萬個IO。
圖7 SPDK基于DPDK和一些新的組件 SPDK由許多組件組成,這些組件相互連接并共享用戶態和輪詢模式操作的通用功能。每個組件都是為了克服特定場景的性能瓶頸而開發的,并且,每個組件也可以集成到非SPDK架構中,從而使客戶可以利用SPDK的技術來加速自己的軟件應用。從底層到上層,SPDK的組件包括:
用戶態PMD驅動:基于PCIe的NVMe驅動、NVMeoF驅動,以及英特爾QuickData驅動(QuickData為Intel志強處理器平臺的IO加速引擎)。
后端塊設備:Ceph RADOS塊設備(Ceph為開源的分布式存儲系統,RADOS為Ceph的分布式集群封裝),Blobstore塊設備(VM或數據庫交互的虛擬設備),Linux AIO(異步IO)。
存儲服務:塊設備抽象層(bdev),Blobstore。
存儲協議:iSCSI Target端,NVMeoF Target端,vhost-scsi Target端,vhost-blk Target端。
編輯:黃飛
評論
查看更多