在该博文中,我们已经了解了什么是 epoll 及其优点,那么实际应用应该如何编写代码呢?epoll 的编码离不开三个基本的函数:epoll_create,epoll_ctl,epoll_wait,下面将介绍 epoll 这三个函数的使用。
有些 epoll 函数原理需要配合 epoll 函数的源码才能进行深入讲解,这里我们参考 Github 用户 wangbojing 的 NtyTcp 项目中关于 epoll 的实现代码做讲解,可以从这里下载项目源码。
epoll函数原理和使用介绍
1. epoll_create
函数原型 :int epoll_create(int size);
功能说明 :创建一个 epoll 对象,返回该对象的描述符,注意要使用 close 关闭该描述符。
参数说明 :从 Linux 内核 2.6.8 版本起,size 这个参数就被忽略了,只要求 size 大于 0 即可。
原理讲解
调用 epoll_create 创建一个 epoll 对象的时候究竟干了什么事情呢?我们可以从 NtyTcp 的代码中查看 epoll_create 的实现代码,最核心的代码就是 struct eventpoll *ep = (struct eventpoll *)calloc(1,sizeof(struct eventpoll)) 创建一个 struct eventpoll 对象,然后对其成员进行初始化。
struct eventpoll 的定义如下,rbr 是一棵红黑树,支持快速查找键值对,所有需要加入监听事件的描述符都需要添加到这棵红黑树来,rbcnt 记录红黑树节点个数。rdlist 是一个双向链表,当红黑树上监听的描述符发生对应的监听事件时,内核会将这个节点插入到该双向链表来,rdnum 是双向链表节点的个数,也就是发生了事件的节点个数。
2. epoll_ctl
函数原型 :int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能说明 :操作控制 epoll 对象,主要涉及 epoll 红黑树上节点的一些操作,比如添加节点,删除节点,修改节点事件。
参数说明
epfd:通过 epoll_create 创建的 epoll 对象句柄。
op:对红黑树的操作,添加节点、删除节点、修改节点监听的事件,分别对应 EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD。
添加事件:相当于往红黑树添加一个节点,每个客户端连接服务器后会有一个通讯套接字,每个连接的通讯套接字都不重复,所以这个通讯套接字就是红黑树的 key。
修改事件:把红黑树上监听的 socket 对应的监听事件做修改。
删除事件:相当于取消监听 socket 的事件。
fd:需要添加监听的 socket 描述符,可以是监听套接字,也可以是与客户端通讯的通讯套接字。
event:事件信息。
关于event参数的讲解
该参数是 struct epoll_event 类型的指针变量,结构体定义如下。
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll 事件 */
epoll_data_t data; /* 用户数据 */
};
events成员
成员 events 代表要监听的 epoll 事件类型,有读事件,写事件,有如下取值。
events参数取值 | 含义 |
EPOLLIN | 监听 fd 的读事件。举例:如果客户端发送消息过来,代表服务器收到了可读事件。 |
EPOLLOUT | 监听 fd 的写事件。如果 fd 对应的发数据内核缓冲区不为满,只要监听了写事件,就会触发可写事件。 |
EPOLLRDHUP | 监听套接字关闭或半关闭事件,Linux 内核 2.6.17 后可用。 |
EPOLLPRI | 监听紧急数据可读事件。 |
举例:假设现在我们的服务器通过调用 accept 函数成功与客户端建立连接并得到了通讯套接字 connfd,如果我们需要关心这个客户端是否给服务器发送数据过来(读事件),我们需要将 event 参数的 events 成员的 EPOLLIN 置位为1,相当于 event.events |= EPOLLIN,如果我们并不关心服务器是否可以往客户端写数据(写事件),我们可以将 event 参数的 events 成员的 EPOLLOUT 置位为 0,相当于 event.events &= ~EPOLLOUT。同理,如果你还关心某个事件或者不关心某个事件,就将该位置为 1 或者 0 即可,再通过 epoll_ctl 函数来修改。这样,当我们调用 epoll_wait 函数等待事件时,一旦发生了我们希望监听的事件时,epoll_wait 函数就会返回并通知我们哪个描述符发生了对应的事件。(本文后续会讲到 epoll_wait 函数)
data成员
data 成员时一个联合体类型,它可以在我们调用 epoll_ctl 给 fd 添加/修改描述符监听的事件时顺带一些数据。最典型的用法就是每个通讯套接字会对应内存中的一块数据区,这块数据区一般存放着一些连接相关的信息,比如对端的 IP,端口等。当我们要添加该通讯套接字监听事件时就可以把这块内存的地址赋值给 ptr,这样当我们调用 epoll_wait 时也可以取出这些信息。
3. epoll_wait
函数原型 :int epoll_wait(int epid, struct epoll_event *events, int maxevents, int timeout);
功能说明 :阻塞一段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知。说白了就是遍历双向链表,把双向链表里的节点数据拷贝出来,拷贝完毕后就从双向链表移除。
参数说明
epid:epoll_create 返回的 epoll 对象描述符。
events:存放就绪的事件集合,这个是传出参数。
maxevents:代表可以存放的事件个数,也就是 events 数组的大小。
timeout:阻塞等待的时间长短,以毫秒为单位,如果传入 -1 代表阻塞等待。
返回值说明
返回值 | 含义 |
>0 | 代表有几个我们希望监听的事件发生了 |
=0 | timeout 超时时间到了 |
<0 | 出错,可以通过 errno 值获取出错原因 |