epoll反应堆模型实现
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反应堆模型实现的更多相关文章
- epoll反应堆模型
================================ 下面代码实现的思想:epoll反应堆模型:( libevent 网络编程开源库 核心思想) 1. 普通多路IO转接服务器: 红黑树 ― ...
- epoll原理详解及epoll反应堆模型
本文转载自epoll原理详解及epoll反应堆模型 导语 设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包),也就是说在每一时刻 ...
- epoll反应堆模型代码
libevent函数库核心思想 /*** epoll_loop.c ***/ #include<stdio.h> #include<sys/epoll.h> #include& ...
- epoll 反应堆
epoll反应堆模型 ================================ 下面代码实现的思想:epoll反应堆模型:( libevent 网络编程开源库 核心思想) . 普通多路IO转接 ...
- (转)Linux下select, poll和epoll IO模型的详解
Linux下select, poll和epoll IO模型的详解 原文:http://blog.csdn.net/tianmohust/article/details/6677985 一).Epoll ...
- Linux下select, poll和epoll IO模型的详解
http://blog.csdn.net/tianmohust/article/details/6677985 一).Epoll 介绍 Epoll 可是当前在 Linux 下开发大规模并发网络程序的热 ...
- epoll事件模型
事件模型 EPOLL事件有两种模型: Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据. Level Triggered (LT) 水平触发只要有数据都会触 ...
- 网络编程学习——Linux epoll多路复用模型
前言 后端开发的应该都知道Nginx服务器,Nginx是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器.后端部署中一般使用的就是Nginx反向代理技术. ...
- Nginx Epoll事件模型优劣
L30-31 Epoll 性能优势主要源于它不用遍历 假设有100万个链接 其它事件可能都需要遍历所有链接,而Epoll只要遍历活跃的链接,这样大大提升了效率
随机推荐
- Python3.7 比较两个Excel文件指定列的值的异同,并将核对结果写入Excel中(含升级版本)
背景: 最近工作中需要核对客户的历史数据, 接近400个产品,需要核对的列有15列,行数有8000+行 肉眼核对简直要吐血 心想着反正在学python呢 人生苦短 何不用python写个脚本 帮助我核 ...
- Linux的六种查找命令
http://www.ruanyifeng.com/blog/2009/10/5_ways_to_search_for_files_using_the_terminal.html 1. find fi ...
- solr -window 安装与启动
1.下载 官网路径 https://solr.apache.org/downloads.html 为了稳定,我用 5.4.1 版本的 , 这是下载地址 https://archive.apac ...
- antd中的form表单 initialValue导致数据不更新问题
初步理解 : initialValue就是所谓的defaultValue,只会在第一次赋值的时候改变,却又有一些不同,因为 initialValue又会因其他改动而改变. 然而当获取的数据重新上来要渲 ...
- debian8.4系统安装后的一些设置
1.添加软件源 su到root用户vi /etc/apt/sources.list 也可用gedit /etc/apt/sources.list (gnome下用,如果kde下则用 ...
- Go语言系列- Socket编程和Redis
Socket编程 一.socket编程概述 什么是socket编程? socket编程是计算机PC机器上2个程序通过一个双向的通信连接实现数据的交互,这个连接的一端就是一个socket.socket的 ...
- Mybatis实现分包定义数据库
Mybatis实现分包定义数据库 背景 业务需求中需要连接两个数据库处理数据,需要用动态数据源.通过了解mybatis的框架,计划 使用分包的方式进行数据源的区分. 原理 前提: 我们使用mybati ...
- SYCOJ223图书管理员
题目-图书管理员 (shiyancang.cn) 图书馆中每本书都有一个图书编码,可以用于快速检索图书,这个图书编码是一个正整数. 每位借书的读者手中有一个需求码,这个需求码也是一个正整数. 如果一本 ...
- XCTF(Web_php_unserialize)
拿到题目,是个这, 我们来一波代码审计 1 <?php 2 class Demo { 3 private $file = 'index.php'; 4 public function __con ...
- Java实现抽奖模块的相关分享
Java实现抽奖模块的相关分享 最近进行的项目中,有个抽奖的需求,今天就把相关代码给大家分享一下. 一.DAO层 /** * 获取奖品列表 * @param systemVersion 手机系统版本( ...