走進 mTCP

mTCP 是一款面向多核系統的用戶態網絡協議棧

內核態協議棧的缺陷

互聯網的發展,使得用戶對網絡應用的性能需求愈來愈高。人們不斷挖掘CPU處理能力增強,添加核的數量,但這並無使得網絡設備的吞吐率線性增長,其中一個緣由是內核協議棧成爲了限制網絡性能提高的瓶頸。node

互斥上鎖引發的開銷

互斥上鎖是多核平臺性能的第一殺手。如今的服務器端應用爲了儘量的實現高併發,一般都是採用多線程的方式監聽客戶端對服務端口發起的鏈接請求。首先,這會形成多個線程之間對accept隊列的互斥訪問。其次,線程間對文件描述符空間的互斥訪問也會形成性能降低。git

報文形成的處理效率低下

內核中協議棧處理數據報文都是逐個處理的, 缺乏批量處理的能力。程序員

頻繁的系統調用引發的負擔

頻繁的短鏈接會引發大量的用戶態/內核態模式切換,頻繁的上下文切換會形成更多的Cache Missgithub

用戶態協議棧的引入

用戶態協議棧-便是將本來由內核完成了協議棧功能上移至用戶態實現。編程

用戶態協議棧

經過利用已有的高性能Packet IO庫 (以DPDK爲例)旁路內核,用戶態協議棧能夠直接收發網絡報文,而沒有報文處理時用戶態/內核態的模式切換。除此以外,因爲徹底在用戶態實現,因此具備更好的可擴展性仍是可移植性。緩存

mTCP 介紹

mTCP做爲一種用戶態協議棧庫的實現,其在架構以下圖所示:服務器

mtcp

mTCP以函數庫的形式連接到應用進程,底層使用其餘用戶態的Packet IO庫。網絡

總結起來,mTCP具備如下特性:數據結構

  • 良好的多核擴展性
  • 批量報文處理機制
  • epoll事件驅動系統
  • BSD風格的socket API
  • 支持多種用戶態Packet IO
  • 傳輸層協議僅支持TCP

多核擴展性

爲了不多線程訪問共享的資源帶來的開銷。mTCP將全部資源(如flow pool socket buffer)都按核分配,即每一個核都有本身獨有的一份。而且,這些數據結構都是cache對齊的。多線程

從上面的架構圖能夠看到,mTCP須要爲每個用戶應用線程(如Thread0)建立一個額外的一個線程(mTCP thread0)。這兩個線程都被綁定到同一個核(設置CPU親和力)以最大程度利用CPUCache

批量報文處理機制

因爲內部新增了線程,所以mTCP在將報文送給用戶線程時,不可避免地須要進行線程間的通訊,而一次線程間的通訊可比一次系統調用的代價高多了。所以mTCP採用的方法是批量進行報文處理,這樣平均下來每一個報文的處理代價就小多了。

epoll事件驅動系統

對於習慣了使用epoll編程的程序員來講,mTCP太友好了,你須要作就是把epoll_xxx()換成mtcp_epoll_xxx()

BSD 風格的 socket API

一樣的,應用程序只須要把BSD風格的Socket API前面加上mtcp_ 就足夠了,好比mtcp_accept()

支持多種用戶態Packet IO庫

mTCP中, Packet IO庫也被稱爲IO engine, 當前版本(v2.1)mTCP支持DPDK(默認)、 netmaponvmpsio 四種IO engine

mTCP的一些實現細節

線程模型

如前所述mTCP須要會爲每一個用戶應用線程建立一個單獨的線程,而這實際上須要每一個用戶應用線程顯示調用下面的接口完成。

mctx_t mtcp_create_context(int cpu);

這以後,每一個mTCP線程會進入各自的Main Loop,每一對線程經過mTCP建立的緩衝區進行數據平面的通訊,經過一系列Queue進行控制平面的通訊

threadmodel

每個mTCP線程都有一個負責管理資源的結構struct mtcp_manager, 在線程初始化時,它完成資源的建立,這些資源都是屬於這個核上的這個線程的,包括保存鏈接四元組信息的flow table,套接字資源池socket pool監聽套接字listener hashtable,發送方向的控制結構sender等等

用戶態 Socket

既然是純用戶態協議棧,那麼全部套接字的操做都不是用glibc那一套了,mTCP使用socket_map表示一個套接字,看上去是否是比內核的那一套簡單多了!

struct socket_map
{
    int id;
    int socktype;
    uint32_t opts;

    struct sockaddr_in saddr;

    union {
        struct tcp_stream *stream;
        struct tcp_listener *listener; 
        struct mtcp_epoll *ep;
        struct pipe *pp;
    };

    uint32_t epoll;            /* registered events */
    uint32_t events;        /* available events */
    mtcp_epoll_data_t ep_data;

    TAILQ_ENTRY (socket_map) free_smap_link;
};

其中的socketype表示這個套接字結構的類型,根據它的值,後面的聯合體中的指針也就能夠解釋成不一樣的結構。注意在mTCP中,咱們一般認爲的文件描述符底層也對應這樣一個socket_map

enum socket_type
{
    MTCP_SOCK_UNUSED, 
    MTCP_SOCK_STREAM, 
    MTCP_SOCK_PROXY, 
    MTCP_SOCK_LISTENER,   
    MTCP_SOCK_EPOLL, 
    MTCP_SOCK_PIPE, 
};

用戶態 Epoll

mTCP實現的epoll相對於內核版本也簡化地多,控制結構struct mtcp_epoll以下:

struct mtcp_epoll
{
    struct event_queue *usr_queue;
    struct event_queue *usr_shadow_queue;
    struct event_queue *mtcp_queue;

    uint8_t waiting;
    struct mtcp_epoll_stat stat;
    
    pthread_cond_t epoll_cond;
    pthread_mutex_t epoll_lock;
};

它內部保存了三個隊列,分別存儲發生了三種類型的事件的套接字。

  • MTCP_EVENT_QUEUE表示協議棧產生的事件,好比LISTEN狀態的套接字accept了,ESTABLISH的套接字有數據能夠讀取了
  • USR_EVENT_QUEUE 表示用戶應用的事件,如今就只有PIPE;
  • USR_SHADOW_EVENT_QUEUE表示用戶態因爲沒有處理完,而須要模擬產生的協議棧事件,好比ESTABLISH上的套接字數據沒有讀取完.

TCP流

mTCP使用tcp_stream表示一條端到端的TCP流,其中保存了這條流的四元組信息、TCP鏈接的狀態、協議參數和緩衝區位置。tcp_stream存儲在每線程的flow table

typedef struct tcp_stream
{
    socket_map_t socket;
    
    // code omitted... 
    
    uint32_t saddr;            /* in network order */
    uint32_t daddr;            /* in network order */
    uint16_t sport;            /* in network order */
    uint16_t dport;            /* in network order */
    
    uint8_t state;            /* tcp state */
    
    struct tcp_recv_vars *rcvvar;
    struct tcp_send_vars *sndvar;
    
    // code omitted... 
} tcp_stream;

發送控制器

mTCP使用struct mtcp_sender完成發送方向的管理,這個結構是每線程每接口的,若是有2mTCP線程,且有3個網絡接口,那麼一共就有6個發送控制器

struct mtcp_sender
{
    int ifidx;

    /* TCP layer send queues */
    TAILQ_HEAD (control_head, tcp_stream) control_list;
    TAILQ_HEAD (send_head, tcp_stream) send_list;
    TAILQ_HEAD (ack_head, tcp_stream) ack_list;

    int control_list_cnt;
    int send_list_cnt;
    int ack_list_cnt;
};

每一個控制器內部包含了3個隊列,隊列中元素是 tcp_stream

  • Control 隊列:負責緩存待發送的控制報文,好比SYN-ACK報文
  • Send 隊列:負責緩存帶發送的數據報文
  • ACK 隊列:負責緩存純ACK報文

例子:服務端TCP鏈接創建流程

假設咱們的服務端應用在某個應用線程建立了一個epoll套接字和一個監聽套接字,而且將這個監聽套接字加入epoll,應用進程阻塞在mtcp_epoll_wait(),而mTCP線程在本身的main Loop中循環
圖片描述

  1. 本機收到客戶端發起的鏈接,收到第一個SYN報文。mTCP線程在main Loop中讀取底層IO收到該報文, 在嘗試在本線程的flow table搜索後,發現沒有此四元組標識的流信息,因而新建一條tcp stream, 此時,這條流的狀態爲TCP_ST_LISTEN
  2. 將這條流寫入Control隊列,狀態切換爲TCP_ST_SYNRCVD,表示已收到TCP的第一次握手
  3. mTCP線程在main Loop中讀取Control隊列,發現其中有剛剛的這條流,因而將其取出,組裝SYN-ACK報文,送到底層IO
  4. mTCP線程在main Loop中讀取底層收到的對端發來這條流的ACK握手信息,將狀態改成TCP_ST_ESTABLISHED(TCP的三次握手完成),而後將這條流塞入監聽套接字的accept隊列
  5. 因爲監聽套接字是加入了epoll的,所以mTCP線程還會將一個MTCP_EVENT_QUEUE事件塞入struct mtcp_epollmtcp_queue隊列。
  6. 此時用戶線程在mtcp_epoll_wait()就能讀取到該事件,而後調用mtcp_epoll_accept()Control隊列讀取到鏈接信息,就能完成鏈接的創建。

參考資料

mTCP: a Highly Scalable User-level TCP Stack for Multicore Systems
mTCP Github Repo

擴展資料

內核協議棧的優化方案 FastSocket
另外一種用戶態協議棧 F-stack

相關文章
相關標籤/搜索