最近,异想天开,想用D实现一个web服务器(似乎已经想这件事好久了,只不过之前是C++),自然而然得开始研究epoll。早就听说过epoll的大名,只不过网上的教程似乎没多少,并且感觉也没怎么把用法给讲完整。好在,通过几天的学习,也算是有所积累,因此想通过这篇post记录下,尽量把细节给讲清楚,希望它对各位有所价值。

在linux平台下,epoll是异步阻塞的,真正异步非阻塞的是AIO,只不过据说在RedHat上测试的结果是,epoll的性能更好,当然在最新的内核下,比如3.18.2,缺乏验证。接下来应该也会研究下AIO,如果可能,我会为大家提供一个性能测试的对比。

同属IO复用,除了epoll,我们也能选择select和poll,当然这并不是说epoll在任何情况下性能都比select和poll好,关键还是要根据场景而定,至于epoll相比后两者的优点,我就不人云亦云了,请大家阅读一下源码,自行了解下。

Changelog

  • [2015-01-24] 添加“epoll的使用模式”一节
  • [2015-08-13] 补充EPOLLHUP事件细节

一、epoll函数接口

创建epoll实例

int epoll_create1(int flags);

函数参数:

  • flags: 当前版本只支持EPOLL_CLOEXEC标志(请注意不支持EPOLL_NONBLOCK标志)

其实我们也能够通过epoll_create(int size)这个函数来创建epoll实例,只不过这个函数中的size在2.6.27内核开始就不必要了,原因请看如下代码片段:

SYSCALL_DEFINE1(epoll_create, int, size)
{if (size <&#61; 0)return -EINVAL;return sys_epoll_create1(0);
}

根据惯例&#xff0c;如果返回-1&#xff0c;则标志出现了问题&#xff0c;我们可以读取errno来定位错误&#xff0c;有如下errno会被设置&#xff1a;

  • EINVAL : 无效的标志
  • EMFILE : 用户打开的文件超过了限制
  • ENFILE : 系统打开的文件超过了限制
  • ENOMEM : 没有足够的内存完成当前操作

管理epoll事件

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数参数&#xff1a;

  • epfd : epoll实例的fd
  • op : 操作标志&#xff0c;下文会描述
  • fd : 监控对象的fd
  • event : 事件的内容&#xff0c;下文描述

op可以有3个值&#xff0c;分别为&#xff1a;

  • EPOLL_CTL_ADD : 添加监听的事件
  • EPOLL_CTL_DEL : 删除监听的事件
  • EPOLL_CTL_MOD : 修改监听的事件

event是一个如下结构体的一个实例&#xff1a;

typedef union epoll_data {void *ptr;int fd;__uint32_t u32;__uint64_t u64;
} epoll_data_t;
struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};

其中&#xff0c;data是一个联合体&#xff0c;能够存储fd或其它数据&#xff0c;我们需要根据自己的需求定制。events表示监控的事件的集合&#xff0c;是一个状态值&#xff0c;通过状态位来表示&#xff0c;可以设置如下事件&#xff1a;

  • EPOLLERR : 文件上发上了一个错误。这个事件是一直监控的&#xff0c;即使没有明确指定
  • EPOLLHUP : 文件被挂断。这个事件是一直监控的&#xff0c;即使没有明确指定
  • EPOLLRDHUP : 对端关闭连接或者shutdown写入半连接
  • EPOLLET : 开启边缘触发&#xff0c;默认的是水平触发&#xff0c;所以我们并未看到EPOLLLT
  • EPOLLONESHOT : 一个事件发生并读取后&#xff0c;文件自动不再监控
  • EPOLLIN : 文件可读
  • EPOLLPRI : 文件有紧急数据可读
  • EPOLLOUT : 文件可写
  • EPOLLWAKEUP : 如果EPOLLONESHOT和EPOLLET清除了&#xff0c;并且进程拥有CAP_BLOCK_SUSPEND权限&#xff0c;那么这个标志能够保证事件在挂起或者处理的时候&#xff0c;系统不会挂起或休眠

注意一下&#xff0c;EPOLLHUP并不代表对端结束了连接&#xff0c;这一点需要和EPOLLRDHUP区分。通常情况下EPOLLHUP表示的是本端挂断&#xff0c;造成这种事件出现的原因有很多&#xff0c;其中一种便是出现错误&#xff0c;更加细致的应该是和RST联系在一起&#xff0c;不过目前相关文档并不是很全面&#xff0c;本文会进一步跟进。

根据惯例&#xff0c;如果返回-1&#xff0c;则标志出现了问题&#xff0c;我们可以读取errno来定位错误&#xff0c;有如下errno会被设置&#xff1a;

  • EBADF : epfd或者fd不是一个有效的文件描述符
  • EEXIST : op为EPOLL_CTL_ADD&#xff0c;但fd已经被监控
  • EINVAL : epfd是无效的epoll文件描述符
  • ENOENT : op为EPOLL_CTL_MOD或者EPOLL_CTL_DEL&#xff0c;并且fd未被监控
  • ENOMEM : 没有足够的内存完成当前操作
  • ENOSPC : epoll实例超过了/proc/sys/fs/epoll/max_user_watches中限制的监听数量

等待epoll事件

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

函数参数&#xff1a;

  • epfd : epoll实例的fd
  • events : 储存事件的数组首地址
  • maxevents : 最大事件的数量
  • timeout : 等待的最长时间

如果函数返回获得的时间的数量&#xff0c;如果返回-1&#xff0c;则标志出现了问题&#xff0c;我们可以读取errno来定位错误&#xff0c;有如下errno会被设置&#xff1a;

  • EBADF : epfd不是一个有效的文件描述符
  • EFAULT : events指向的内存无权访问
  • EINTR : 在请求事件发生或者过期之前&#xff0c;调用被信号打断
  • EINVAL : epfd是无效的epoll文件描述符

二、关于水平触发和边缘触发

用英文来表示&#xff0c;水平触发为Level Trigger&#xff0c;边缘触发为Edge Trigger&#xff0c;不过很多文章也将LT翻译为条件触发&#xff0c;有点搞不清为何这么翻译。

那么为什么在这里突兀得提及ET和LT呢&#xff1f;是这样的&#xff0c;想必各位应该已经注意到EPOLLET了&#xff0c;这个就代表ET事件&#xff0c;而epoll默认采取的是LT&#xff0c;也就是说在能够正确使用epoll之前&#xff0c;我们必须弄明白ET和LT&#xff0c;尤其是准备直接使用nonblocking和ET的朋友。

LT和ET原本应该是用于脉冲信号的&#xff0c;可能用它来解释更加形象。Level和Edge指的就是触发点&#xff0c;Level为只要处于水平&#xff0c;那么就一直触发&#xff0c;而Edge则为上升沿和下降沿的时候触发。听起来到时挺玄乎的&#xff0c;那么怎么区分这个Level和Edge呢&#xff1f;很简单&#xff0c;0->1这种类型的事件就是Edge&#xff0c;而Level则正好相反&#xff0c;1->1这种类型就是&#xff0c;由此可见&#xff0c;当缓冲区有数据可取的时候&#xff0c;ET会触发一次事件&#xff0c;之后就不会再触发&#xff0c;而LT只要我们没有取完缓冲区的数据&#xff0c;就会一直触发。

为了加深大家的印象&#xff0c;我们用个段子来描述&#xff1a;

  1. LT 水平触发
  • 儿子&#xff1a;“妈妈&#xff0c;我收到了5000元压岁钱。”

  • 妈妈&#xff1a;“恩&#xff0c;省着点花&#xff01;”

  • 儿子&#xff1a;“妈妈&#xff0c;我今天买了个ipad&#xff0c;花了3000元。”

  • 妈妈&#xff1a;“噢&#xff0c;这东西真贵。”

  • 儿子&#xff1a;“妈妈&#xff0c;我今天买好多吃的&#xff0c;还剩1000元。”

  • 妈妈&#xff1a;“用完了这些钱&#xff0c;我可不会再给你了。”

  • 儿子&#xff1a;“妈妈&#xff0c;那1000元我没花&#xff0c;零花钱够用了。”

  • 妈妈&#xff1a;“恩&#xff0c;这才是明智的做法&#xff01;”

  • 儿子&#xff1a;“妈妈&#xff0c;那1000元我没花&#xff0c;我要攒起来。”

  • 妈妈&#xff1a;“恩&#xff0c;加油&#xff01;”

是不是没完没了&#xff1f;只要儿子手中还有钱&#xff0c;他就会一直汇报&#xff0c;这就是LT模式。有钱就是1&#xff0c;没钱就是0&#xff0c;那么只要儿子还有钱&#xff0c;这种事件就是1->1类型事件&#xff0c;自然是LT。

  1. ET 边缘触发
  • 儿子&#xff1a;“妈妈&#xff0c;我收到了5000元压岁钱。”

  • 妈妈&#xff1a;“恩&#xff0c;省着点花&#xff01;”

  • 儿子&#xff1a;“……”

  • 妈妈&#xff1a;“你倒是说话啊&#xff1f;压岁钱呢&#xff1f;&#xff01;”

这个就是ET模式&#xff0c;简洁得有点过头&#xff0c;但很高效&#xff01;虽然妈妈可能并不这么认为。。。儿子从没钱到有钱&#xff0c;是一个0->1的过程&#xff0c;因此为ET。儿子和妈妈说过自己拿到了压岁钱就完事了&#xff0c;至于怎么花钱&#xff0c;还剩多少钱&#xff0c;一概不说&#xff0c;有钱就是这么任性&#xff01;

我们将上述的儿子换做缓冲区&#xff0c;而钱换成数据&#xff0c;那么就是epoll中的ET和LT了&#xff0c;所以说编程也是源自生活的。还有一点需要强调ET模式只能应用于设置了O_NONBLOCK的fd&#xff0c;而LT则同时支持同步和异步。使用得当ET效率比LT高&#xff0c;但是LT更加易用&#xff0c;不容易除错。

三、epoll的使用模式

解释了这个多&#xff0c;我们应该怎么来用epoll呢&#xff1f;简单的几个函数&#xff0c;用起来可着实不轻松。好在&#xff0c;这里有一个大概的模式供大家参考&#xff0c;如下为伪代码:

epfd &#61; epoll_init1(0);
event.events &#61; EPOLLET | EPOLLIN;
event.data.fd &#61; serverfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, serverfd, &event);
// 主循环
while(true) {// 这里的timeout很重要&#xff0c;实际使用中灵活调整count &#61; epoll_wait(epfd, &events, MAXEVENTS, timeout);for(i &#61; 0; i if(events[i].events & EPOLLERR || events[i].events & EPOLLHUP)// 处理错误if(events[i].data.fd &#61;&#61; serverfd)// 为接入的连接注册事件else if(events[i].events & EPOLLIN)// 处理可读的缓冲区read(events[i].data.fd, buf, len);event.events &#61; EPOLLET | EPOLLOUT;event.data.fd &#61; events[i].data.fd;epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &event);else// 处理可写的缓冲区write(events[i].data.fd, buf, len);// 后续可以关闭fd或者MOD至EPOLLOUT}
}

使用上述的框架&#xff0c;我们可以完成很多事情&#xff0c;但是内部的细节&#xff0c;比如错误处理&#xff0c;信号处理等&#xff0c;还是不能大意&#xff0c;需要完善。

四、epoll实例 —— 啰嗦的echo man

接下来&#xff0c;让我们来看个示例吧。这只是一个hello world级别的代码&#xff0c;无论是你发送什么数据给它&#xff0c;它只会回复“it&#39;s echo man”。使用的是ET模式&#xff0c;相信对于大家应该有些许参考价值。

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXEVENTS 64
int create_and_bind (int port) {int sfd &#61; socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if(sfd &#61;&#61; -1) {return -1;}struct sockaddr_in sa;bzero(&sa, sizeof(sa));sa.sin_family &#61; AF_INET;sa.sin_port &#61; htons(port);sa.sin_addr.s_addr &#61; htonl(INADDR_ANY);if(bind(sfd, (struct sockaddr*)&sa, sizeof(struct sockaddr)) &#61;&#61; -1) {return -1;}return sfd;
}
int make_socket_non_blocking (int sfd) {int flags &#61; fcntl (sfd, F_GETFL, 0);if (flags &#61;&#61; -1) {return -1;}if(fcntl (sfd, F_SETFL, flags | O_NONBLOCK) &#61;&#61; -1) {return -1;}return 0;
}
/* 此函数用于读取参数或者错误提示 */
int read_param(int argc, char *argv[]) {if (argc !&#61; 2) {fprintf (stderr, "Usage: %s [port]\n", argv[0]);exit (EXIT_FAILURE);}return atoi(argv[1]);
}
int main (int argc, char *argv[]) {int sfd, s;int efd;struct epoll_event event;struct epoll_event *events;int port &#61; read_param(argc, argv);/* 创建并绑定socket */sfd &#61; create_and_bind (port);if (sfd &#61;&#61; -1) {perror("create_and_bind");abort ();}/* 设置sfd为非阻塞 */s &#61; make_socket_non_blocking (sfd);if (s &#61;&#61; -1) {perror("make_socket_non_blocking");abort ();}/* SOMAXCONN 为系统默认的backlog */s &#61; listen (sfd, SOMAXCONN);if (s &#61;&#61; -1) {perror ("listen");abort ();}efd &#61; epoll_create1 (0);if (efd &#61;&#61; -1) {perror ("epoll_create");abort ();}event.data.fd &#61; sfd;/* 设置ET模式 */event.events &#61; EPOLLIN | EPOLLET;s &#61; epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);if (s &#61;&#61; -1) {perror ("epoll_ctl");abort ();}/* 创建事件数组并清零 */events &#61; calloc (MAXEVENTS, sizeof event);/* 开始事件循环 */while (1) {int n, i;n &#61; epoll_wait (efd, events, MAXEVENTS, -1);for (i &#61; 0; i if (events[i].events & (EPOLLERR | EPOLLHUP)) {/* 监控到错误或者挂起 */fprintf (stderr, "epoll error\n");close (events[i].data.fd);continue;} if(events[i].events & EPOLLIN) {if (sfd &#61;&#61; events[i].data.fd) {/* 处理新接入的socket */while (1) {struct sockaddr_in sa;socklen_t len &#61; sizeof(sa);char hbuf[INET_ADDRSTRLEN];int infd &#61; accept (sfd, (struct sockaddr*)&sa, &len);if (infd &#61;&#61; -1) {if ((errno &#61;&#61; EAGAIN) || (errno &#61;&#61; EWOULDBLOCK)) {/* 资源暂时不可读&#xff0c;再来一遍 */break;} else {perror ("accept");break;}}inet_ntop(AF_INET, &sa.sin_addr, hbuf, sizeof(hbuf));printf("Accepted connection on descriptor %d ""(host&#61;%s, port&#61;%d)\n", infd, hbuf, sa.sin_port);/* 设置接入的socket为非阻塞 */s &#61; make_socket_non_blocking (infd);if (s &#61;&#61; -1) abort ();/* 为新接入的socket注册事件 */event.data.fd &#61; infd;event.events &#61; EPOLLIN | EPOLLET;s &#61; epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);if (s &#61;&#61; -1) {perror ("epoll_ctl");abort ();}}//continue;} else {/* 接入的socket有数据可读 */while (1) {ssize_t count;char buf[512];count &#61; read (events[i].data.fd, buf, sizeof buf);if (count &#61;&#61; -1) {if (errno !&#61; EAGAIN) {perror ("read");close(events[i].data.fd);}break;} else if (count &#61;&#61; 0) {/* 数据读取完毕&#xff0c;结束 */close(events[i].data.fd);printf ("Closed connection on descriptor %d\n", events[i].data.fd);break;}/* 输出到stdout */s &#61; write (1, buf, count);if (s &#61;&#61; -1) {perror ("write");abort ();}event.events &#61; EPOLLOUT | EPOLLET;epoll_ctl(efd, EPOLL_CTL_MOD, events[i].data.fd, &event);}}} else if((events[i].events & EPOLLOUT) && (events[i].data.fd !&#61; sfd)) {/* 接入的socket有数据可写 */write(events[i].data.fd, "it&#39;s echo man\n", 14);event.events &#61; EPOLLET | EPOLLIN;epoll_ctl(efd, EPOLL_CTL_MOD, events[i].data.fd, &event);}}}free (events);close (sfd);return EXIT_SUCCESS;
}

我们可以通过ncat命令和它聊天&#xff1a;

[codesun&#64;lucode ~]$ ncat 127.0.0.1 8000
hello
it&#39;s echo man

ncat和echo_man通信的时候其实用的是长连接&#xff08;除非我们自己CTRL&#43;C&#xff09;。对于长连接这种东西&#xff0c;需要一定的处理策略。一般而言&#xff0c;我们会采用如下几种策略来处理&#xff1a;

  1. 心跳&#xff0c;通过这个来表示长连接有效&#xff0c;没有了心跳自然就表示结束
  2. 特殊字符&#xff0c;标记数据传输完毕
  3. 协议中添加length&#xff0c;这个比较常规
  4. 设置timeout&#xff0c;超过这个threshold就关闭半连接或者全连接

总之&#xff0c;长连接绝对是个好东西&#xff0c;在很大程度上避免了建立和关闭TCP连接时握手带来的延迟&#xff0c;不过&#xff0c;想要让服务端一直持有长连接也是有点理想化。

这就是epoll简单应用的全部内容了&#xff0c;当然一旦涉及多线程和多进程&#xff0c;那么这种场景下处理epoll会变得极其有趣。暂且说到这里&#xff0c;谢谢阅读&#xff01;