一直以来,网络协议栈都和内核密切相关,内核作为操作系统的控制者,也是负责网络资源分配的最佳人选。随着Linux系统的不断壮大,内核协议栈的功能性能和稳定性都得到了高度认可。

现在互联网业务蒸蒸日上,本该是性能瓶颈的网络传输,被聪明的开发者们通过集群、分布式等方式不断优化,将网络业务的力量不断提高。但随之而来,Linux内核作为一个调度者,不适宜对外提供业务服务,也不适合占用系统资源,无论多么巧妙的开发技巧,都存在面对单点性能瓶颈的问题。

而用户态协议栈,可以让Linux内核更专注系统的控制调度;将复杂的协议处理放到用户态,使用更多的系统资源,提供给开发者更自由的环境,做更多更酷的的事。

自己之前做过mTCP在x86和ARM(需要对mTCP的IO部分做一些修改才能在ARM下编译运行)下的测试工作,主要是测试吞吐和尾延迟,去年还做过一段时间将Web应用向mTCP移植的工作。总之,对mTCP的基本工作有一定的了解,但是没有深度调研mTCP的工作流程。近期由于工作需要,准备阅读mTCP的源码,梳理其代码结构和mTCP应用的工作流程。

这里以mTCP示例程序epserver为例简要分析,如图所示

mTCP 应用程序代码结构

如图,是mTCP epserver中的函数调用流程

部分mTCP API

int mtcp_init(const char *config_file)

只在application的main函数起来时初始化一次

1
2
3
CreateAddressPool()
SetRoutingTable()
LoadARPTable()
mctx_t mtcp_create_context(int cpu)

在每个核上都会被调用

1
2
3
初始化本核协议栈的上下文,包括log
MTCPRunThread(void *arg)对[struct mtcp_manager *mtcp]进行初始化;
RunMainLoop(struct mtcp_thread_context *ctx)

socketAPI

int mtcp_socket(mctx_t mctx, int domain, int type, int protocol)

创建 socket

1
2
3
从每个核上的 mtcp_manager_t mtcp 的 free_smap 链表中取下一个 socket 并完成初始化;
socket类型有6种:MTCP_SOCK_UNUSED, MTCP_SOCK_STREAM, MTCP_SOCK_PROXY, MTCP_SOCK_LISTENER, MTCP_SOCK_EPOLL,MTCP_SOCK_PIPE
AllocateSocket(mctx, type, FALSE)
int mtcp_listen(mctx_t mctx, int sockid, int backlog)
1
2
3
4
5
把socket类型设置为MTCP_SOCK_LISTENER;
ListenerHTInsert()
创建struct tcp_listener对象,并初始化;
创建accept queue
把listener放入mtcp->listeners队列(说是hashtable结果是链表....)
int mtcp_accept(mctx_t mctx, int sockid, struct sockaddr addr, socklen_t addrlen)

从socket的listener对象的accept queue里取连接请求

1
2
3
如果没有请求且socket->opt中没有nonblock选项则pthread_cond_wait()等待;
创建一个类型为MTCP_SOCK_STREAM的socket并初始化;
如果还有连接请求未处理且epoll为电平触发,则生成一个MTCP_EPOLLIN事件放入USR_SHADOW_EVENT_QUEUE
ssize_t mtcp_recv(mctx_t mctx, int sockid, char *buf, size_t len, int flags)
1
2
3
检查stream状态机状态, 如果socket类型是MTCP_SOCK_PIPE,直接调用PipeRead()并返回
CopyToUser(mtcp, cur_stream, buf, len) //将数据拷贝到用户缓冲区,并将stream加入ACK队列,这个过程需要上锁
如果stream缓冲区还有未读数据且epoll为电平触发,产生一个MTCP_EPOLLIN事件放入USR_SHADOW_EVENT_QUEUE队列
ssize_t mtcp_write(mctx_t mctx, int sockid, const char *buf, size_t len)
1
2
3
4
5
检查stream状态机状态;
如果socket类型是MTCP_SOCK_PIPE,直接调用PipeWrite()并返回;
CopyFromUser(mtcp, cur_stream, buf, len) //将数据拷贝到stream缓冲区,此过程需要对write_lock上锁
将stream放入发送队列,此过程需要上锁;
如果发送窗口有剩余,且epoll为电平触发,生成MTCP_EPOLLOUT事件放入USR_SHADOW_EVENT_QUEUE

协议栈触发API

static void RunMainLoop(struct mtcp_thread_context *ctx)
1
2
3
4
5
6
7
8
每个device从DPDK收包,并ProcessPacket()进行处理;
//检查各个定时器;
CheckRtmTimeout();
CheckTimewaitExpire();
CheckConnectionTimeout();
FlushEpollEvents(mtcp, ts) //处理epoll事件;
HandleApplicationCalls(mtcp, ts) //处理应用发来的流事件请求
如果设置了ctx->interrupt,调用InterruptApplication(mtcp);
int ProcessTCPPacket(mtcp_manager_t mtcp, uint32_t cur_ts, const int ifidx, const struct iphdr *iph, int ip_len)
1
2
3
4
ValidateSequence(mtcp, cur_stream, cur_ts, tcph, seq, ack_seq, payloadlen) //进行sequence处理
一些异常情况需要加入处理队列,回复ACK;
处理rst标志位;
根据cur_stream->state和TCP状态机进行处理;
static inline void FlushEpollEvents(mtcp_manager_t mtcp, uint32_t cur_ts)
1
2
3
此函数全程对该核上的ep->epoll_lock上锁
如果ep->mtcp_queue有内容且ep->usr_queue有剩余空间,把mtcpq的事件拷贝到usrq中
如果epoll处于waiting状态且usrq或shadowq中有事件,发出epoll_cond信号唤醒应用线程
static inline void HandleApplicationCalls(mtcp_manager_t mtcp, uint32_t cur_ts)
1
2
3
4
5
6
把mtcp->connectq上所有stream取下来并AddtoControlList(mtcp, stream, cur_ts),这些流在WriteTCPControlList()中被处理,发送control packet
把mtcp->sendq上所有stream取下来并AddtoSendList(mtcp, stream),这些流在WriteTCPDataList()中被处理
把mtcp->ackq上所有stream取下来并EnqueueACK(mtcp, stream, cur_ts, ACK_OPT_AGGREGATE),这些流在WriteTCPACKList()中被处理
处理mtcp->closeq上的stream
处理mtcp->resetq上的stream
处理mtcp->destroyq上的stream
static inline void WritePacketsToChunks(mtcp_manager_t mtcp, uint32_t cur_ts)
1
2
3
4
//对g_sender和所有n_sender
WriteTCPControlList();
WriteTCPACKList();
WriteTCPDataList();
static void InterruptApplication(mtcp_manager_t mtcp)
1
2
如果epoll处于waiting状态,发送mtcp->ep->epoll_cond信号
检查每个端口的listener,发送pthread_cond_signal(&listener->accept_cond)信号

epoll API

int mtcp_epoll_create(mctx_t mctx, int size)
1
//在每个核的线程初始化的时候进行调用
mtcp_epoll_ctl()
1
//用于对MTCP_EPOLL_CTL_ADD,MTCP_EPOLL_CTL_MOD, MTCP_EPOLL_CTL_DEL三种epoll事件操作的处理