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

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

內核態協議棧的缺陷

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

互斥上鎖引起的開銷

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

報文造成的處理效率低下

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

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

頻繁的短連接會引起大量的用戶態/內核態模式切換,頻繁的上下文切換會造成更多的Cache Miss

用戶態協議棧的引入

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

mtcp

通過利用已有的高性能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親和力)以最大程度利用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進行控制平面的通信

在這裏插入圖片描述

每一個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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章