Switch-Router

走进 mTCP

Published at 2019-03-04 | Last Update 2019-03-04

mTCP 是一款面向多核系统的用户态网络协议栈

内核态协议栈的缺陷

互联网的发展,使得用户对网络应用的性能需求越来越高。人们不断挖掘CPU处理能力加强,添加核的数量,但这并没有使得网络设备的吞吐率线性增加,其中一个原因是内核协议栈成为了限制网络性能提升的瓶颈。

互斥上锁引起的开销

互斥上锁是多核平台性能的第一杀手。现在的服务器端应用为了尽可能的实现高并发,通常都是采用多线程的方式监听客户端对服务端口发起的连接请求。首先,这会造成多个线程之间对accept队列的互斥访问。其次,线程间对文件描述符空间的互斥访问也会造成性能下降。

报文造成的处理效率低下

内核中协议栈处理数据报文都是逐个处理的, 缺少批量处理的能力。

频繁的系统调用引起的负担

频繁的短连接会引起大量的用户态/内核态模式切换,频繁的上下文切换会造成更多的Cache Miss

用户态协议栈的引入

用户态协议栈-即是将原本由内核完成了协议栈功能上移至用户态实现。

用户态协议栈

通过利用已有的高性能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