本文转载自epoll原理详解及epoll反应堆模型

导语

设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包),也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效的处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?实际上,在Linux2.4版本以前,那时的select或者poll事件驱动方式是这样做的。

这里有个非常明显的问题,即在某一时刻,进程收集有事件的连接时,其实这100万连接中的大部分都是没有事件发生的。因此如果每次收集事件时,都把100万连接的套接字传给操作系统(这首先是用户态内存到内核态内存的大量复制),而由操作系统内核寻找这些连接上有没有未处理的事件,将会是巨大的资源浪费,然后select和poll就是这样做的,因此它们最多只能处理几千个并发连接。而epoll不这样做,它在Linux内核中申请了一个简易的文件系统,把原先的一个select或poll调用分成了3部分:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

详解:

  1. 调用epoll_create建立一个epoll对象(在epoll文件系统中给这个句柄分配资源);
  2. 调用epoll_ctlepoll对象中添加这100万个连接的套接字;
  3. 调用epoll_wait收集发生事件的连接。

这样只需要在进程启动时建立1个epoll对象,并在需要的时候向它添加或删除连接就可以了,因此,在实际收集事件时,epoll_wait的效率就会非常高,因为调用epoll_wait时并没有向它传递这100万个连接,内核也不需要去遍历全部的连接。

epoll原理详解

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,如下所示:

struct eventpoll {
  ...
  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
  也就是这个epoll监控的事件*/
  struct rb_root rbr;
  /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
  struct list_head rdllist;
  ...
};

我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。

epoll中对于每一个事件都会建立一个epitem结构体,如下所示:

struct epitem {
  ...
  //红黑树节点
  struct rb_node rbn;
  //双向链表节点
  struct list_head rdllink;
  //事件句柄等信息
  struct epoll_filefd ffd;
  //指向其所属的eventepoll对象
  struct eventpoll *ep;
  //期待的事件类型
  struct epoll_event event;
  ...
}; // 这里包含每一个事件对应着的信息。

当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接。

总结:

  • 一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。
  • 执行epoll_create()时,创建了红黑树和就绪链表;
  • 执行epoll_ctl()时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不- 存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;
  • 执行epoll_wait()时立刻返回准备就绪链表里的数据即可。

epoll的两种触发模式

epollEPOLLLTEPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

  • LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;
  • ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。
  • 还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fdepoll_wait便可以收到通知。

epoll为什么要有EPOLLET触发模式?:

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边缘触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

总结:

  • ET模式(边缘触发)只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回。

  • LT 模式(水平触发,默认)只要有数据都会触发,缓冲区剩余未读尽的数据会导致epoll_wait返回。

epoll反应堆模型

epoll模型原来的流程:

epoll_create(); // 创建监听红黑树
epoll_ctl(); // 向书上添加监听fd
epoll_wait(); // 监听
有监听fd事件发送--->返回监听满足数组--->判断返回数组元素--->
lfd满足accept--->返回cfd---->read()读数据--->write()给客户端回应。

epoll反应堆模型的流程:

epoll_create(); // 创建监听红黑树
epoll_ctl(); // 向书上添加监听fd
epoll_wait(); // 监听
有客户端连接上来--->lfd调用acceptconn()--->将cfd挂载到红黑树上监听其读事件--->
epoll_wait()返回cfd--->cfd回调recvdata()--->将cfd摘下来监听写事件--->
epoll_wait()返回cfd--->cfd回调senddata()--->将cfd摘下来监听读事件--->...--->

Demo:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h> #define MAX_EVENTS 1024 /*监听上限*/
#define BUFLEN 4096 /*缓存区大小*/
#define SERV_PORT 6666 /*端口号*/ void recvdata(int fd,int events,void *arg);
void senddata(int fd,int events,void *arg); /*描述就绪文件描述符的相关信息*/
struct myevent_s
{
int fd; //要监听的文件描述符
int events; //对应的监听事件,EPOLLIN和EPLLOUT
void *arg; //指向自己结构体指针
void (*call_back)(int fd,int events,void *arg); //回调函数
int status; //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
char buf[BUFLEN];
int len;
long last_active; //记录每次加入红黑树 g_efd 的时间值
}; int g_efd; //全局变量,作为红黑树根
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组. +1-->listen fd /*
* 封装一个自定义事件,包括fd,这个fd的回调函数,还有一个额外的参数项
* 注意:在封装这个事件的时候,为这个事件指明了回调函数,一般来说,一个fd只对一个特定的事件
* 感兴趣,当这个事件发生的时候,就调用这个回调函数
*/
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int fd,int events,void *arg), void *arg)
{
ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
if(ev->len <= 0)
{
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
}
ev->last_active = time(NULL); //调用eventset函数的时间
return;
} /* 向 epoll监听的红黑树 添加一个文件描述符 */
void eventadd(int efd, int events, struct myevent_s *ev)
{
struct epoll_event epv={0, {0}};
int op = 0;
epv.data.ptr = ev; // ptr指向一个结构体(之前的epoll模型红黑树上挂载的是文件描述符cfd和lfd,现在是ptr指针)
epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
if(ev->status == 0) //status 说明文件描述符是否在红黑树上 0不在,1 在
{
op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1
ev->status = 1;
}
if(epoll_ctl(efd, op, ev->fd, &epv) < 0) // 添加一个节点
printf("event add failed [fd=%d],events[%d]\n", ev->fd, events);
else
printf("event add OK [fd=%d],events[%0X]\n", ev->fd, events);
return;
} /* 从epoll 监听的 红黑树中删除一个文件描述符*/
void eventdel(int efd,struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
if(ev->status != 1) //如果fd没有添加到监听树上,就不用删除,直接返回
return;
epv.data.ptr = NULL;
ev->status = 0;
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);
return;
} /* 当有文件描述符就绪, epoll返回, 调用该函数与客户端建立链接 */
void acceptconn(int lfd,int events,void *arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1)
{
if(errno != EAGAIN && errno != EINTR)
{
sleep(1);
}
printf("%s:accept,%s\n",__func__, strerror(errno));
return;
}
do
{
for(i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素,类似于select中找值为-1的元素
{
if(g_events[i].status ==0)
break;
}
if(i == MAX_EVENTS) // 超出连接数上限
{
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break;
}
int flag = 0;
if((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) //将cfd也设置为非阻塞
{
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
eventset(&g_events[i], cfd, recvdata, &g_events[i]); //找到合适的节点之后,将其添加到监听树中,并监听读事件
eventadd(g_efd, EPOLLIN, &g_events[i]);
}while(0); printf("new connect[%s:%d],[time:%ld],pos[%d]",inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return;
} /*读取客户端发过来的数据的函数*/
void recvdata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len; len = recv(fd, ev->buf, sizeof(ev->buf), 0); //读取客户端发过来的数据 eventdel(g_efd, ev); //将该节点从红黑树上摘除 if (len > 0)
{
ev->len = len;
ev->buf[len] = '\0'; //手动添加字符串结束标记
printf("C[%d]:%s\n", fd, ev->buf); eventset(ev, fd, senddata, ev); //设置该fd对应的回调函数为senddata
eventadd(g_efd, EPOLLOUT, ev); //将fd加入红黑树g_efd中,监听其写事件 }
else if (len == 0)
{
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
}
else
{
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return;
} /*发送给客户端数据*/
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len; len = send(fd, ev->buf, ev->len, 0); //直接将数据回射给客户端 eventdel(g_efd, ev); //从红黑树g_efd中移除 if (len > 0)
{
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventset(ev, fd, recvdata, ev); //将该fd的回调函数改为recvdata
eventadd(g_efd, EPOLLIN, ev); //重新添加到红黑树上,设为监听读事件
}
else
{
close(ev->fd); //关闭链接
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
} /*创建 socket, 初始化lfd */ void initlistensocket(int efd, short port)
{
struct sockaddr_in sin; int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞 memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port); bind(lfd, (struct sockaddr *)&sin, sizeof(sin)); listen(lfd, 20); /* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]); /* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); //将lfd添加到监听树上,监听读事件 return;
} int main()
{
int port=SERV_PORT; g_efd = epoll_create(MAX_EVENTS + 1); //创建红黑树,返回给全局 g_efd
if(g_efd <= 0)
printf("create efd in %s err %s\n", __func__, strerror(errno)); initlistensocket(g_efd, port); //初始化监听socket struct epoll_event events[MAX_EVENTS + 1]; //定义这个结构体数组,用来接收epoll_wait传出的满足监听事件的fd结构体
printf("server running:port[%d]\n", port); int checkpos = 0;
int i;
while(1)
{
/* long now = time(NULL);
for(i=0; i < 100; i++, checkpos++)
{
if(checkpos == MAX_EVENTS);
checkpos = 0;
if(g_events[checkpos].status != 1)
continue;
long duration = now -g_events[checkpos].last_active;
if(duration >= 60)
{
close(g_events[checkpos].fd);
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]);
}
} */
//调用eppoll_wait等待接入的客户端事件,epoll_wait传出的是满足监听条件的那些fd的struct epoll_event类型
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0)
{
printf("epoll_wait error, exit\n");
exit(-1);
}
for(i = 0; i < nfd; i++)
{
//evtAdd()函数中,添加到监听树中监听事件的时候将myevents_t结构体类型给了ptr指针
//这里epoll_wait返回的时候,同样会返回对应fd的myevents_t类型的指针
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
//如果监听的是读事件,并返回的是读事件
if((events[i].events & EPOLLIN) &&(ev->events & EPOLLIN))
{
ev->call_back(ev->fd, events[i].events, ev->arg);
}
//如果监听的是写事件,并返回的是写事件
if((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT))
{
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
return 0;
}

参考

https://blog.csdn.net/qq_36359022/article/details/81355897

https://blog.csdn.net/weixin_40204595/article/details/83213332

http://www.cnblogs.com/pluser/p/epoll_principles.html

https://www.cnblogs.com/aspirant/p/9166944.html

epoll原理详解及epoll反应堆模型的更多相关文章

  1. EPOLL原理详解(图文并茂)

    文章核心思想是: 要清晰明白EPOLL为什么性能好. 本文会从网卡接收数据的流程讲起,串联起CPU中断.操作系统进程调度等知识:再一步步分析阻塞接收数据.select到epoll的进化过程:最后探究e ...

  2. Java网络编程和NIO详解6:Linux epoll实现原理详解

    Java网络编程和NIO详解6:Linux epoll实现原理详解 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO h ...

  3. epoll机制详解

    epoll机制详解 大牛的详解 epoll详解 什么是epoll? epoll是为处理大批量句柄而作了改进的poll, 是性能最好的多路I/O就绪通知方法; 只有三个系统调用: epoll_creat ...

  4. epoll使用详解(精髓)

    epoll使用详解(精髓) epoll - I/O event notification facility 在linux的网络编程中,很长的时间都在使用select来做事件触发.在linux新的内核中 ...

  5. Java网络编程和NIO详解3:IO模型与Java网络编程模型

    Java网络编程和NIO详解3:IO模型与Java网络编程模型 基本概念说明 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32 ...

  6. 块级格式化上下文(block formatting context)、浮动和绝对定位的工作原理详解

    CSS的可视化格式模型中具有一个非常重要地位的概念——定位方案.定位方案用以控制元素的布局,在CSS2.1中,有三种定位方案——普通流.浮动和绝对定位: 普通流:元素按照先后位置自上而下布局,inli ...

  7. 【转】VLAN原理详解

    1.为什么需要VLAN 1.1 什么是VLAN? VLAN(Virtual LAN),翻译成中文是“虚拟局域网”.LAN可以是由少数几台家用计算机构成的网络,也可以是数以百计的计算机构成的企业网络.V ...

  8. 【转载】JAVA消息服务JMS规范及原理详解

    转载:https://www.cnblogs.com/molao-doing/articles/6557305.html 作者: moyun- 一.简介 JMS即Java消息服务(Java Messa ...

  9. JAVA消息服务JMS规范及原理详解

    JAVA消息服务JMS规范及原理详解 一.简介 JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应 ...

随机推荐

  1. MVC与三层架构解析学习

    概要 MVC与三层架构不是简单的相等,二者之间存在一些区别. 今天,看到一位博主总结笔记,借鉴而来,以供以后学习. 将javaweb开发中的MVC(SSM框架)与三级架构比较,来解析二者之间的关系. ...

  2. docker通过dockerfile构建JDK最小镜像,Docker导出导入镜像

    docker通过dockerfile构建JDK最小镜像,Docker导出导入镜像 一.docker通过dockerfile构建JDK最小镜像 1.1 下载JRE 1.2 解压JRE,删除相关不需要文件 ...

  3. GeoMesa命令行,索引概述

    GeoMesa 一.GeoMesa命令行 查看classpath 创建表 描述表 批量导入数据 解释查询 统计分析 导出feature 删除feature 获取目录中的全部表的名称 删除表 删除目录 ...

  4. Docker及其使用思维导图

    学习Docker的使用. 包括Docker的一些命令,Docker容器的使用,Docker镜像的使用,Docker容器连接等内容. 各种思维导图下载地址 Docker使用思维导图及各各种命令 Dock ...

  5. 使用FOR XML PATH实现多行数据合并成一列

    有时为避免循环操作数据库.列表展示等一些原因需要将数据及关联数据批量加载进行集中处理,一种解决办法可以使用FOR XML PATH将多行数据合并成一列,达到字段拼接的效果.例如有两个表, 部门表T_D ...

  6. 小希的迷宫B - B

    上次Gardon的迷宫城堡小希玩了很久(见Problem B),现在她也想设计一个迷宫让Gardon来走.但是她设计迷宫的思路不一样,首先她认为所有的通道都应该是双向连通的,就是说如果有一个通道连通了 ...

  7. HDU - 3374 String Problem (kmp求循环节+最大最小表示法)

    做一个高产的菜鸡 传送门:HDU - 3374 题意:多组输入,给你一个字符串,求它最小和最大表示法出现的位置和次数. 题解:刚刚学会最大最小表示法,amazing.. 次数就是最小循环节循环的次数. ...

  8. fzu2198 快来快来数一数

    Accept: 204    Submit: 627 Time Limit: 1000 mSec    Memory Limit : 65536 KB  Problem Description n个六 ...

  9. fzu2202 犯罪嫌疑人

    Problem Description 福尔摩斯是个大侦探,他总是在解决疑难案件.这一次的案件也不例外,案件是这样的:有编号为1到N的N位嫌疑犯,他们其中有一个犯了罪,然后每个嫌疑犯都被询问,&quo ...

  10. 浅谈Webpack模块打包工具二

    Webpack插件机制介绍 Loader专注实现资源模块加载,plugin解决其他自动化工作,比如清除dist目录,拷贝静态文件值输出目录,压缩输出代码等,下面列举几个plugin 的例子. Webp ...