一、概述
在传统的阻塞I/O模型中,一个Socket(套接字)通常只能处理一个TCP连接,即一对一的关系。每个TCP连接都需要分配一个独立的Socket来处理。
然而,使用多路复用技术,可以在单个线程中同时监视多个Socket的状态,以确定哪些Socket有可读或可写事件。通过在单个线程内等待和处理多个连接的事件,高效地处理大量的并发连接,减少资源消耗。
二、epoll
在linux上多路复用技术有select、poll、epoll。其中epoll的性能表现是最优异的,能支持的并发量也最大。linux中和epoll相关的函数有如下三个:
- epoll_create: 创建一个epoll对象。
- epoll_ctl: 向epoll对象添加要管理的连接。
- epoll_wait: 等待epoll对象管理的连接上的IO事件。
三、epoll内核对象的创建
用户进程调用epoll_create时,内核会创建一个eventpoll的内核对象,并把它加入到当前进程已打开的文件列表中。eventpoll内核对象的定义如下:,
struct eventpoll {
//等待队列
wait_queue_head_t wq;
//就绪的描述符链表
struct list_head rdllist;
//每个eventpoll对象中都有一颗红黑树
struct rb_root rbr;
}
- wq:等待队列。用户进程陷入阻塞时,其进程描述符会保存在eventpoll对象的等待队列。
- rbr:一颗红黑树。用来管理用户进程添加进来的所有的socket,为了支持对海量连接的高效查找、插入和删除。
- rdllist:就绪的描述符链表。当有连接事件就绪的时候,内核会把就绪的连接放到rdllist,这样只需要判断rdllist链表就能找出就绪的连接,而不用去遍历整颗树。
四、epoll添加socket
这一步是由用户进程调用epoll_ctl完成的。假设现在和客户端的多个连接的socket都创建好了,也创建好了eventpoll对象。epoll_ctl在注册每一个socket的时候,内核都会做如下三件事情:
- 初始化一个红黑树节点对象epitem。
- 在socket对象的等待队列中,注册事件到达回调函数ep_poll_callback。
- 将epitem插入eventpoll的红黑树。
struct epitem {
......
struct epoll_filefd ffd; //socket句柄
struct eventpoll *ep; //所属的eventpoll
}
通过epoll_ctl添加两个socke以后,这些内核数据结构最终在进程中的关系如下:
4.1 设置socket对象等待队列
epoll_ctl先完成epitem对象的初始化后,接着就会去设置socket的等待队列,把ep_poll_callback设置为socket数据就绪时的回调函数,注册到socket等待队列:
// include/linux/wait.h
static inline void init_waitqueue_func_entry(wait_queue_t *q