I/O多路复用——epoll函数
1 select的低效率
select/poll函数效率比较低,主要有以下两个原因:
(1)调用select函数后需要对所有文件描述符进行循环查找
(2)每次调用select函数时都需要向该函数传递监视对象信息
在这两个原因中,第二个原因是主要原因:每次调用select函数时,应用程序都要将所有文件描述符传递给操作系统,这给程序带来很大的负担。在高并发的环境下,无论怎样优化应用程序的代码,都无法完成应用的服务。
所以,select与poll并不适合以Web服务器端开发为主流的现代开发环境,只在要求满足以下两个条件是适用:
(1)服务器端接入者少
(2)程序要求兼容性
2 Linux的epoll机制
由上一节,我们需要一种类似于select的机制来完成高并发的服务器。需要有以下两个特点(epoll和select的区别):
(1)应用程序仅向操作系统传递1次监视对象
(2)监视范围或内容发生变化是,操作系统只通知发生变化的事项给应用程序
幸运的是,的确存在这样的机制。Linux的支持方式是epoll,Windows的支持方式是IOCP。
3 epoll函数原型
epoll操作由三个函数组成:
#include <sys/epoll.h>
int epoll_create(int size);
//成功时返回epoll文件描述符,失败时返回-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//成功时返回0,失败时返回-1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//成功时返回发生事件的文件描述数,失败时返回-1
(1)epoll_create:创建保存epoll文件描述符的空间。
调用epoll_create函数时创建的文件描述符保存空间称为“epoll例程”。但要注意:size参数只是应用程序向操作系统提的建议,操作系统并不一定会生成一个大小为size的epoll例程。
(2)epoll_ctl:向空间注册并注销文件描述符。
参数epfd指定注册监视对象的epoll例程的文件描述符,op指定监视对象的添加、删除或更改等操作,有以下两种常量:
1)EPOLL_CTL_ADD:将文件描述符注册到epoll例程
2)EPOLL_CTL_DEL:从epoll例程中删除文件描述符
3)EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况
fd指定需要注册的监视对象文件描述符,event指定监视对象的事件类型。epoll_event结构体如下:
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
epoll_event的成员events中可以保存的常量及所指的事件类型有以下:
1)EPOLLIN:需要读取数据的情况
2) EPOLLOUT:输出缓冲为空,可以立即发送数据的情况
3) EPOLLPRI:收到OOBO数据的情况
4) EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用
5) EPOLLERR:发生错误的情况
6) EPOLLET:以边缘触发的方式得到事件通知
7) EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数EPOLL_CTL_MOD,再次设置事件。
(3)epoll_wait:与select函数类似,等待文件描述符发生变化。操作系统返回epoll_event类型的结构体通知监视对象的变化。timeout函数是为毫秒为单位的等待时间,传递-1时,一直等待直到事件发生。声明足够大的epoll_event结构体数组后,传递给epoll_wait函数时,发生变化的文件符信息将被填入该数组。因此,不需要像select函数那样针对所有文件符进行循环。
4 基于epoll的echo服务器代码:
#define BUF_SIZE 1024
#define EPOLL_SIZE 50
void error_handling(char *buf); int main(int argc, char *argv[])
{
int listenfd, connfd;
struct sockaddr_in serv_addr;
socklen_t socklen;
char buf[BUF_SIZE]; int epfd, event_cnt;
struct epoll_event *ep_events;
struct epoll_event event; if (argc != )
{
printf("Usage: echo <port>\n");
exit();
} listenfd = socket(PF_INET, SOCK_STREAM, );
memset(&serv_addr, , sizeof(serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[])); if (bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -)
error_handling("bind() error\n");
if (listen(serv_addr, ) == -)
error_handling("listen() error\n"); epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(epoll_event)*EPOLL_SIZE); event.event = EPOLLIN;
event.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event); for (;;)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -);
if (event_cnt == -)
error_handling("epoll_wait() error\n");
for (int i = ; i < event_cnt; ++i)
{
if (ep_events[i].data.fd == listenfd)
{
connfd = accept(listenfd, NULL, NULL);
event.events = EPOLLIN;
event.data.fd = connfd;
epoll_ctl(pefd, EPOLL_CTL_ADD, connfd, &event);
printf("connect another client\n");
}
else
{
int nread = read(ep_events[i].dada.fd, buf, BUF_SIZE);
if (nread == )
{
close(ep_events.data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events.data.fd, NULL);
printf("disconnect with a client\n");
}
else
{
write(ep_events[i].data.fd, buf, nread);
}
}
}
}
close(listenfd);
close(epfd);
return ;
} void error_handling(char* buf)
{
printf("%s\n", buf);
exit();
}
5 条件触发与边缘触发
条件触发:只要引起epoll_wait返回的事件还存在,再次调用epoll_wait时,该事件还会被注册
边缘触发:每个事件在刚发生的时候被注册一次,之后就不会被注册,除非又有新的事件发生。
比如,一个已连接的socket套接字收到了数据,而读取缓冲区小于接收到的数据,这时,两种触发方式有以下区别:(1)条件触发:一次读取之后,套接字缓冲区里还有数据,再调用epoll_wait,该套接字的EPOLL_IN事件还是会被注册;(2)边缘触发:一次读取之后,套接字缓冲区里还有数据,再调用epoll_wait,该套接字的EPOLL_IN事件不会被注册,除非在这期间,该套接字收到了新的数据。
epoll默认采用条件触发,上一节的代码采用的就是条件触发。
还是不太清楚?用代码来砸!边缘触发实现echo服务器:
//设置较小的读取缓冲区,以测试边缘触发特性
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char *buf); int main(int argc, char *argv[])
{
int listenfd, connfd;
struct sockaddr_in serv_addr;
socklen_t socklen;
char buf[BUF_SIZE]; int epfd, event_cnt;
struct epoll_event *ep_events;
struct epoll_event event; if (argc != )
{
printf("Usage: echo <port>\n");
exit();
} listenfd = socket(PF_INET, SOCK_STREAM, );
memset(&serv_addr, , sizeof(serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[])); if (bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -)
error_handling("bind() error\n");
if (listen(serv_addr, ) == -)
error_handling("listen() error\n"); epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(epoll_event)*EPOLL_SIZE); event.event = EPOLLIN;
event.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event); for (;;)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -);
if (event_cnt == -)
error_handling("epoll_wait() error\n");
printf("event_cnt() return\n"); //指示一次返回
for (int i = ; i < event_cnt; ++i)
{
if (ep_events[i].data.fd == listenfd)
{
connfd = accept(listenfd, NULL, NULL);
//设置为非阻塞I/O
int flag = fcntl(fd, F_GETFL, );
fcntl(fd, F_SETFL, flag | O_NONBLOCK); event.events = EPOLLIN|EPOLLET; //边缘触发
event.data.fd = connfd;
epoll_ctl(pefd, EPOLL_CTL_ADD, connfd, &event);
printf("connect another client\n");
}
else
{
//读完每个已连接socket的缓冲区里的数据
while ()
{
int nread = read(ep_events[i].dada.fd, buf, BUF_SIZE);
if (nread == )
{
close(ep_events.data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events.data.fd, NULL);
printf("disconnect with a client\n");
}
else if (nread < )
{
//errno为EAGAIN,则缓冲区内已没有数据
if (errno == EAGAIN)
break;
}
else
{
write(ep_events[i].data.fd, buf, nread);
}
} }
}
}
close(listenfd);
close(epfd);
return ;
} void error_handling(char* buf)
{
printf("%s\n", buf);
exit();
}
几个说明:
(1)在使用epoll_ctl注册事件的时候,选择边缘触发,|EPOLLET
(2)处理已发生的边缘触发的事件时,要处理完所有的数据再返回。例中,使用了循环的方式读取了套接字中的所有数据
(3)读/写套接字的时候采用非阻塞式I/O。为何?边缘触发方式下,以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿。
那么边缘触发好不好?有什么优点呢?书上说,边缘触发可以分离接收数据和处理数据的时间点。也就是说,在事件发生的时候,我们只记录事件已经发生,而不去处理数据,等到以后的某段时间才去处理数据,即分离接收数据和处理数据的时间点。好奇的我一定会问:条件触发没办法分离接收数据和处理数据的时间点吗?答案是可以的。但存在问题:在数据被处理之前,每次调用epoll_wait都会产生相应的事件,在一个具有大量这样的事件的繁忙服务器上,这是不现实的。
可是。还没有说边缘触发和条件触发哪个更好呀?马克思说,要辩证地看问题。so,边缘触发更有可能带来高性能,但不能简单地认为“只要使用边缘触发就一定能提高速度”,要具体问题具体分析。好吧,马克思的这一个“具体问题具体分析”适用于回答绝大部分比较类问题,已和“多喝水”,“重启一下试试看”,“不行就分”并列成为最简单粗暴的4个通用回答。
I/O多路复用——epoll函数的更多相关文章
- UNIX网络编程学习指南--epoll函数
epoll是select/poll的强化版,都是多路复用的函数,epoll有了很大的改进. epoll的功能 1.支持监听大数目的socket描述符 一个进程内,select能打开的fd是有限制的,有 ...
- epoll函数及三种I/O复用函数的对比
epoll函数 #include <sys/epoll.h>int epoll_create(int size)int epoll_ctl(int epfd, int op, int fd ...
- 详解网络编程必会的poll和epoll函数
前言 之前已经介绍过select函数,请参考这篇博客:https://www.cnblogs.com/liudw-0215/p/9661583.html,原理都是类似的,有时间先阅读下那篇博客,以便于 ...
- Linux-C网络编程之epoll函数
上文中说到假设从100的不同的地方取外卖,那么epoll相当于一部手机,当外卖到达后,送货员能够通知你.从而达到每去必得,少走非常多路. 它是怎样实现这些作用的呢? epoll的功能 epoll是se ...
- 非阻塞套接字编程, IO多路复用(epoll)
非阻塞套接字编程: server端 import socket server = socket.socket() server.setblocking(False) server.bind(('', ...
- (转)浅析epoll – epoll函数深入讲解
原文地址:http://www.cppfans.org/1418.html 浅析epoll – epoll函数深入讲解 前一篇大致讲了一下epoll是个什么东西,优点等内容,这篇延续上一篇的内容,主要 ...
- I/O多路复用-EPOLL探索
什么是I/O多路复用 I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个IO能够读写,通知程序进行相应的读写操作. I/O多路复用的场合 1.当客户处理多个描述字时(通常是交互式输入和网络套 ...
- epoll函数与参数总结学习 & errno的线程安全
select/poll被监视的文件描述符数目非常大时要O(n)效率很低:epoll与旧的 select 和 poll 系统调用完成操作所需 O(n) 不同, epoll能在O(1)时间内完成操作,所以 ...
- UNIX环境高级编程——epoll函数使用详解
epoll - I/O event notification facility 在linux的网络编程中,很长的时间都在使用select来做事件触发.在linux新的内核中,有了一种替换它的机制,就是 ...
随机推荐
- 自己动手模拟百分百<select>下拉列表
浏览器默认的下拉确实不好用啊,主要是样式不好修改和统一. (一)下手之前先理清一下<select>的流程: 1.结构:<select> <option value=&qu ...
- 超好玩!10款神奇的字符图案 & 词汇云生成工具
在这里,我们推荐10款惊人的字符图案生成工具.词云可以定义为词频的图形表示,而字符图案发生器是一个把数据,如文字和标签在以视觉和吸引人的方式展示的简单的工具.这些生成工具具有不同的功能,其中包括不同的 ...
- FUNCS.H中的函数声明
/*************************************************************************** ...
- Java魔法堂:注释和注释模板
一.注释 1. 注释类型 [a]. 单行注释 // 单行注释 String type = "单行注释"; [b]. 多行注释 /* * 多行注释 */ String type ...
- QCustomplot使用分享(五) 布局
一.历史对比 关于QCPLayoutElement这个元素的讲解之前,我想先对1.3.2release版本和2.0.0beta版本的该元素做以简单的对比介绍,首先,1.3.2release版本时,鼠标 ...
- C语言编译过程详解
前言 C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程. 编写hello world C程序: // hello.c #include ...
- Sprint第三个冲刺(第二天)
一.Sprint介绍 任务进度: 二.Sprint周期 看板: 燃尽图:
- 0406.复利计算器5.0版-release
复利计算器5.0-release 目录 项目简介 Github链接推送 客户需求 新增需求分析 项目设计 效果演示 操作说明 程序结构 结对分工 合作照片 总结 1.项目简介 项目名称:复利计算器 目 ...
- Github教程(3)
Pull Request Pull Request 是自己修改源代码后,请求对方仓库采纳该修改时采取的一种行为. 场景1: 用户A在fork完用户B的项目时,A修改了代码并提交了一个Pull Requ ...
- P6 EPPM手动安装指南(Oracle数据库)(一)
P6 EPPM手动安装指南(Oracle数据库) P6 EPPM Manual Installation Guide (Oracle Database) 1. 内容... 1 1.1. ...