DPDK2.1開發者手冊3-4

  1. 環境抽象層EAL

環境抽象層的任務對訪問底層資源例如硬件和內存提供入口。它提供了隱藏應用和庫的特殊性性的通用接口。它的責任是初始化分配資源(內存,pci設備,定時器,控制檯等等)。node

EAL提供的典型服務有:linux

l  DPDK加載和啓動:DPDK和程序鏈接成 一個單獨的程序且必須經過某種方式加載。git

l  CPU親和性/分配處理:EAL提供了一種機制將執行單元分配給特定的核就如同建立一個執行程序同樣。web

l  系統內存分配:EAL實現不一樣內存區域的分配,例如用於物理設備交互的內存區域。數據庫

l  PCI地址的抽象:EAL提供了對PCI地址空間訪問的接口。數組

l  追蹤調試功能:日誌,棧轉存,異常等等。緩存

l  公用功能:libc庫不能提供的自旋鎖和原子計數器安全

l  CPU特徵辨識:決定cpu運行時的特殊功能,例如:Intel AVX。決定當前CPU支持的特性以便編譯對應的二進制程序。網絡

l  中斷處理:提供註冊/反註冊對特定中斷的回調函數。數據結構

l  告警功能:設置或取消運行時特定時間告警處理的回調函數接口。

  1.  
  2.  
  3.  

3.1.    linux執行環境下的EAL

在linux用戶態空間,DPDK程序使用pthread線程庫最爲一個用戶態程序運行。設備的pci信息和地址空間經過/sys內核接口和像uio_pci_generic或者是igb_uio內核模塊來獲取。參閱UIO:linux內核中用戶態驅動文檔。這段內存是在程序中mmap的。

      EAL執行物理地址的分配,從hugetblbfs管理的內存經過使用mmap()實現(使用大頁來提高性能)。這些內存對於dpdk服務層都是可見的例如Mempool Library。

      在這點上,dpdk服務層已經初始化了,接着經過設置線程親和性的調用,每一個執行單元將會分配給特定的邏輯核心想一個user-level線程同樣運行。

      定時器是由CPU的時間戳計時器(TSC)或者是經過mmap()調用的HPET內核接口提供。

3.1.1.  初始化和core運行

初始化分佈是gblic的開始函數作的。檢查也是在初始化時作的以確保配置文件中選擇的架構宏定義是cpu支持的類型。而後,main()函數被調用。core的初始化和加載使在rte_eal_init()中作的(看API文檔)。它由線程庫的調用組成(pthread_self(),pthread_create(),pthread_setaffinity_np())。

注意:對象的初始化,例如內存區域,ring,mempool,lpm表和hash表,應該做爲整個程序初始化的一部分在主邏輯核上完成。建立和初始化這些對象的函數不是多線程安全的。無論怎麼樣,一旦初始化完成,對象自身能夠安全用於多線程。

3.1.2.  多進程支持

linux下EAL容許同多線程部署模式同樣支持多進程。具體看2.20章節《Multi-process Support》

3.1.3.  內存映射和內存分配

大量的物理連續的內存分配是使用hugetlbfs內核文件系統來作的。EAL提供了一個API來申請指定大小的物理連續的內存塊。這個API也會返回申請到的內存的物理地址給用戶。

注意:內存申請是用rte_malloc庫接口來作的,它也是hugetlbfs文件系統大頁支持的。

3.1.4.  Xen Dom0非大頁支持

現存的內存管理機制是基於linux內核的大頁機制。然而,Xen Dom0並不支持大頁,因此要將一個新的內核模塊rte_dom0_mm插入以避開這個限制。

EAL使用IOCTL接口通知內核模塊rte_dom0_mm分配指定大小的內存且從這個模塊獲取全部內存段信息。EAL使用MMAP接口映射分配到的內存。對全部的內存段來講,在其內的物理地址都是連續的,可是實際上硬件地址只是在2MB內連續。

 

Fig.3.1 linux應用環境下EAL初始化過程

3.1.5.  PCI設備訪問

EAL使用內核提供的/sys/bus/pci掃描PCI總線上的內容。訪問PCI內存,內核模塊uio_pci_generic 提供了/dev/uioX設備文件以及/sys中的資源文件,它被映射以獲取從用戶態程序訪問pci地址空間的能力。DPDK特有的igb_uio模塊也提供了這一功能。這兩個驅動都用到uio內核特性(用戶態驅動)。

 

3.1.6.  每一個邏輯核共享變量

注意:邏輯核就是處理器的邏輯執行單元,又是也稱做硬件線程。

共享變量是默認的作法。額米格邏輯核的變量的實現是經過使用Thread Local Storage(TLS)線程局部存儲?來提供每一個線程的本地存儲。

3.1.7.  日誌

EAL提供了日誌API.在linux環境下,日誌默認是發送到系統日誌文件和終端上.然而,用戶可使用不一樣的日誌機制來代替DPDK提供的日誌功能.

調試功能

有一些調試函數轉存棧數據. rte_panic()能自動產生一個abort信號,這個信號會觸發產生gdb調試用的core文件。

3.1.8.  cpu特性

EAL能夠在運行時查詢CPU狀態(經過rte_cpu_get_feature()函數)決定哪一個cpu能夠用。

3.1.9.  用戶態中斷事件

l  主線程對用戶態的中斷和警告處理

EAL建立一個主線程輪詢UIO設備文件描述符以檢測中斷。EAL能夠註冊或者是反註冊一個特定中斷的回調函數,這個函數能夠在主線程中異步調用。EAL也容許像NIC的中斷一樣的方式註冊定時器中斷回調函數。

注意:DPDK PMD,主線程只對鏈接狀態改變的中斷處理,例如網卡鏈接打開和關閉操做。

l  收包中斷事件

PMD提供的收發包程序並不限制自身在輪詢模式下執行。對於極小的流量減小輪詢下的cpu利用率,能夠中斷輪詢並等待wake-up事件的發送。收包中斷對於此類的wake-up事件是最佳選擇,單也不是惟一的。

EAL提供了事件驅動模式的API。以linuxapp爲例,其運行依賴於epoll。EAL線程能夠監控添加了全部wake-up事件的文件描述符對象。事件文件描述符能夠建立並根據UIO/VFIO說明來映射到中斷向量。對於bsdapp,kqueue是可選的,可是還沒有實現。

EAL初始化事件描述符和中斷向量之間的映射關係,每一個設備初始化中斷向量和隊列之間的映射。這樣,EAL其實是忽略在指定向量上的發生的中斷。eth_dev驅動會負責執行後者的映射。

注意:每一個RX中斷事件隊列只支持VFIO,後者支持多個MSIX向量。在UIO中,收包中斷和其它發生的中斷共享中斷向量。因此,當RX中斷和LSC(鏈接狀態改變)中斷同時發生時(intr_conf.lsc==1 && intr_conf.rxq==1),只有前者生效。

使用網卡設備API控制、打開、關閉RX中斷<rte_eth_dev_rx_intr_*>,若是PMD不支持則返回失敗。intr_conf.rxq標識用於打開每一個設備的RX中斷。

3.1.10.  黑名單

EAL pci設備黑名單功能是用於標記網卡某一個端口做爲黑名單,以便DPDK忽略該端口。用PCIe描述符(Domain:Bus:Device.Function)將端口標記黑名單。

3.1.11.  複雜指令集功能

i686和x86_64架構上的鎖和原子操做。

3.2.    內存分段和內存區(memzone)

物理地址的映射就是EAL經過這個來實現的。物理內存多是分隔不連續的,全部的內存都由一個內存描述符表管理,且每一個描述符(rte_memseg)指向的都是一段連續的物理內存。

在這之上,memzone分配器的角色就是保存一段物理連續的內存。這些內存區的內存被申請到使用時會有一個惟一名字來標示。

rte_memzone描述符也在配置結構體中。可使用te_eal_get_configuration()接口訪問這個結構體。經過名字查找一個內存區會返回一個有內存區物理地址的描述符。

內存區能夠以特定的開始地址以指定的對齊參數對齊(默認是cache_line大小的對齊)。對齊值應該是2的冪次方且很多於cache_line大小(64字節)。內存區能夠是2m或者是1g的內存頁,系統二者都支持。

3.3.    多線程

dpdk一般是指定核上跑指定線程以免任務調度的開銷。這個對於性能的提高頗有用,可是缺乏靈活性且不是老是有效的。

經過限制cpu的運行頻率,電源管理有助於提高CPU效能。然而也能是利用空閒的指令週期來充分的使用CPU所有性能。

經過使用cgroup,cpu使用量能夠很輕鬆的分配。這個提供了另外的方式來提高cpu效能,然而有一個先決條件DPDK必須處理每一個核多線程之間的上下文切換。

要更加靈活,就設置線程的cpu親和性不是對cpu而是cpu集。

3.3.1.  EAL線程和邏輯核親和性

lcore指的是EAL線程,就是一個真正的linux/freeBSD線程。EAL建立和管理eal線程,且經過remote_launch來實現任務分配。在每一個EAL線程中,有一個稱爲_lcore_id TLS是線程的獨一無二的id。通常EAL線程使用1:1來綁定物理cpu,_lcore_id一般等於CPU id。

當使用多線程時,綁定再也不在線程和指定物理cpu之間老是1:1,EAL線程設爲對cpu集的親和性,而_lcore_id再也不和CPU id同樣。由於這個,有一個EAL選項-lcores,設置lcore的cpu親和性。對於指定lcore ID或者是ID組,這個選項容許對EAL線程設置CPU集。

格式模板:-lcores=lcores=’<lcore_set>[@cpu_set][,<lcore_set>[@cpu_set],...]’

       lcore_set 和cpu_set能夠是一個數,範圍或者是組。數必須是「digit([0-9]+)」,範圍則是「<number>-<number>」,組則是「(<number|range>[,<number|range>,...])」。

       若是@cpu_set的值沒有提供,則默認將其設爲lcore_set相同的值。

例如:"--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'"意味着啓動9個eal線程「

lcore 0 runs on cpuset 0x41 (cpu 0,6);

lcore 1 runs on cpuset 0x2 (cpu 1);

lcore 2 runs on cpuset 0xe0 (cpu 5,6,7);

lcore 3,4,5 runs on cpuset 0x5 (cpu 0,2);

lcore 6 runs on cpuset 0x41 (cpu 0,6);

lcore 7 runs on cpuset 0x80 (cpu 7);

lcore 8 runs on cpuset 0x100 (cpu 8).

使用這個選項,每一個給定的lcore ID能夠分配指定的cpu。也兼容corelist(‘-l‘)選項模式。

3.3.2.  非EAL線程支持

在DPDK執行上下文環境中執行用戶線程(也稱做非EAL線程)時可行的。在非EAL線程中,_lcore_id老是等於LCORE_ID_ANY,這個宏標示有效的非EAL線程,其值是惟一的_lcore_id。一些庫會使用傳統的id(例如線程標示符TID),有些是一點影響都沒有,有些則是由使用限制(如timer和mempool庫)。

全部的影響都在Known Issues章節中提到了。

3.3.3.  公共線程API

有兩個公用API:rte_thread_set_affinity()和rte_pthread_get_affinity()。當在任意一個線程上下文環境中使用時,TLS會被設置/獲取。

那些TLS包括_cpuset和_socket_id:

l  _cpuset存的是線程親和性的CPU組位圖

l  _socket_id存的是CPU集的NUMA節點。若是cpu集中的cpu分屬不一樣的numa節點,則_socket_id會被設成SOCKET_ID_ANY(-1)。

3.3.4.  已知問題

l  rte_mempool

rte_mempool在mempol中使用每一個lcore緩存。對於非EAL線程,rte_lcore_id()返回無效值。因此目前當rte_mempool在非EAL線程中使用,put/get操做將忽視mempool緩存且 因爲這個會有性能損失。對非EAL線程的mempool的cache支持如今能夠了。

l  rte_ring

rte_ring支持多生產者入隊列和多消費者出隊列。然而,它是非搶佔的,這有一個消極的影響:使得rte_mempool也是非搶佔的。

注意:非搶佔的限制意味着:

一個線程對給定的ring執行多生產者入隊列時,隊列必須不被其它線程搶佔執行入隊列。

一個線程對給定的ring執行多消費者出隊列時,隊列必須不被其它先佔搶佔執行出隊列。

忽略這個限制會引發第二個線程自旋知道第一個線程被從新調度。此外,若是第一個線程被更高優先級的上下文搶佔,可能會引發死鎖。

這不意味着它不能用,很簡單,有必要在一樣的核上減小多線程同時訪問ring的場景。

  1. 能夠用於單生產者和單消費者的場景。
  2. 在調度策略是SCHED_OTHER(cfs徹底公平調度程序)時能夠在多生產者/多消費者線程中使用。用戶須要在使用前知道其性能損失。
  3. 在調度策略是SCHED_FIFO或者是SCHED_RR時,不能在多生產者/多消費者環境中使用。

爲了rte_ring減小競爭定義了RTE_RING_PAUSE_REP_COUNT,主要是爲了狀況2,在N次重複暫停後放棄對ring的操做。

它增長了sched_yield()系統調用,當線程自旋等待其它線程完成對ring的操做過久時,這個給了被搶佔的線程機會繼續執行且完成入/出隊列操做。

l  rte_timer

在非EAL線程中,沒有每一個線程自有的日誌等級和日誌類型,用的是全局的日誌等級。

l  misc

在非EAL線程中,對rte_ring,rte_mempool,rte_timer的調試統計是不支持的。

3.3.5.  cgroup控制

下面是一個簡單的cgroup控制的使用例子,有兩個線程(他t1和t2)在一樣的core(CPU)上作包的I/O。咱們指望只有50%的CPU用於包I/O:

      mkdir /sys/fs/cgroup/cpu/pkt_io

mkdir /sys/fs/cgroup/cpuset/pkt_io

echo $cpu > /sys/fs/cgroup/cpuset/cpuset.cpus

echo $t0 > /sys/fs/cgroup/cpu/pkt_io/tasks

echo $t0 > /sys/fs/cgroup/cpuset/pkt_io/tasks

echo $t1 > /sys/fs/cgroup/cpu/pkt_io/tasks

echo $t1 > /sys/fs/cgroup/cpuset/pkt_io/tasks

cd /sys/fs/cgroup/cpu/pkt_io

echo 100000 > pkt_io/cpu.cfs_period_us

echo 50000 > pkt_io/cpu.cfs_quota_us

3.4.    Malloc

EAL提供了一個malloc API分配任意大小內存。這個提供相似malloc函數功能的API目的是容許從大頁內存中分配且便於程序移植。DPDK API參考手冊詳細介紹了這個函數功能。

通常的,這種分配內存的方式不能用於高速數據處理平臺,由於它相對於基於pool的分配方式實在太慢了且在分配和釋放時都得加鎖。然而,能夠在配置生成的代碼中用到。

能夠從DPDKAPI參考手冊中查閱rte_malloc()函數的更多細節信息描述。

3.4.1.  Cookies

當CONFIG_RTE_MALLOC_DEBUG選項打開時,分配的內存包含內存區域寫保護以幫助識別緩存溢出。

3.4.2.  對齊和NUMA結構的限制

rte_malloc()有一個對齊參數,用於申請對齊於這個值的倍數(2的冪次方)的內存區域。

在支持NUMA的系統中,rte_malloc()函數調用會返回調用者所使用core的NUMA socket上分配的內存。提供一系列的API,以實現直接在指定的NUMA socket上分配內存,或者是在其它core所在的NUMA socket上分配,假如內存是其它一個邏輯核使用的而不是正在執行內存分配的這個核。

3.4.3.  用例

這個API用於應用程序在初始化時請求使用malloc類似的函數分配內存。

在運行是分配和釋放數據,在程序的快速通道建議用mempool代替。

3.4.4.  內部實現

3.4.4.1. 數據結構

在malloc庫內部有兩個數據結構類型使用:

l  結構體malloc_heap:用於跟蹤每一個socket上的空閒內存

l  結構體 malloc_elem:分配的基本元素且庫內用於跟蹤空閒內存空間

結構體:malloc_heap

malloc_heap結構體用於管理每一個socket上的空閒內存空間。實際上,每個NUMA node上都有一個heap結構體對象,這樣就能夠實現線程從其所在運行的NUMA node上分配內存。當不能保證所使用的內存是在運行的NUMA node上,那就和在混合或者隨機的node上分配內存好不到那裏去。

堆結構體中的主要成員和函數描述以下:

l  lock:鎖成員是爲了實現同步訪問堆。堆中的空閒內存是一個鏈表維護的,爲了防止兩個線程同時訪問這個鏈表就須要加鎖。

l  free_head:空閒內存鏈表頭指針指向malloc_heap的空閒內存鏈表的第一個成員。

注意:malloc_heap結構體並不監測使用的內存塊,因此這些內存塊除非被從新釋放不然就絕對沒法接觸到,從新釋放就是將指向內存塊的指針做爲參數傳給free()函數。

 

Fig.3.2 malloc庫內的malloc heap和malloc elemets例子

結構體:malloc_elem

malloc_elem結構體用於各類內存塊的通用結構,用於3中不一樣的方式:

  1. 做爲一個空閒或者是分配的內存塊的頭-正常狀況(
  2. 做爲一個內存塊內的填充頭
  3. 做爲一個內存表段的結束標記

結構體中最重要的成員和如何使用以下描述:

注意:在上面的三個使用狀況中沒有用到的特定成員,這些成員能夠認爲在那種狀況下沒有明確的值。例如, 對於填充頭padding header,只有成員state和pad有可用的值。

l  heap:這個指針是堆結構體分配的內存塊的引用返回值。它用於釋放的普通內存塊,將其添加到堆的空閒內存鏈表

l  prev:這個指針是指向在內存表中當前位置後面(不該該是前面嗎?)緊靠着的頭結構對象/內存塊。當釋放一個內存塊的時候,這個指針指向的內存塊會被檢查是否也是空閒的,若是是,就將這兩個空閒內存塊合併成一個大的內存塊。(減小內存碎片)

l  next_free:這個指針用於將沒有分配的內存塊鏈接到空閒內存鏈表上。它只用於正常的內存塊,malloc()時就查看一個合適的空閒內存塊分配,free()時就是將從新釋放的內存塊添加到空閒鏈表上。

l  state:這個成員有三種值:FREE,BUSY,PAD。前兩個代表普通內存塊的分配狀態,後則代表結構體是一個在內存塊起始位置填充無心義數據的尾部的虛設結構體。那就是說,數據在內存塊中的開始位置不是在數據庫的頭上,這是因爲數據對齊的限制。如此的話,填充頭結構就是用於定位塊內實際分配的內存的結構體頭部。在內存表的尾部,結構體內這個值爲BUSY,確保不會再有元素了。在釋放的時候會跳過這個搜索其它的內存塊來合併成大的空閒內存塊。

l  pad:這個表明了當前內存塊開始位置填充無用段的長度。在一個正常的內存塊頭部,將它與頭結構體尾部地址相加就是數據段開始地址,就是說這個會做爲malloc的返回值給程序。在這段填充區內有虛設的頭,頭內成員pad有一樣的值,從該頭結構的地址減去pad值就是實際分配的內存塊頭結構地址。

l  size:數據塊的大小,包括自身的頭部分。在內存表尾部的那個虛設結構體中,這個size是0,儘管它從沒有被檢查過。在一個標準的內存塊釋放時,這個值會代替next指針來定位靠在一塊兒的下一個內存塊,萬一後者是FREE狀態,那麼兩者就能夠合二爲一了。

內存分配

在EAL初始化時,全部的內存表都是組織到malloc堆下,這是會將內存表的尾部設置一個BUSY狀態的虛設結構體。當CONFIG_RTE_MALLOC_DEBUG選項打開且在內存表的頭部有一個FREE狀態元素頭,虛設機構體中就可能包含一個哨兵值。FREE元素會被加入到malloc堆的空閒鏈表中。

當程序調用相似malloc函數時,malloc函數會先查看調用線程的lcore_config結構體,肯定該線程所在的NUMA節點。NUMA節點用做malloc_heap結構體數組的下標,且會做爲其中一個參數和其它參數請求內存大小、類型、對齊值、邊界一塊兒傳遞給heap_alloc()函數。

heap_malloc()會先掃描heap的空閒鏈表,試圖找到一個匹配請求存儲數據大小和對齊方式、邊界限制的空閒內存塊,

當一個匹配的空閒元素標記時,算出的內存指針會被返回給用戶。而cacheline大小的內存會在指針以前用malloc_elem裝填。因爲對齊和邊界限制,在元素的開頭和結尾會有空白空間,這回致使一下問題:

  1. 檢查尾部空間。若是尾部空間足夠大,也就是說>128字節,就會分割這個元素。若是不是,那麼就忽略這個尾部空間(白白浪費掉的空間)
  2. 檢查元素頭空間。若是空間很小,就是<=128字節,就會用部分空間做爲填充頭結構,其它的也是浪費掉。然而,若是頭空間足夠大,那麼就將這個空閒元素分割成兩個。

從現有的元素的尾部分配內存的好處就是不用調整空閒鏈表來代替-空閒鏈表上現有元素只須要調整size變量,且其後的元素也只需將prev指針指向新產生的元素就能夠了。

 

釋放內存

要釋放一段內存,數據段開始地址地址會傳遞給free函數。指針值減去malloc_elem結構體打下就是這個內存塊的元素頭。若是頭中type是PAD,那就將指針減去pad值獲得實際的內存塊元素頭結構。

從這個元素頭中,咱們就拿到了從堆中分配的內存塊指針,且它須要在哪裏釋放。和prev指針同樣,經過size能夠計算出緊挨着的後面一個元素的頭指針。檢查先後元素是不是FREE,若是是就與當前的元素合併。這意味着咱們不可能有兩個FREE狀態的元素靠在一塊兒,它們老是會被合併成一個單獨的內存塊。

 

  1. Ring Labrary

ring是管理隊列的。取代無限制大小的鏈表,rte_ring有下面的特性:

l  FIFO,先進先出

l  大小是固定的,指針存在表中。

l  無鎖實現

l  多個或單個消費者出隊列

l  多個或單個生成者入隊列

l  bulk出隊列-指定數目對象出隊列,不然失敗

l  bulk入隊列-同上

l  burst出隊列-按照指定數目最大可能得出隊列,可能出的不足數

l  burst入隊列-按照指定數目最大可能得入隊列,可能入的不足數

這個數據機構相對於鏈表隊列的好處是:

l  更快。只須要一個大小爲sizeof(void *)的原子操做CAS指令來代替多個double CAS指令。

l  比一個徹底無鎖隊列要簡單。

l  適應大量入/出對壘操做。因爲指針是存儲於表中,多個對象出隊列就不會如鏈表同樣會出現多個cache丟失。一樣,bulk出隊列的開銷也不會比單個出隊列大。

缺點就是:

l  大小固定

l  ring會比鏈表隊列消耗更多內存,即便一個空的ring也是包含至少N個指針大小的內存。

對於ring中生產者和消費者head、tailer指針指向的數據結構中存儲的對象的簡單展現:

 

Fig. 4.1Ring結構

4.1     Ring在FreeBSD中的應用參考

下面的代碼在FreeBSD8.0中添加,用於一些網絡設備的驅動(至少是intel的驅動):

l  bufring.h in FreeBSD

l  bufring.c in FreeBSD

4.2     linux中無鎖環形緩存區

下面是描述linux無鎖環形緩衝區設計的連接:http://lwn.net/Articles/340400/

4.3     其它特性

4.3.1 名字

每一個ring都是經過獨一無二的名字來辨別。不可能建立兩個一樣名字的ring(經過rte_ring_create()建立ring時,若是名字已經存在就返回空)

4.3.2 閥值

ring能夠有一個閥值(臨界值)。若是這個閥值配置了,一旦入隊列操做到達閥值,那麼生產者會獲得通知。

這個機制可能用到,例如,IO壓力過大時能夠通知LAN 暫停。

4.3.3 調試

當調試開關打開(CONFIG_RTE_LIBRTE_RING_DEBUG設置了),ring庫會存儲每一個ring的一些關於出入隊列的個數的統計,這個統計是每一個core都有的以免同時訪問或者是原子操做。

4.4     應用場景

ring庫的應用包括:

l  DPDK應用程序間的通信

l  內存池的分配會用到。

4.5     (ring buffer)環形緩衝區的詳細剖析

這部分講解了ring buffer的運做。ring結構體包括兩對頭尾指針(head,tail)。一對用於生產者,一對用於消費者。就是下面段落中圖標提到的prod_head,prod_tail,cons_head 和cons_tail。

每一個圖都表明了環形緩衝區的一個狀態。函數本地變量在圖的上方,底下則是ring結構體的內容。

4.5.1 單生產者入隊列

這部分講的是單個生產者將一個對象加入到ring中發生的事。在這個例子中,只有生產者的head和tail(prod_head與prod_tail)被修改了,有且只有一個生產者啊。

初始狀態,prod_head和prod_tail指向同一個位置。

入隊列第一步

首先,ring->prod_head和ring->cons_tail的值拷貝到本地變量,prod_next本地變量會指向表中的下一個元素位置,或者是bulk入隊列後的幾個元素

若是ring中沒有足夠的空間(經過檢查cons_tail來判斷),返回error。

入隊列第二步

第二步就是修改ring結構體中ring->prod_head的值指向prod_next指向的位置。

指向要添加對象的指針被拷貝到ring中(圖中obj4)。

入隊列最後一步

一旦對象唄加入到ring中,ring->prod_tail就修改爲與ring->prod_head指向的相同位置。入隊列操做完成。

 

Fig. 4.2: Enqueue first step

 

Fig. 4.3: Enqueue second step

 

Fig. 4.4: Enqueue last step

4.5.2 單消費者出隊列

這段講解了當單消費者從ring中出隊列一個對象時發生了啥。在這個例子中,只有ring結構體中消費者的頭尾(cons_head和cons_tail)修改,當且僅當是一個消費者啊。

初始狀態下,cons_head和cons_tail指向相同的位置。

出隊列第一步

首先,ring->cons_head和ring->prod_tail會拷貝到本地變量cons_head和prod_tail。而本地變量cons_next會指向表中的下一個對象(就是cons_head指向的對象的下一個),或者是bulk出隊列的多個對象的下一個。

若是ring中沒有足夠的對象(經過檢測prod_tail),返回errors。

出隊列第二步

第二步就是修改ring->cons_head指向和cons_next指向的相同位置。

要刪除的對象(obj1)指針會拷貝到用戶提供的指針上。

 

Fig. 4.5: Dequeue last step

 

Fig. 4.6: Dequeue second step

出隊列最後一步

最後,ring->cons_tail修改爲ring->cons_head相同的值。出隊列操做完成。

 

Fig. 4.7: Dequeue last step

4.5.3 多生產者入隊列

這部分講了兩個生產者同時添加一個對象到ring中發生的事。在本例中,只有生產者的頭尾(prod_head和prod_tail)被修改了。

初始狀態prod_head和prod_tail執行同一個位置。

對生產者入隊列第一步(原文是消費者,整個例子下面都是用的消費者,估計是弄錯了)

在兩個核上,ring->prod_head和ring->cons_tail都拷貝到本地變量。本地變量prod_next執行表中下一個對象位置,或者是bulk入隊列的多個對象的下一個位置。

若是沒有足夠的空間(檢查prod_tail)就返回error。

 

Fig. 4.8: Multiple consumer enqueue first step

多生產者入隊列第二步

第二步就是修改ring結構體的ring->prod_head指向prod_next指向的位置。這個操做時用原子操做Compare And Swap(CAS)指令,其原子的執行下面的操做:

l  若是ring->prod_head不一樣於本地變量prod_head,CAS操做失敗,代碼從新執行第一步。

l  不然,ring->prod_head設置成本地的prod_next值,CAS操做成功,繼續執行。

在途中,這個操做在覈1上執行成功,在覈2上失敗從新執行第一步。

 

 

多生產者入隊列第三步

CAS操做不停的嘗試直到在覈2上成功。

核1更新對象(obj4)到ring中,核2更新對象(obj5)到ring中。

 

Fig. 4.10: Multiple consumer enqueue third step

 

多生產者入隊列第四步

每一個核都要更新ring->prod_tail。那個核上ring->prod_tail等於本地變量prod_head的才能更新。這個在本例中只有核1上能夠,本步操做在覈1上完成。

 

 

多生產者入隊列最後一步

一旦ring->prod_tail被核1更新完成,核2就能夠更新了。這個操做總在在覈2上完成。

 

4.5.4  無符號32位索引

在先前的圖中,prod_head,prod_tail,cons_head,cons_tail的值都是被一個箭頭表明。在實際使用中,這些值不是在0到ring的大小減去1之間增長,這是假設的狀況。值實際是在0到2^32-1之間變化,咱們在訪問這個值指向的指針表位置(就是ring自身)時會對這個值作掩碼運算。32位無符號代表對這個數的操做(例如加和減)將自動對超出32位數字範圍的數取2^32取模。

下面兩個例子解釋了無符號索引數在ring中如何使用的。

注意:爲了簡單化,咱們將32位的操做用16位的操做示例代替。那4個索引值就用16位無符號整數來定義,與32位無符號整數實際上差很少。

ring包含11000個對象

ring包含12536個對象,

注意:爲了簡單理解,在上面的例子中咱們都是用的65536取模操做。可是在實際的執行時,這是多餘而低效的,由於cpu會自動在溢出時作這個。

代碼總會保持生產者和消費者索引之間的位置差距在0和sizeof(ring)-1之間。感謝這個特性,咱們能夠在兩個索引值之間作以32位模爲基礎的減法:這就是爲啥這個索引值溢出不是問題。

在任什麼時候間,ring中使用空間和空閒空間在0和sizeof(ring)-1之間,即便第一個減法已經溢出了。

uint32_t entries = (prod_tail - cons_head);

uint32_t free_entries = (mask + cons_tail -prod_head);

原本懂的,翻譯着差點把本身搞迷糊了。很簡單的東西,就是利用了無符號數的減法原理,prod_tail-cons_head就是已經使用的空間,即便出現prod_tail增加超出2^32的範圍,因爲無符號數的特性(涉及到補碼和反碼吧,這個底層的概念忘記是哪一個了…..),它會變成超出數-2^32值,此時prod_tail比cons_head小,可是無符號數的相減仍是會獲得實際的差值。因此就如上所說,即便第一個公式溢出了,始終能獲得使用的空間值。再用總空間mask-(prod_tail - cons_head)就獲得空閒空間值,去掉括號就是第二個公式,看內核的kfifo就一目瞭然了。

     

 

4.6     參考文件

l  bufring.h in FreeBSD (version 8)       http://svnweb.freebsd.org/base/release/8.0.0/sys/sys/buf_ring.h?revision=199625&amp;view=markup

l  bufring.c in FreeBSD (version 8)        http://svnweb.freebsd.org/base/release/8.0.0/sys/kern/subr_bufring.c?revision=199625&amp;view=markup

l  Linux Lockless Ring Buffer Design        http://lwn.net/Articles/340400/

相關文章
相關標籤/搜索