前言

前面讲了IO多路复用的API,select和poll的缺点是性能不够,客户端连接越多性能下降越明显,epoll的出现解决了这个问题,引用The Linux Programming Interface的一个统计对比如下:

fd数量      poll CPU时间(秒)    select CPU时间(秒)   epoll CPU时间(秒)
---------------------------------------------------------------------
10              0.61                0.73                0.41
100             2.9                 3.0                 0.42
1000            35                  35                  0.53
10000           990                 930                 0.66
---------------------------------------------------------------------

可以看出fd达到100个以后,select/poll就非常慢了,而epoll即使达到10000个也表现得非常好,因为:

  • 每次调用select/poll,内核必须检查所有传进来的描述符;而对于epoll,每次调用epoll_ctl,内核会把相关信息与底层的文件描述关联起来,当IO事件就绪时,内核把信息加到epoll的就绪列表里。随后调用epoll_wait,内核只需把就绪列表中的信息提取出来返回即可。
  • 每次调用select/poll,都要把待监控的所有文件描述符传给内核,函数返回时,内核要把描述符返回并标识哪些就绪,得到结果后还要逐个判断所有描述符,才能确定哪些有事件;epoll在调用epoll_ctl时就已经维护着监控的列表,epoll_wait不需要传入任何信息,并且返回的结果只包含就绪的描述符,这样就不用去判断所有描述符。

从概念上理解epoll是这样的,把要监控的fd的IO事件注册给epoll(调用epoll_ctl),然后调用epoll的API等待事件到达(调用epoll_wait),内核可能对每个fd维护着一个读和写的缓冲区,那么:

  • 如果我监控读事件,并且读缓冲区有数据了,epoll_wait就会返回,此时我可以调用read读数据。
  • 如果我监控写事件,并且写缓存区未满,epoll_wait也会返回,此时我可以调用write写数据。
  • 如果fd发生了一些错误,epoll_wait也会返回,此时我根据返回的标志位,就可以知道。
  • 如果我监控读事件 并且有客户端连接进来,epoll_wait就会返回,此时我可以调用accept接受客户端。

epoll的API介绍

  • int epoll_create(int size);
    创建一个epoll实例,返回代表实例的文件描述符(fd),size自Linux 2.6.8以后忽略,但必须大于0.
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll控制接口,epfd就是epoll的文件描述符,fd是要操作的文件描述符,op有如下几种:

    • EPOLL_CTL_ADD 注册fd的事件,事件类型在event指定。
    • EPOLL_CTL_MOD 修改已注册的fd事件。
    • EPOLL_CTL_DEL 删除fd的事件。

epoll_event有一个events成员,指定要注册的事件类型,比较重要的几个:

  • EPOLLIN fd可读事件
  • EPOLLOUT fd可写事件
  • EPOLLERR fd发生错误,这个事件总是会被监控,不必手动增加
  • EPOLLHUP fd被挂起时,这个事件总是会被监控,不必手动增加,这通常发生在socket异常关闭时,此时read返回0,然后正常的清理socket资源。

epoll_event还有一个epoll_data_t成员,由外部设置自定义数据,以方便后续处理。

  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待事件发生,如果没有事件发生,线程会被挂起,maxevents指定最大事件数,events外部传入的事件数组,长度应当等于maxevents,当事件发生时,epoll会把事件信息填到这里, timeout指定等待的最大时间, 0表示马上返回,-1表示无限等待。
    epoll_wait返回等待到的事件数,返回时,遍历events对fd进行处理。 当epoll不再使用时,应该调用close关闭epollfd。

水平触发和边缘触发

epoll触发事件有两种模式,默认的叫水平触发(LT),另一种叫边缘触发(ET):

  • LT模式:只要fd的读缓冲区不空,或写缓冲区不满,epoll_wait就会一直触发事件(也就是返回)。
  • ET模式:当被监控的fd状态变化时(从未就绪变成就绪状态),事件触发一次。此后内核不再通知,除非有新的事件到来。

// 多谢 @黄蔚 的指正,原来ET模式的描述有误,仔细阅读过man文档后已修正过来。

LT处理起来比ET要简单得多,读事件触发,只需要read一次,如果数据没读完,下次epoll_wait还会返回,写也是一样的;ET模式就要求事件触发时,一直读一直写直到明确知道已经读写完毕(返回EAGIN或EWOULDBLOCK的错误码)。

水平触发的服务器程序大概是这样的流程:

  • accept一个新连接,将这个新连接的fd加到epoll事件中,监听EPOLLIN事件。
  • EPOLLIN事件到达时,read该fd中的数据。
  • 如果要向该fd写事件,向epoll增加EPOLLOUT事件。
  • EPOLLOUT事件到达,向fd write数据,如果数据太大无法一次写出,那么先保留EPOLLOUT事件,下次事件到达继续写;如果写出完毕,从epoll删除EPOLLOUT事件。

一个实用的echo程序:

这一次我们要使用epoll和非阻塞socket来写一个真正实用的echo服务器,调用fcntl函数,设置O_NONBLOCK标志位,即可让socket的文件描述符变成非阻塞模式。非阻塞模式处理起来比阻塞模式要复杂一些:

  • read, write, accept这些函数不会阻塞,要么成功,要么返回-1失败,errno记录了失败的原因,有几个错误码要关注:

    • EAGAIN 或 EWOULDBLOCK 只有非阻塞的fd才会发生,表示没数据可读,或没空间可写,或没有客户端可接受,下次再来吧。这两个值可能相同也可能不同,最好一起判断。
    • EINTR 表示被信号中断,这种情况可以再一次尝试调用。
    • 其他错误表示真的出错了。
  • 向一个fd写数据比较麻烦,我们没法保证一次性把所有数据都写完,所以需要先保存在缓冲里,然后向epoll增加写事件,事件触发时再向fd写数据。等数据都写完了,再把事件从epoll移除。这个程序把写数据保存在链表中。

我们把监听fd保留为阻塞模式,因为epoll_wait返回,可以确定一定有客户端连接进来,所以accept一般可以成功,并不用担心会阻塞。客户端连接使用的是非阻塞模式,确保读写未完成时不会阻塞。

下面是这个程序的代码,关键地方加了一些注释,仔细看代码比看文字描述更有用:)

#include "socket_lib h"
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>#define MAX_CLIENT 10000
#define MIN_RSIZE 124
#define BACKLOG 128
#define EVENT_NUM 64// 缓存结点
struct twbuffer {struct twbuffer *next;      // 下一个缓存void *buffer;        // 缓存char *ptr;           // 当前未发送的缓存,buffer != ptr表示只发送了一部分int size;            // 当前未发送的缓存大小
};// 缓存列表
struct twblist {struct twbuffer *head;struct twbuffer *tail;
};// 客户端连接信息
struct tclient {int fd;             // 客户端fdint rsize;          // 当前读的缓存区大小int wbsize;         // 还未写完的缓存大小struct twblist wblist;  // 写缓存链表
};// 服务器信息
struct tserver {int listenfd;       // 监听fdint epollfd;        // epollfdstruct tclient clients[MAX_CLIENT];     // 客户端结构数组
};// epoll增加读事件
void epoll_add(int efd, int fd, void *ud) {struct epoll_event ev;ev.events = EPOLLIN;ev.data.ptr = ud;epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev);
}// epoll修改写事件
void epoll_write(int efd, int fd, void *ud, int enabled) {struct epoll_event ev;ev.events = EPOLLIN | (enabled ? EPOLLOUT : 0);ev.data.ptr = ud;epoll_ctl(efd, EPOLL_CTL_MOD, fd, &ev);
}// epoll删除fd
void epoll_del(int efd, int fd) {epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
}// 设置socket为非阻塞
void set_nonblocking(int fd) {int flag = fcntl(fd, F_GETFL, 0);if (flag >= 0) {fcntl(fd, F_SETFL, flag | O_NONBLOCK);}
}// 增加写缓存
void add_wbuffer(struct twblist *list, void *buffer, int sz) {struct twbuffer *wb = malloc(sizeof(*wb));wb->buffer = buffer;wb->ptr = buffer;wb->size = sz;wb->next = NULL;if (!list->head) {list->head = list->tail = wb;} else {list->tail->next = wb;list->tail = wb;}
}// 释放写缓存
void free_wblist(struct twblist *list) {struct twbuffer *wb = list->head;while (wb) {struct twbuffer *tmp = wb;wb = wb->next;free(tmp);}list->head = NULL;list->tail = NULL;
}// 创建客户端信息
struct tclient* create_client(struct tserver *server, int fd) {int i;struct tclient *client = NULL;for (i = 0; i < MAX_CLIENT; ++i) {if (server->clients[i].fd < 0) {client = &server->clients[i];break;}}if (client) {client->fd = fd;client->rsize = MIN_RSIZE;set_nonblocking(fd);        // 设为非阻塞模式epoll_add(server->epollfd, fd, client);     // 增加读事件return client;} else {fprintf(stderr, "too many client: %dn", fd);close(fd);return NULL;}
}// 关闭客户端
void close_client(struct tserver *server, struct tclient *client) {assert(client->fd >= 0);epoll_del(server->epollfd, client->fd);if (close(client->fd) < 0) perror("close: ");client->fd = -1;client->wbsize = 0;free_wblist(&client->wblist);
}// 初始化服务信息
struct tserver* create_server(const char *host, const char *port) {struct tserver *server = malloc(sizeof(*server));memset(server, 0, sizeof(*server));for (int i = 0; i < MAX_CLIENT; ++i) {server->clients[i].fd = -1;}server->epollfd = epoll_create(MAX_CLIENT);server->listenfd = tcpListen(host, port, BACKLOG);epoll_add(server->epollfd, server->listenfd, NULL);return server;
}// 释放服务器
void release_server(struct tserver *server) {for (int i = 0; i < MAX_CLIENT; ++i) {struct tclient *client = &server->clients[i];if (client->fd >= 0) {close_client(server, client);}}epoll_del(server->epollfd, server->listenfd);close(server->listenfd);close(server->epollfd);free(server);
}// 处理接受
void handle_accept(struct tserver *server) {struct sockaddr_storage claddr;socklen_t addrlen = sizeof(struct sockaddr_storage);for (;;) {int cfd = accept(server->listenfd, (struct sockaddr*)&claddr, &addrlen);if (cfd < 0) {int no = errno;if (no == EINTR)continue;perror("accept: ");exit(1);        // 出错}char host[NI_MAXHOST];char service[NI_MAXSERV];if (getnameinfo((struct sockaddr *)&claddr, addrlen, host, NI_MAXHOST, service, NI_MAXSERV, 0) == 0)printf("client connect: fd=%d, (%s:%s)n", cfd, host, service);elseprintf("client connect: fd=%d, (?UNKNOWN?)n", cfd);create_client(server, cfd);break;}
}// 处理读
void handle_read(struct tserver *server, struct tclient *client) {int sz = client->rsize;char *buf = malloc(sz);ssize_t n = read(client->fd, buf, sz);if (n < 0) {        // errorfree(buf);int no = errno;if (no != EINTR && no != EAGAIN && no != EWOULDBLOCK) {perror("read: ");close_client(server, client);}return;}if (n == 0) {       // client closefree(buf);printf("client close: %dn", client->fd);close_client(server, client);return;}// 确定下一次读的大小if (n == sz)client->rsize >>= 1;else if (sz > MIN_RSIZE && n *2 < sz)client->rsize <<= 1;// 加入写缓存add_wbuffer(&client->wblist, buf, n);// 增加写事件epoll_write(server->epollfd, client->fd, client, 1);
}// 处理写
void handle_write(struct tserver *server, struct tclient *client) {struct twblist *list = &client->wblist;while (list->head) {struct twbuffer *wb = list->head;for (;;) {ssize_t sz = write(client->fd, wb->ptr, wb->size);if (sz < 0) {int no = errno;if (no == EINTR)        // 信号中断,继续continue;else if (no == EAGAIN || no == EWOULDBLOCK)   // 内核缓冲满了,下次再来return;else {      // 其他错误 perror("write: ");close_client(server, client);return;}}client->wbsize -= sz;if (sz != wb->size) {       // 未完全发送出去,下次再来wb->ptr += sz;wb->size -= sz;return;}break;}list->head = wb->next;free(wb);}list->tail = NULL;// 到这里写全部完成,关闭写事件epoll_write(server->epollfd, client->fd, client, 0);
}// 先处理错误
void handle_error(struct tserver *server, struct tclient *client) {perror("client error: ");close_client(server, client);
}int main() {signal(SIGPIPE, SIG_IGN);struct tserver *server = create_server("127.0.0.1", "3459");struct epoll_event events[EVENT_NUM];for (;;) {int nevent = epoll_wait(server->epollfd, events, EVENT_NUM, -1);if (nevent <= 0) {if (nevent < 0 && errno != EINTR) {perror("epoll_wait: ");return 1;}continue;}int i = 0;for (i = 0; i < nevent; ++i) {struct epoll_event ev = events[i];if (ev.data.ptr == NULL) {  // accepthandle_accept(server);} else {if (ev.events & (EPOLLIN | EPOLLHUP)) {  // readhandle_read(server, ev.data.ptr);}if (ev.events & EPOLLOUT) {     // writehandle_write(server, ev.data.ptr);}if (ev.events & EPOLLERR) {     // errorhandle_error(server, ev.data.ptr);}}}}release_server(server);return 0;
}

epoll哪些触发模式_网络编程:epoll相关推荐

  1. kqueue epoll 边界触发模式的网络编程模型

    本文并不打算介绍边界触发模式,需要了解的朋友自己到网上搜索. 本文只是打算介绍近期总结的三种边界触发模式的实现方式,后面会实现每一种然后做一个性能比较. 1)模仿windows完成端口的模式. 这是最 ...

  2. 网络编程——epoll

    参考 <TCP/IP网络编程> 尹圣雨 epoll epoll也是Linux下实现I/O复用的一种方法,其性能优于select. 基于select的I/O复用服务器的设计缺陷 调用sele ...

  3. linux线程同步 epoll,Linux网络编程--epoll 模型原理详解以及实例

    1.简介 Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数.Linux 2.6内核中有提高网络I/O性能的新方法,即epoll . epoll是什么?按 ...

  4. epoll哪些触发模式_5.epoll的水平触发和边缘触发

    本篇是多路复用的第五篇,主要来讲解epoll的水平触发和边缘触发是怎么回事. 一.概念介绍 EPOLL事件有两种模型,水平出发和边缘触发,如下所示: 1. Level Triggered (LT) 水 ...

  5. epoll哪些触发模式_epoll的内部实现 百万级别句柄监听 lt和et模式非常好的解释...

    epoll是Linux高效网络的基础,比如event poll(例如nodejs),是使用libev,而libev的底层就是epoll(只不过不同的平台可能用epoll,可能用kqueue). epo ...

  6. 第六周 Java语法总结_设计原则_工厂模式_单例模式_代理模式(静态代理_动态代理)_递归_IO流_网络编程(UDP_TCP)_反射_数据库

    文章目录 20.设计原则 1.工厂模式 2.单例模式 1)饿汉式 2)懒汉式 3.Runtime类 4.代理模式 1)静态代理 2)动态代理 动态代理模板 21.递归 22.IO流 1.File 2. ...

  7. 五种高级IO | select poll epoll 水平触发模式 边缘触发模式 惊群问题

    一.高级IO 在介绍多路复用IO之前,先介绍一下其它四种高级IO: 阻塞IO: 在内核将数据准备好之前,系统调用会一直等待.所以的套集字默认是阻塞方式. 非阻塞IO: 在内核还未将数据准备好,则系统调 ...

  8. socket编程listen函数限制连接数的解决方案_网络编程——服务器篇

    目录 一.客户端实现 二.单进程服务器 2.1 单进程实现 2.2 单进程非阻塞实现 2.3 TCP服务器(select版) 2.4 epoll版服务器实现 三.多进程服务器和多线程服务器 四.协程 ...

  9. epoll边缘触发_4.2.3、epoll:水平触发与边缘触发

    select和poll都只提供了一个函数:select或者poll函数. 而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建 ...

最新文章

  1. 人脸识别是怎么识别的?为什么需要大数据?(原理篇)
  2. 剑指offer:栈的压入、弹出序列
  3. linux下配置DHCP中继代理
  4. 2012 Multi-University #8
  5. 使用 Source Generator 自动生成 WEB API
  6. python字符串随机排序_python 随机数使用方法,推导以及字符串,双色球小程序实例...
  7. python 按条件选择行和列数据_小白学数据结构-排序算法Python(冒泡、选择、快速、希尔等等)...
  8. Spring Restful Web服务示例 - 使用JSON/Jackson和客户端程序
  9. mybatis直接执行sql语句后续之一
  10. 2022年,图机器学习Graph ML发展到哪了?
  11. HttpHelper使用记录
  12. 微信小程序初识到躺坑
  13. CE认证---EMC最新标准-EN55032
  14. SQL中EXISTS的用法
  15. 如何讲好一个故事 - by锤子手机活动策划负责人草威
  16. 夜间灯光数据dn值_DMSP/OLS和VIIRS/DNB夜间灯光影像的校正及拟合
  17. [TJOI2019]唱、跳、rap和篮球——NTT+生成函数+容斥
  18. ElasticSearch Cause: Cluster state has not been recovered yet, cannot write to the [null] index
  19. 区块链交易验证和支付验证
  20. 跟同事关系再好,这3种话宁烂肚里也别张嘴,莫让福运悄悄离开你

热门文章

  1. httpclient 忽略证书_对接外部接口,又一次证书问题!
  2. 虚拟机centos7 识别不出网卡的解决方案
  3. switch_to及ret_from_sys_call控制任务的切换与返回
  4. 【模电基础】滤波电容的选择
  5. 【JUnit 报错】 method initializationerror not found:JUnit4单元测试报错问题
  6. 二进制八进制十六进制之间的快速转换------ 心算笔算方法总结
  7. 【filezilla】 ubuntu下安装filezilla
  8. 【转】AngularJs 弹出框 model(模态框)
  9. 腾讯、百度、阿里、微软面试题精选(不断更新)
  10. IOS开发之手势—UIGestureRecognizer 共存