共计 4119 个字符,预计需要花费 11 分钟才能阅读完成。
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_queue 队列。
此时用户线程在 mtcp_epoll_wait()就能读取到该事件,然后调用 mtcp_epoll_accept()从 Control 队列读取到连接信息,就能完成连接的建立。
参考资料
mTCP: a Highly Scalable User-level TCP Stack for Multicore Systems