本系列記錄了做者在項目過程當中因爲好奇心驅使而瞭解到的部分DPDK
實現細節。比較適合有一樣好奇心的DPDK
的初學者,經過本文html
您能夠學習到編程
您不能學習到segmentfault
若是對DPDK
的起源不是很清楚的話,能夠先瀏覽下 絕對乾貨!初學者也能看懂的DPDK解析,重點就是Linux + x86網絡IO瓶頸 這部分,總結一句話就是Linux內核協議棧太慢了,爲了突破這種性能瓶頸,DPDK
的方案是繞過(bypass)內核,直接從網卡把數據抓到用戶空間。數組
首先必須明白的一點就是,DPDK
是以若干個lib的形式提供給應用連接使用,其中最終要的一個lib就是EAL
了,EAL
的全稱是(Environment Abstraction Layer, 環境抽象層),它負責爲應用間接訪問底層的資源,好比內存空間、線程、設備、定時器等。若是把咱們的應用比做一個豪宅的主人的話,EAL
就是這個豪宅的管家。緩存
這兩個概念在 DPDK
的代碼中隨處可見,注意這裏的 socket 不是網絡編程裏面的那一套東西,而是CPU相關的東西。具體的概念能夠參看Differences between physical CPU vs logical CPU vs Core vs Thread vs Socket 或者其翻譯版本physical CPU vs logical CPU vs Core vs Thread vs Socket(翻譯)。網絡
對咱們來講,只要知道能夠DPDK
能夠運行在多個lcore
上就足夠了.數據結構
DPDK
如何知道有多少個lcore
呢 ? 在啓動時解析文件系統中的特定文件就能夠了, 參考函數eal_cpu_detected
多線程
大部分DPDK
的代碼是以lib的形式運行在用戶應用的進程上下文.爲了達到更高的性能。應用一般都會多進程或者多線程的形式運行在不一樣的lcore
上app
多線程的場景:框架
多進程的場景:
多進程的場景下,多個應用實例如何保證關鍵信息(好比內存資源)的一致性呢? 答案是不一樣進程將公共的數據mmap
同一個文件,這樣任何一個進程對數據的修改均可以影響到其餘進程。
多進程場景下,進程有兩種角色Primary
或者Secondary
,正如其名字,Primary
進程能夠create 資源,而Secondary
進程只能 attach已存在的資源。一山不容二虎,一個多進程的應用,有且只有一個Primary
進程,其他都是Secondary
進程。應用能夠經過命令行參數 --proc-type 來指定應用類型。
如同main
函數在應用程序中的地位,rte_eal_init
函數即是DPDK
夢開始的地方(其實前面的圖已經畫出來了!),咱們來看看它作了什麼事。
/* Launch threads, called at application init(). */ int rte_eal_init(int argc, char **argv) { thread_id = pthread_self(); rte_eal_cpu_init(); eal_parse_args(argc, argv); rte_config_init(); rte_mp_channel_init(); rte_eal_intr_init(); rte_eal_memzone_init(); rte_eal_memory_init(); rte_eal_malloc_heap_init() eal_thread_init_master(rte_config.master_lcore); RTE_LCORE_FOREACH_SLAVE(i) { pipe(lcore_config[i].pipe_master2slave); pipe(lcore_config[i].pipe_slave2pipe); /* create a thread for each lcore */ ret = pthread_create(&lcore_config[i].thread_id, NULL, eal_thread_loop, NULL); ..... } /* * Launch a dummy function on all slave lcores, so that master lcore * knows they are all ready when this function returns. */ rte_eal_mp_remote_launch(sync_func, NULL, SKIP_MASTER); rte_eal_mp_wait_lcore(); ...... }
rte_eal_init
總結起來乾的工做就是
lcore
是可使用的slave lcore
上啓動線程上面提到了一個概念是slave lcore
,與之對應的是master lcore
,在一個運行在多個lcore
的DPDK
應用中,啓動線程運行的lcore
是master lcore
,其他都是slave lcore
,master lcore
和全部slave lcore
之間經過pipe進行通訊,拓撲上組成一個星型網絡。
每一個lcore
的狀態和配置記錄在全局變量 lcore_config 中,這是一個數組,每一個lcore
只會訪問本身的那一份
struct lcore_config lcore_config[RTE_MAX_LCORE]
多進程的狀況稍微複雜一些,除了線程間的通訊外,還要完成primary
進程和其餘secondary
進程的通訊。這是經過在
剛纔那一堆子模塊初始化中的下面函數完成的(mp
表示multiple process
),其內部會單首創建一個線程用來接收來自其餘進程的消息。
int rte_mp_channel_init(void)
DPDK
要高速處理網絡報文,報文須要內存來承載,因此DPDK
天然免不了就是頻繁的內存申請釋放。顯然,若是在須要內存時 malloc, 不須要時 free ,那麼這個效率過低了。所以DPDK
使用內存池來負責內存申請釋放,相關的數據結構主要有rte_memzone
rte_ring
和rte_mempool
。
先將通常狀況下,三者之間的關係畫出來
rte_memzone
在DPDK
的內存資源管理中起到的是其餘資源管家的做用,默認狀況下,在DPDK
初始化時會建立RTE_MAX_MEMZONE
個rte_memzone
,每個均可以記錄一個rte_ring
或者rte_mempool
的內存位置。從上面的圖中也能夠看到每個rte_ring
或者rte_mempool
都有一個指針回指到它關聯的rte_memzone
rte_ring
描述了一個循環隊列,它有如下特色
void*
)
如上圖所示,每一個rte_ring
內部包含了兩對遊標用以記錄當前rte_ring
的的存儲狀態,之因此用兩對而不是兩個的緣由是一是爲了支持多消費者模型和多生產者模型,二是爲了支持批量存取。
這裏僅以多生產者競爭下入隊列的場景說明rte_ring
是如何工做的,其中上面的方框表示兩個 core 上的本地遊標,下面的方框表示這個rte_ring
內部記錄的遊標
注意:這裏的每一個 core 既適用於多線程也適用於多進程
每一個 core 將 ring->proc_head 拷貝到本地 proc_head ,再將 proc_next 設置爲下一個位置
嘗試修改 ring->proc_head 爲 proc_next 的值,這一步用到了Compare And Swap指令來保證原子性, 這裏,只有當 ring->proc_head 與 proc_head 相等時這個操做纔會成功,不然從新進行 Step1 。在本例子中,假設在 Core 1 上的操做成功了。在 Core 2 上操做時,因爲 ring->proc_head 已經與本地的 proc_head 的不相等的了,因此不會成功,而是從新進行 Step1 的拷貝。
Core 2 上的操做成功,將內容(一個指針)寫入 rte_ring
接下來就是要嘗試更新 ring->proc_tail ,這一步一樣用到了Compare And Swap,只有當 ring->proc_tail 與本地的 proc_tail 相同時才能成功,更新爲本地的 proc_head 在本例中,顯然只有在 Core 1 上才能成功。
最後, 再將 ring->proc_tail 更新爲 Core 2 上的 proc_head。
其餘場景,如 單生產者 單消費者 多消費者的場景請參考
rte_ring
對應用者來講,知道如何使用可能比知道其內部工做原理更有用。rte_ring
主要接口有下面兩個:
建立 rte_ring
struct rte_ring* rte_ring_create(const char* name, unsigned count, int socket_id, unsigned flags);
根據名字,查找已經建立的 rte_ring
struct rte_ring* rte_ring_lookup(const char* name);
通常來講,能夠在 master lcore 或者 primary process 上建立,在 slave lcore 或者 secondary process 上查找。
向rte_ring
存入一個指針(生產者)
int rte_ring_enqueue(struct rte_ring* r, void* obj);
從rte_ring
取出一個指針(消費者)
int rte_ring_dequeue(struct rte_ring* r, void **obj_p);
rte_ring
只能存儲一個指針,而 rte_ring
能夠存儲必定容量的其餘大小元素的數據,但有一點要注意,這個元素大小一樣在建立的時候就要指定,一樣指定的還有容量。
雖然 rte_ring
和 rte_mempool
是兩個獨立的數據結構,但如同上面的關係圖中描述的,通常的 rte_mempool
會內置一個rte_ring
用來管理 rte_mempool
中的元素,我認爲這正是rte_ring
中存儲的是指針的緣由,它指向的內容就是rte_mempool
種內容。
多核場景下,若是兩個線程向同一個rte_mempool
申請或釋放內存,勢必引發對rte_ring
的CAS
操做失敗,所以DPDK
容許用戶在建立rte_mempool
時爲每一個lcore
建立緩存,緩存同rte_ring
同樣存儲的是指針。
因此對於有緩存的的rte_mempool
,它在內存中的佈局以下:
官方文檔中,帶 Cache 的rte_mempool
表示以下:
當一個應用想從rte_mempool
申請內存時,他會首先嚐試從 Cache 中看有沒有爲當前 lcore 預留的內存,若是有就直接使用就行了(這樣不會有競爭),若是沒有再去從rte_ring
獲取。
rte_mempool
對應用程序來講,rte_mempool
主要提供的接口有如下幾個
建立一個標準的 rte_mempool
struct rte_mempool* rte_mempool_create(const char *name, unsigned n, unsigned elt_size, unsigned cache_size, unsigned private_data_size, rte_mempool_ctor_t *mp_init, void *mp_init_arg, rte_mempool_obj_cb_t *obj_init, void *obj_init_arg, int socket_id, unsigned flags);
根據名字 查找一個rte_mempool
.
struct rte_mempool* rte_mempool_lookup(const char *name);
從內存池中獲取一個對象(消費者)
int rte_mempool_get(struct rte_mempool* mp, void **obj_p);
向內存池歸還一個對象
void rte_mempool_put(struct rte_mempool* mpu, void* obj);
建立一個空的rte_mempool
struct rte_mempool* rte_mempool_create_empty(const char *name, unsigned n, unsigned elt_size, unsigned cache_size, unsigned private_data_size, int socket_id, unsigned flags);
空的rte_mempool
是指大部分數據結構的關係已經設置好,但這個rte_mempool
尚未分配池中元素的內存,即用戶是不能從空的rte_mempool
獲得內存,若是用GDB調試的話,能夠看到當建立空的rte_mempool
後,其內置的rte_ring
中 ring->proc_head = ring->proc_tail ,這時咱們還須要使用下 rte_mempool_populate_*() 這類函數真正爲內存池分配內存(這個過程稱爲 populate )。默認的接口以下:
int rte_mempool_populate_default(struct rte_mempool *mp);
因此其實建立非空的rte_mempool
的大體實現是,先建立空的內存池,再爲其中的元素向系統申請內存
struct rte_mempool * rte_mempool_create(const char *name, unsigned n, unsigned elt_size, unsigned cache_size, unsigned private_data_size, rte_mempool_ctor_t *mp_init, void *mp_init_arg, rte_mempool_obj_cb_t *obj_init, void *obj_init_arg, int socket_id, unsigned flags) { mp = rte_mempool_create_empty(name, n, elt_size, cache_size, private_data_size, socket_id, flags); ... rte_mempool_populate_default(mp); }