DPDK 實現的不徹底筆記

寫在前面

本系列記錄了做者在項目過程當中因爲好奇心驅使而瞭解到的部分DPDK實現細節。比較適合有一樣好奇心的DPDK初學者,經過本文html

您能夠學習到編程

  • DPDK的總體工做原理以及部分實現細節

您不能學習到segmentfault

  • 應用DPDK進行性能調優

若是對DPDK的起源不是很清楚的話,能夠先瀏覽下 絕對乾貨!初學者也能看懂的DPDK解析,重點就是Linux + x86網絡IO瓶頸 這部分,總結一句話就是Linux內核協議棧太慢了,爲了突破這種性能瓶頸,DPDK的方案是繞過(bypass)內核,直接從網卡把數據抓到用戶空間。數組

一些基本的概念

EAL

首先必須明白的一點就是,DPDK是以若干個lib的形式提供給應用連接使用,其中最終要的一個lib就是EAL了,EAL的全稱是(Environment Abstraction Layer, 環境抽象層),它負責爲應用間接訪問底層的資源,好比內存空間、線程、設備、定時器等。若是把咱們的應用比做一個豪宅的主人的話,EAL就是這個豪宅的管家。緩存

lcore & socket

這兩個概念在 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的運行形式

大部分DPDK的代碼是以lib的形式運行在用戶應用的進程上下文.爲了達到更高的性能。應用一般都會多進程或者多線程的形式運行在不一樣的lcoreapp

多線程的場景:框架

mutil thread

多進程的場景:

多進程的場景下,多個應用實例如何保證關鍵信息(好比內存資源)的一致性呢? 答案是不一樣進程將公共的數據mmap同一個文件,這樣任何一個進程對數據的修改均可以影響到其餘進程。

multi process

Primary & Secondary

多進程場景下,進程有兩種角色Primary或者Secondary,正如其名字,Primary進程能夠create 資源,而Secondary進程只能 attach已存在的資源。一山不容二虎,一個多進程的應用,有且只有一個Primary進程,其他都是Secondary進程。應用能夠經過命令行參數 --proc-type 來指定應用類型。

DPDK的入口

如同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,在一個運行在多個lcoreDPDK應用中,啓動線程運行的lcoremaster lcore,其他都是slave lcoremaster 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_ringrte_mempool

先將通常狀況下,三者之間的關係畫出來

圖片描述

rte_memzone

rte_memzoneDPDK的內存資源管理中起到的是其餘資源管家的做用,默認狀況下,在DPDK初始化時會建立RTE_MAX_MEMZONErte_memzone,每個均可以記錄一個rte_ring或者rte_mempool的內存位置。從上面的圖中也能夠看到每個rte_ring或者rte_mempool都有一個指針回指到它關聯的rte_memzone

rte_ring

rte_ring描述了一個循環隊列,它有如下特色

  • FIFO 先入先出
  • 隊列的容量在建立以後是固定的,且必定是 2 的整數次冪
  • 隊列中存儲的是指針 (void*)
  • 支持單消費者和多消費者模型
  • 支持單生產者和多生產者模型
  • 支持批量存取

圖片描述
如上圖所示,每一個rte_ring內部包含了兩對遊標用以記錄當前rte_ring的的存儲狀態,之因此用兩對而不是兩個的緣由是一是爲了支持多消費者模型和多生產者模型,二是爲了支持批量存取。

這裏僅以多生產者競爭下入隊列的場景說明rte_ring是如何工做的,其中上面的方框表示兩個 core 上的本地遊標,下面的方框表示這個rte_ring內部記錄的遊標

注意:這裏的每一個 core 既適用於多線程也適用於多進程

Step1

每一個 corering->proc_head 拷貝到本地 proc_head ,再將 proc_next 設置爲下一個位置
ring-mp-enqueue1.svg

Step2

圖片描述
嘗試修改 ring->proc_headproc_next 的值,這一步用到了Compare And Swap指令來保證原子性, 這裏,只有當 ring->proc_headproc_head 相等時這個操做纔會成功,不然從新進行 Step1 。在本例子中,假設在 Core 1 上的操做成功了。在 Core 2 上操做時,因爲 ring->proc_head 已經與本地的 proc_head 的不相等的了,因此不會成功,而是從新進行 Step1 的拷貝。

Step3

圖片描述
Core 2 上的操做成功,將內容(一個指針)寫入 rte_ring

Step4

圖片描述

接下來就是要嘗試更新 ring->proc_tail ,這一步一樣用到了Compare And Swap,只有當 ring->proc_tail 與本地的 proc_tail 相同時才能成功,更新爲本地的 proc_head 在本例中,顯然只有在 Core 1 上才能成功。

Step5

圖片描述
最後, 再將 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_mempool

rte_ring 只能存儲一個指針,而 rte_ring 能夠存儲必定容量的其餘大小元素的數據,但有一點要注意,這個元素大小一樣在建立的時候就要指定,一樣指定的還有容量。

雖然 rte_ringrte_mempool 是兩個獨立的數據結構,但如同上面的關係圖中描述的,通常的 rte_mempool會內置一個rte_ring用來管理 rte_mempool中的元素,我認爲這正是rte_ring中存儲的是指針的緣由,它指向的內容就是rte_mempool種內容。

Local Cache

多核場景下,若是兩個線程向同一個rte_mempool申請或釋放內存,勢必引發對rte_ringCAS操做失敗,所以DPDK容許用戶在建立rte_mempool時爲每一個lcore建立緩存,緩存同rte_ring同樣存儲的是指針。

因此對於有緩存的的rte_mempool,它在內存中的佈局以下:
圖片描述

官方文檔中,帶 Cacherte_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_ringring->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);
}
相關文章
相關標籤/搜索