mTCP是面向多核系统的用户态网络协议栈内核态协议栈缺陷互联网的发展使得用户对网络应用的性能要求越来越高。人们不断提高CPU处理能力,增加内核数量,但这并没有线性增加网络设备的吞吐量。其中一个原因是内核协议栈已经成为限制网络性能提升的瓶颈。互斥和锁定带来的开销互斥和锁定是多核平台性能的头号杀手。为了尽可能的做到高并发,目前的服务器端应用通常采用多线程的方式来监听客户端向服务端口发起的连接请求。首先,这会造成多个线程之间对accept队列的访问互斥。其次,线程间对文件描述符空间的互斥访问也会造成性能下降。报文处理效率低内核中的协议栈是一个一个地处理数据包,缺乏批处理能力。频繁的系统调用带来的负担频繁的短连接会造成大量的用户态/内核态切换,频繁的上下文切换会导致更多的CacheMiss用户态协议栈被引入用户态协议栈——也就是原来的内核完成了协议栈的功能,将其移至用户态执行。利用现有的高性能PacketIO库(以DPDK为例)绕过内核,用户态协议栈可以直接发送和接收网络数据包,而无需在数据包处理过程中切换用户态和内核态。此外,由于完全在用户态下实现,因此具有更好的可扩展性或可移植性。mTCP引入mTCP作为用户模式协议栈库的实现。其架构如下图所示:mTCP以函数库的形式链接到应用进程,底层使用其他用户态PacketIO库。综上所述,mTCP具有以下特点:良好的多核可扩展性批量消息处理机制epoll类事件驱动系统BSD风格的socketAPI支持多种用户模式??PacketIO库传输层协议只支持TCP多核可扩展性避免多线程访问共享资源的开销。mTCP按核心分配所有资源(如流池套接字缓冲区),即每个核心都有自己独特的份额。此外,这些数据结构是缓存对齐的。从上面的架构图可以看出,mTCP需要为每个用户应用线程(比如Thread0)创建一个额外的线程(mTCPthread0)。两个线程都绑定到同一个核心(设置CPUaffinity),以最大限度地利用CPU的Cache。由于批量消息处理机制中新增了内部线程,mTCP在向用户线程发送消息时不可避免地需要进行线程间通信,而线程间通信的成本远高于系统调用。因此,mTCP采用的方法是分批处理数据包,这样平均每个数据包的处理成本要小很多。epoll-like事件驱动系统对于习惯用epoll编程的程序员来说,mTCP太友好了,你只需要把epoll_xxx()换成mtcp_epoll_xxx()BSD风格的socketAPI在前面加上mtcp_就可以了套接字API。例如,mtcp_accept()支持mTCP中的多个用户模式数据包IO库。PacketIO库也称为IO引擎。当前版本(v2.1)mTCP支持DPDK(默认)、netmap、onvm、psio四种IO引擎。mTCP的一些实现细节线程模型如前所述,mTCP需要为每个用户应用线程创建一个单独的线程,而这实际上需要每个用户应用线程显示并调用下面的接口来完成。mctx_tmtcp_create_context(intcpu);之后,每个mTCP线程都会进入自己的MainLoop,每对线程通过mTCP创建的buffer在dataplane上进行通信,在controlplane上通过一系列的Queues进行通信。每个mTCP线程都有一个结构体structmtcp_manager负责管理资源。当线程被初始化时,它就完成了资源的创建。这些资源属于核心上的线程,包括保存连接四元组信息的流表,以及socket资源。Poolsocketpoollisteningsocketlistenerhashtable,sender控制结构sender等。User-modeSocket由于是纯用户态协议栈,所以所有socket操作都不使用glibc。mTCP用socket_map来表示一个Socket,是不是看起来比kernelset简单多了!结构socket_map{intid;袜子类型;uint32_t选择;结构sockaddr_insaddr;union{structtcp_stream*stream;结构tcp_listener*侦听器;结构mtcp_epoll*ep;管道结构*pp;活动;/*可用事件*/mtcp_epoll_data_tep_data;TAILQ_ENTRY(socket_map)free_smap_link;};其中sockettype表示socket结构的类型,根据其取值,后续union中的指针可以解释为不同的结构。注意,在mTCP中,我们通常认为的文件描述符底层也对应这样一个socket_mapenumsocket_type{MTCP_SOCK_UNUSED,MTCP_SOCK_STREAM,MTCP_SOCK_PROXY,MTCP_SOCK_LISTENER,MTCP_SOCK_EPOLL,MTCP_SOCK_PIPE,};用户态EpollmTCP实现的epoll相比内核版本也简化了很多,控制结构structmtcp_epoll如下:structmtcp_epoll{structevent_queue*usr_queue;结构事件队列*usr_shadow_queue;结构事件队列*mtcp_queue;uint8_t等待;结构mtcp_epoll_stat统计;pthread_cond_tepoll_cond;pthread_mutex_tepoll_lock;};它内部保存了三个队列,分别存储了三种类型事件的套接字。MTCP_EVENT_QUEUE表示协议栈产生的事件,比如处于LISTEN状态的socket接受,ESTABLISHsocket有数据可以读取。USR_EVENT_QUEUE表示用户应用程序的事件。现在只有PIPE;USR_SHADOW_EVENT_QUEUE表示用户状态还没有处理。需要模拟生成的协议栈事件,比如ESTABLISH上的socket数据还没有被读取。TCP流mTCP使用tcp_stream表示一个端到端的TCP流,里面存储了这个流和TCP连接的四重信息。状态、协议参数和缓冲区位置。tcp_stream存放在每个线程的流表中typedefstructtcp_stream{socket_map_tsocket;//代码省略...uint32_tsaddr;/*按网络顺序*/uint32_tdaddr;/*按网络顺序*/uint16_tsport;/*按网络顺序*/uint16_tdport;/*按照网络顺序*/uint8_tstate;/*tcp状态*/structtcp_recv_vars*rcvvar;结构tcp_send_vars*sndvar;//省略代码...}tcp_stream;发送控制器mTCP使用structmtcp_sender来完成发送方向的管理,这个结构是perthread和perinterface,如果有2个mTCP线程,有3个网络接口,那么总共有6个发送控制器structmtcp_sender{intifidx;/*TCP层发送队列*/TAILQ_HEAD(control_head,tcp_stream)control_list;TAILQ_HEAD(send_head,tcp_stream)发送列表;TAILQ_HEAD(ack_head,tcp_stream)ack_list;intcontrol_list_cnt;intsend_list_cnt;intack_list_cnt;};就是tcp_stream控制队列:负责缓存要发送的控制包,比如SYN-ACK包发送队列:负责缓存带ACK发送的数据包队列:负责缓存纯ACK包示例:service端到端的TCP连接建立过程假设我们的服务端应用在一个应用线程中创建了一个epollsocket和一个监听socket,并将这个监听socket添加到epoll中,应用进程阻塞在mtcp_epoll_wait()中,mTCP线程循环在它自己的主循环中。本机收到客户端发起的连接,收到第一个SYN报文。mTCP线程在主循环中读取底层IO并接收消息。它正在尝试登录该线程的流表。查找后发现并没有这个四元组标识的流信息,于是新建了一个tcp流。此时这个流的状态为TCP_ST_LISTEN,将流写入Control队列,状态切换为TCP_ST_SYNRCVD,表示收到TCP报文。mTCP线程在第一次握手的主循环中读取Control队列,发现有刚才的stream,就取出来,组装好SYN-ACK报文,发送给IOmTCP底层线程读取在主循环中接收流。对端发送本流的ACK握手信息,将状态变为TCP_ST_ESTABLISHED(TCP的三次握手完成),然后将本流插入到监听套接字的accept队列中,因为监听套接字加入了epoll,所以mTCP线程也会往structmtcp_epoll的mtcp_queue队列中塞一个MTCP_EVENT_QUEUE事件。此时用户线程可以读取mtcp_epoll_wait()中的事件,然后调用mtcp_epoll_accept()从Control队列中读取连接信息,完成连接的建立。参考资料mTCP:aHighlyScalableUser-levelTCPStackforMulticoreSystemsmTCPGithubRepoExtendedDataKernelProtocolStackOptimizationSchemeFastSocketAnotherUser-levelProtocolStackF-stack
