epoll反应堆模型demo实现

在高并发TCP请求中,为了实现资源的节省,效率的提升,Epoll逐渐替代了之前的select和poll,它在用户层上规避了忙轮询这种效率不高的监听方式,epoll的时间复杂度为O(1), 也就意味着,epoll在高并发场景,随着文件描述符的增长,有良好的可扩展性。

  • select 和 poll 监听文件描述符list,进行一个线性的查找 O(n)
  • epoll: 使用了内核文件级别的回调机制O(1)

关键函数有三个:

  • epoll_create(int size): 创建一个epoll实例,文件描述符

    返回一个对象,即红黑树的树根,在代码实现中多为全局变量文件描述符。size参数是对连接的预估,对内核只有参考价值,实际超过也无妨

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

    该系统调用用于添加、修改或删除文件引用的列表描述符epfd。它要求执行操作op对于目标文件描述符fd。op的操作有添加,删除,更改。事件参数描述链接到文件的对象描述符fd。数据类型为下方核心结构epoll_event。

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

    等待epoll事件从epoll实例中发生, 并返回事件以及对应文件描述符.

    当timeout大于0时为规定时限,等于0为非阻塞,小于0为阻塞,maxevents为events可以接收的最大元素个数,即最大返回事件个数,events是一个传出数组,它用来接收返回事件。

核心数据结构

点击查看代码
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 */
};

原理

当某一进程调用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是非常高效的。

触发模式

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;

ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。

还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

【总结】:

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

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

反应堆模型流程

【epoll反应堆模型的流程】:

epoll_create(); // 创建监听红黑树

epoll_ctl(); // 向书上添加监听fd

epoll_wait(); // 监听

有客户端连接上来--->lfd调用acceptconn()--->将cfd挂载到红黑树上监听其读事件--->

epoll_wait()返回cfd--->cfd回调recvdata()--->将cfd摘下来监听写事件--->

epoll_wait()返回cfd--->cfd回调senddata()--->将cfd摘下来监听读事件--->...--->

demo

点击查看代码
/*
epoll基于非阻塞I/O事件驱动
*/
#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 8080 void recvdata(int fd ,int events , void*arg);
void senddata(int fd ,int events , void*arg); struct myevent_s
{
int fd; //要监听的文件描述符
int events; //对应的监听事件
void *arg; //泛型函数
void (*call_back)(int fd, int events,void *arg); //回调函数
int status; //是否在监听:1 在红黑树上(监听) 0 不在
char buf[BUFLEN]; //缓冲区
int len; //缓冲区的大小
long last_active; //记录加入红黑树的时间
}; int g_efd; //全局变量 记录epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组 //初始化结构体myevent_s成员变量
void eventset(struct myevent_s *ev , int fd, void (*call_back)(int, int ,void*),void *arg)
{
ev->fd=fd;
ev->events=0;
ev->call_back=call_back;
ev->status=0;
ev->arg = arg;
if(ev->len <= 0)
{
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
}
ev->last_active= (time)NULL;
return;
} /*向监听红黑树添加一个文件描述符*/
void event_add(int efd , int events , struct myevent_s *ev){
struct epoll_event epv = {0,{0}};
int op;
epv.events = ev->events = events;
epv.data.ptr = ev; if(ev->status == 0 ){
op = EPOLL_CTL_ADD; //将其加入到红黑树gfd中
ev->status = 1;
}
if(epoll_ctl(efd,op,ev->fd,&epv)<0){
printf("epv add failed\n");
}
else
printf("epv add succeed\n");
} void eventdel(int efd ,struct myevent_s *ev){
struct epoll_event epv = {0,{0}};
if(ev->status != 1){
return;
}
epv.data.ptr = NULL;
ev->status = 0;
epoll_ctl(efd,EPOLL_CTL_DEL,ev->fd, &epv);
return ;
} 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)
{
printf("accept error\n");
return;
}
else
{
printf("connect success\n");
}
do
{
for (i =0;i<MAX_EVENTS;i++)
if(g_events[i].status == 0)
break;
if(i == MAX_EVENTS){
printf("connect limit\n");
break;
}
int flag = 0;
if((flag= fcntl(cfd,F_SETFL,O_NONBLOCK))< 0 ){
printf("nonblock failed\n");
break;
} eventset(&g_events[i],cfd,recvdata,&g_events[i]);
event_add(g_efd,EPOLLIN,&g_events[i]);
} while (0);
return;
}
//接收data
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); //读文件描述符,数据存到myevent_s中
eventdel(g_efd, ev); //将节点从红黑树中摘下
if(len>0){
ev->len = len;
ev->buf[len] = '\0'; //手动添加结束标志
eventset(ev,fd,senddata,ev);
event_add(g_efd,EPOLLOUT,ev); //将fd添加到g_efd 中监听写事件 }
else if(len == 0){
close(ev->fd);
printf("recv null and close");
}
else{
close(ev->fd);
printf("recv error");
}
} 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); //从红黑树中删除
if(len>0){
printf("send success");
eventset(ev,fd,recvdata,ev);
event_add(g_efd,EPOLLIN,ev);
}
else{
close(fd);
printf("send error");
}
} 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));
sin.sin_family=AF_INET;
sin.sin_addr.s_addr =INADDR_ANY;
sin.sin_port = htons(SERV_PORT); bind(lfd, (struct sockaddr *)&sin,sizeof(sin));
listen(lfd , 128);
eventset(&g_events[MAX_EVENTS],lfd,acceptconn,&g_events[MAX_EVENTS]); event_add(g_efd,EPOLLIN,&g_events[MAX_EVENTS]); } int main(){
int port = SERV_PORT;
g_efd = epoll_create(MAX_EVENTS+1); //创建红黑树返回给全局文件描述符
if(g_efd<=0){
printf("create epoll error");
}
initlistensocket(g_efd,port); //初始化监听socket struct epoll_event events[MAX_EVENTS+1]; //保存已经满足就绪事件的文件描述符数组 为epoll_wait做准备
printf("server running:port[%d]\n", port); int checkpos = 0 ,i;
while (1)
{ //超时验证
/*long now = time(NULL);
for(i = 0 ;i<100;i++){
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("timeout");
eventdel(g_efd,&g_events[checkpos]);
}
}*/ //监听红黑树g_efd,将满足的事件添加到g_events中。
int nfd = epoll_wait(g_efd,events,MAX_EVENTS+1,1000);
if(nfd<0){
printf("epoll_wait error");
exit(-1);
} for(i = 0 ;i < nfd ;i++){
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);
}
} } }

使用方法:
gcc server.c -o server
./server
重新打开新的终端:
nc 127.1 8080

epoll反应堆模型实现的更多相关文章

  1. epoll反应堆模型

    ================================ 下面代码实现的思想:epoll反应堆模型:( libevent 网络编程开源库 核心思想) 1. 普通多路IO转接服务器: 红黑树 ― ...

  2. epoll原理详解及epoll反应堆模型

    本文转载自epoll原理详解及epoll反应堆模型 导语 设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包),也就是说在每一时刻 ...

  3. epoll反应堆模型代码

    libevent函数库核心思想 /*** epoll_loop.c ***/ #include<stdio.h> #include<sys/epoll.h> #include& ...

  4. epoll 反应堆

    epoll反应堆模型 ================================ 下面代码实现的思想:epoll反应堆模型:( libevent 网络编程开源库 核心思想) . 普通多路IO转接 ...

  5. (转)Linux下select, poll和epoll IO模型的详解

    Linux下select, poll和epoll IO模型的详解 原文:http://blog.csdn.net/tianmohust/article/details/6677985 一).Epoll ...

  6. Linux下select, poll和epoll IO模型的详解

    http://blog.csdn.net/tianmohust/article/details/6677985 一).Epoll 介绍 Epoll 可是当前在 Linux 下开发大规模并发网络程序的热 ...

  7. epoll事件模型

    事件模型 EPOLL事件有两种模型: Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据. Level Triggered (LT) 水平触发只要有数据都会触 ...

  8. 网络编程学习——Linux epoll多路复用模型

    前言 后端开发的应该都知道Nginx服务器,Nginx是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器.后端部署中一般使用的就是Nginx反向代理技术. ...

  9. Nginx Epoll事件模型优劣

    L30-31 Epoll 性能优势主要源于它不用遍历 假设有100万个链接 其它事件可能都需要遍历所有链接,而Epoll只要遍历活跃的链接,这样大大提升了效率

随机推荐

  1. JMeter_使用正则和JSON提取器参数化(常用于提取token)

    一.使用正则表达式提取器提取token 查看登录响应参数找出token.图中token为 "ticketString": "ccf26b17-a96f-4913-8925 ...

  2. 细谈 == 和 equals 的具体区别 【包括equals源码分析】

    1.前言 昨天舍友去面试,被面试官的问题难住了:俩个学生类除了学生姓名不同用.equal来比较. 我是一脸懵逼的 ,问题说的很模糊 , 理解字面意思为 :一个 实体类名叫Student ,内部属性有学 ...

  3. dispatcher-servlet.xml文件配置模板

    完整代码如下: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http:/ ...

  4. C#进阶——从应用上理解异步编程的作用(async / await)

    欢迎来到学习摆脱又加深内卷篇 下面是学习异步编程的应用 1.首先,我们建一个winfrom的项目,界面如下: 2.然后先写一个耗时函数: /// <summary> /// 耗时工作 // ...

  5. vue render中如何正确配置img的路径

    第一种:适用于静态路径 attrs: { src: require('../common/images/logo.png'), title: 'img' } 第二种:适用于动态路径 domProps: ...

  6. android入门--环境搭建

    运行环境 windows 7 下载地址 环境下载 最近开接触Android(安卓)嵌入式开发,首要问题是搭建Andoid开发环境,由于本人用的是windows7的笔记本,也就只能到Windows中搭建 ...

  7. httprunner3.x全网最详细教程

    一.所需环境 wiindows10以上 python3.6以上 httprunner3.1.6(最新版本) pycharm社区版 二.安装httprunner 1.卸载旧版本 卸载之前版本的命令为:p ...

  8. 机器学习|线性回归三大评价指标实现『MAE, MSE, MAPE』(Python语言描述)

    原文地址 ?传送门 对于回归预测结果,通常会有平均绝对误差.平均绝对百分比误差.均方误差等多个指标进行评价.这里,我们先介绍最常用的3个: 平均绝对误差(MAE) 就是绝对误差的平均值,它的计算公式如 ...

  9. MySQL提权之mof提权

    mof提权原理 关于 mof 提权的原理其实很简单,就是利用了 c:/windows/system32/wbem/mof/ 目录下的 nullevt.mof 文件,每分钟都会在一个特定的时间去执行一次 ...

  10. 《剑指offer》面试题34. 二叉树中和为某一值的路径

    问题描述 输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径.从树的根节点开始往下一直到叶节点所经过的节点形成一条路径. 示例: 给定如下二叉树,以及目标和 sum = 22, 5 ...