linux源碼解讀(三十二):dpdk原理概述(一)


    1、操作系統、計算機網絡誕生已經幾十年了,部分功能不再能滿足現在的業務需求。如果對操作系統做更改,成本非常高,所以部分問題是在應用層想辦法解決的,比如前面介紹的協程、quic等,都是在應用層重新開發的框架,簡單回顧如下:

  • 協程:server多線程通信時,如果每連接一個客戶端就要生成一個線程去處理,對server硬件資源消耗極大!為了解決多線程以及互相切換帶來的性能損耗,應用層發明了協程框架:單線程人為控制跳轉到不同的代碼塊執行,避免了cpu浪費、線程鎖/切換等一系列耗時的問題!
  • quic協議:tcp協議已經深度嵌入了操作系統,更改起來難度很大,所以同樣也是在應用層基於udp協議實現了tls、擁塞控制等,徹底讓協議和操作系統松耦合!

   除了上述問題,操作還有另一個比較嚴重的問題:基於os內核的網絡數據IO!傳統做網絡開發時,接收和發送數據用的是操作系統提供的receive和send函數,用戶配置一下網絡參數、傳入應用層的數據即可!操作系統由於集成了協議棧,會在用戶傳輸的應用層數據前面加上協議不同層級的包頭,然后通過網卡發送數據;接收到的數據處理方式類似,按照協議類型一層一層撥開,直到獲取到應用層的數據!整個流程大致如下:

  網卡接受數據----->發出硬件中斷通知cpu來取數據----->os把數據復制到內存並啟動內核線程
         --->軟件中斷--->內核線程在協議棧中處理包--->處理完畢通知用戶層

  大家有沒有覺得這個鏈條忒長啊?這么長的處理流程帶來的問題:

  • “中間商”多,整個流程耗時;數據進入下一個環節時容易cache miss
  • 同一份數據在內存不同的地方存儲(緩存內存、內核內存、用戶空間的內存),浪費內存
  • 網卡通過中斷通知cpu,每次硬中斷大約消耗100微秒,這還不算因為終止上下文所帶來的Cache Miss(L1、L2、TLB等cpu的cache可能都會更新)
  • 用戶到內核態的上下文切換耗時
  • 數據在內核態用戶態之間切換拷貝帶來大量CPU消耗,全局鎖競爭
  • 內核工作在多核上,為保障全局一致,即使采用Lock Free,也避免不了鎖總線、內存屏障帶來的性能損耗

     這一系列的問題都是內核處理網卡接收到的數據導致的。大膽一點想象:如果不讓內核處理網卡數據了?能不能避免上述各個環節的損耗了?能不能讓3環的應用直接控制網卡收發數據了?

       2、如果真的通過3環應用層直接讀寫網卡,面臨的問題:

  •    用戶空間的內存要映射到網卡,才能直接讀寫網卡
  •    驅動要運行在用戶空間

     (1)這兩個問題是怎么解決的了?這一切都得益於linux提供的UIO機制! UIO 能夠攔截中斷,並重設中斷回調行為(相當於hook了,這個功能還是要在內核實現的,因為硬件中斷只能在內核處理),從而繞過內核協議棧后續的處理流程。這里借用別人的一張圖:

         

   UIO 設備的實現機制其實是對用戶空間暴露文件接口,比如當注冊一個 UIO 設備 uioX,就會出現文件 /dev/uioX(用於讀取中斷,底層還是要在內核處理,因為硬件中斷只能發生在內核),對該文件的讀寫就是對設備內存的讀寫(通過mmap實現)。除此之外,對設備的控制還可以通過 /sys/class/uio 下的各個文件的讀寫來完成。所以UIO的本質:

  •  讓用戶空間的程序攔截內核的中斷,更改中斷的handler處理函數,讓用戶空間的程序第一時間拿到剛從網卡接收到的“一手、熱乎”數據,減少內核的數據處理流程!由於應用程序拿到的是網絡鏈路層(也就是第二層)的數據,這就需要應用程序自己按照協議解析數據了!說個額外的:這個功能可以用來抓包

  簡化后的示意圖如下:原本網卡是由操作系統內核接管的,現在直接由3環的dpdk應用控制了!

     

   這就是dpdk的第一個優點;除了這個,還有以下幾個:

   (2)Huge Page 大頁:傳統頁面大小是4Kb,如果進程要使用64G內存,則64G/4KB=16000000(一千六百萬)頁,所有在頁表項中占用16000000 * 4B=62MB;但是TLB緩存的空間是有限的,不可能存儲這么多頁面的地址映射關系,所以可能導致TLB miss;如果改成2MB的huge Page,所需頁面減少到64G/2MB=2000個。在TLB容量有限的情況下盡可能地多在TLB存放地址映射,極大減少了TLB miss!下圖是采用不同大小頁面時TLB能覆蓋的內存對比!

        

   (3)mempool 內存池:任何網絡協議都要處理報文,這些報文肯定是存放在內存的!申請和釋放內存就需要調用malloc和free函數了!這兩個是系統調用,涉及到上下文切換;同時還要用buddy或slab算法查找空閑內存塊,效率較低!dpdk 在用戶空間實現了一套精巧的內存池技術,內核空間和用戶空間的內存交互不進行拷貝,只做控制權轉移。當收發數據包時,就減少了內存拷貝的開銷!

 (4)Ring 無鎖環:多線程/多進程之間互斥,傳統的方式就是上鎖!但是dpdk基於 Linux 內核的無鎖環形緩沖 kfifo 實現了自己的一套無鎖機制,支持多消費者或單消費者出隊、多生產者或單生產者入隊;

   (5)PMD poll-mode網卡驅動:網絡IO監聽有兩種方式,分別是

  • 事件驅動,比如epoll:這種方式進程讓出cpu后等數據;一旦有了數據,網卡通過中斷通知操作系統,然后喚醒進程繼續執行!這種方式適合於接收的數據量不大,但實時性要求高的場景;
  • 輪詢,比如poll:本質就是用死循環不停的檢查內存有沒有數據到來!這種方式適合於接收大塊數據,實時性要求不高的場景;

  總的來說說:中斷是外界強加給的信號,必須被動應對,而輪詢則是應用程序主動地處理事情。前者最大的影響就是打斷系統當前工作的連續性,而后者則不會,事務的安排自在掌握!

  dpdk采用第二種輪詢方式:直接用死循環不停的地檢查網卡內存,帶來了零拷貝、無系統調用的好處,同時避免了網卡硬件中斷帶來的上下文切換(理論上會消耗300個時鍾周期)、cache miss、硬中斷執行等損耗

     (6)NUMA:dpdk 內存分配上通過 proc 提供的內存信息,使 CPU 核心盡量使用靠近其所在節點的內存,避免了跨 NUMA 節點遠程訪問內存的性能問題;其軟件架構去中心化,盡量避免全局共享,帶來全局競爭,失去橫向擴展的能力

  (7)CPU 親和性: dpdk 利用 CPU 的親和性將一個線程或多個線程綁定到一個或多個 CPU 上,這樣在線程執行過程中,就不會被隨意調度,一方面減少了線程間的頻繁切換帶來的開銷,另一方面避免了 CPU L1、L2、TLB等緩存的局部失效性,增加了 CPU cache的命中率。

   3、一個簡單的數據接收demo,主要是對網絡中的數據進行一層一層解包:

int main(int argc, char *argv[])
{
    
    // 初始化環境,檢查內存、CPU相關的設置,主要是巨頁、端口的設置
    if (rte_eal_init(argc, argv) < 0)
    {
    
        rte_exit(EXIT_FAILURE, "Error with EAL init\n");
    }

    // 內存池初始化,發送和接收的數據都在內存池里
    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
        "mbuf pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
    if (NULL == mbuf_pool)
    {
    
        rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
    }

    // 啟動dpdk
    ng_init_port(mbuf_pool);

    while (1)
    {
    
        // 接收數據
        struct rte_mbuf *mbufs[BURST_SIZE];
        unsigned num_recvd = rte_eth_rx_burst(gDpdkPortId, 0, mbufs, BURST_SIZE);
        if (num_recvd > BURST_SIZE)
        {
    
            // 溢出
            rte_exit(EXIT_FAILURE, "Error receiving from eth\n");
        }

        // 對mbuf中的數據進行處理
        unsigned i = 0;
        for (i = 0; i < num_recvd; i++)
        {
    
            // 得到以太網中的數據
            struct rte_ether_hdr *ehdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
            // 如果不是ip協議
            if (ehdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4))
            {
    
                continue;
            }
            struct rte_ipv4_hdr *iphdr =
                rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));

            // 接收udp的數據幀
            if (iphdr->next_proto_id == IPPROTO_UDP)
            {
    
                struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);

                uint16_t length = ntohs(udphdr->dgram_len);
                // udp data copy to buff
                uint16_t udp_data_len = length - sizeof(struct rte_udp_hdr) + 1;
                char buff[udp_data_len];
                memset(buff, 0, udp_data_len);
                --udp_data_len;
                memcpy(buff, (udphdr + 1), udp_data_len);

                //源地址
                struct in_addr addr;
                addr.s_addr = iphdr->src_addr;
                printf("src: %s:%d, ", inet_ntoa(addr), ntohs(udphdr->src_port));

                //目的地址+數據長度+數據內容
                addr.s_addr = iphdr->dst_addr;
                printf("dst: %s:%d, %s\n",
                       inet_ntoa(addr), ntohs(udphdr->dst_port), buff);

                // 用完放回內存池
                rte_pktmbuf_free(mbufs[i]);
            }
        }
    }
}

 

 

 

 

參考:

1、https://cloud.tencent.com/developer/article/1198333  一文看懂dpdk

4、https://lwn.net/Articles/232575/  uio機制
5、https://chowdera.com/2021/12/202112162035343569.html dpdk示例
6、https://cloud.tencent.com/developer/article/1736535 dpdk內存池


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM