mTCP
是一款面向多核系統的用戶態網絡協議棧
內核態協議棧的缺陷
互聯網的發展,使得用戶對網絡應用的性能需求越來越高。人們不斷挖掘CPU處理能力加強,添加核的數量,但這並沒有使得網絡設備的吞吐率線性增加,其中一個原因是內核協議棧成爲了限制網絡性能提升的瓶頸。
互斥上鎖引起的開銷
互斥上鎖是多核平臺性能的第一殺手。現在的服務器端應用爲了儘可能的實現高併發,通常都是採用多線程的方式監聽客戶端對服務端口發起的連接請求。首先,這會造成多個線程之間對accept
隊列的互斥訪問。其次,線程間對文件描述符空間的互斥訪問也會造成性能下降。
報文造成的處理效率低下
內核中協議棧處理數據報文都是逐個處理的, 缺少批量處理的能力。
頻繁的系統調用引起的負擔
頻繁的短連接會引起大量的用戶態/內核態模式切換,頻繁的上下文切換會造成更多的Cache Miss
用戶態協議棧的引入
用戶態協議棧-即是將原本由內核完成了協議棧功能上移至用戶態實現。
通過利用已有的高性能Packet IO
庫 (以DPDK
爲例)旁路內核,用戶態協議棧可以直接收發網絡報文,而沒有報文處理時用戶態/內核態的模式切換。除此之外,由於完全在用戶態實現,所以具有更好的可擴展性還是可移植性。
mTCP 介紹
mTCP
作爲一種用戶態協議棧庫的實現,其在架構如下圖所示:
mTCP
以函數庫的形式鏈接到應用進程,底層使用其他用戶態的Packet IO
庫。
總結起來,mTCP
具有以下特性:
- 良好的多核擴展性
- 批量報文處理機制
- 類
epoll
事件驅動系統 BSD
風格的socket API
- 支持多種用戶態
Packet IO
庫 - 傳輸層協議僅支持
TCP
多核擴展性
爲了避免多線程訪問共享的資源帶來的開銷。mTCP
將所有資源(如flow pool
socket buffer
)都按核分配,即每個核都有自己獨有的一份。並且,這些數據結構都是cache
對齊的。
從上面的架構圖可以看到,mTCP
需要爲每一個用戶應用線程(如Thread0
)創建一個額外的一個線程(mTCP thread0
)。這兩個線程都被綁定到同一個核(設置CPU
親和力)以最大程度利用CPU
的Cache
。
批量報文處理機制
由於內部新增了線程,因此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
(默認)、 netmap
、onvm
、 psio
四種IO engine
。
mTCP的一些實現細節
線程模型
如前所述mTCP
需要會爲每個用戶應用線程創建一個單獨的線程,而這實際上需要每個用戶應用線程顯示調用下面的接口完成。
mctx_t mtcp_create_context(int cpu);
這之後,每個mTCP
線程會進入各自的Main Loop
,每一對線程通過mTCP
創建的緩衝區進行數據平面的通信,通過一系列Queue
進行控制平面的通信
每一個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
完成發送方向的管理,這個結構是每線程每接口的,如果有2個mTCP
線程,且有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
中循環
- 本機收到客戶端發起的連接,收到第一個
SYN
報文。mTCP
線程在main Loop
中讀取底層IO
收到該報文, 在嘗試在本線程的flow table
搜索後,發現沒有此四元組標識的流信息,於是新建一條tcp stream
, 此時,這條流的狀態爲TCP_ST_LISTEN - 將這條流寫入
Control
隊列,狀態切換爲TCP_ST_SYNRCVD,表示已收到TCP
的第一次握手 mTCP
線程在main Loop
中讀取Control
隊列,發現其中有剛剛的這條流,於是將其取出,組裝SYN-ACK
報文,送到底層IO
mTCP
線程在main Loop
中讀取底層收到的對端發來這條流的ACK
握手信息,將狀態改爲TCP_ST_ESTABLISHED(TCP的三次握手完成),然後將這條流塞入監聽套接字的accept
隊列- 由於監聽套接字是加入了
epoll
的,因此mTCP
線程還會將一個MTCP_EVENT_QUEUE
事件塞入struct mtcp_epoll
的mtcp_queu
e隊列。 - 此時用戶線程在
mtcp_epoll_wait()
就能讀取到該事件,然後調用mtcp_epoll_accept()
從Control
隊列讀取到連接信息,就能完成連接的建立。
參考資料
mTCP: a Highly Scalable User-level TCP Stack for Multicore Systems
mTCP Github Repo
擴展資料
內核協議棧的優化方案 FastSocket
另一種用戶態協議棧 F-stack