因为这道题目经常被问到。干脆总结一下,免得遗漏了。

参考文章:http://www.cnblogs.com/qiaoconglovelife/p/5735936.html

1 本质上都是同步I/O

  三者都是I/O复用,本质上都属于同步I/O。因为三者只是负责通知应用程序什么时候数据准备好了,实际的I/O操作还是在由应用程序处理;如果是异步I/O的话,实际I/O由内核处理,然后再通知应用程序。这一点要搞清楚。

(注:其实也要看同步/异步的定义)

2 epoll 相比select、poll 的缺点:

(1)Linux系统独有:epoll函数并不是Unix系统通用,所以不适合开发兼容性强的程序;

(2)select、poll都只有一个函数,而epoll有三个(epoll_create,epoll_ctl和epoll_wait),操作起来更复杂,并且由于要实现回调机制,epoll的内部实现也更加复杂。如果并发量小且连接不频繁的话,最好使用select和poll,性能可能更好。

3 epoll相比select、poll的优点  

  (1)每次调用select、poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大,而epoll函数只有使用epoll_ctl函数时才会进行fd的拷备,并且只拷备增加的fd;

  (2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大,而epoll函数只传递所有新注册事件的fd;

  (3)select支持的文件描述符数量太小了,默认是1024,而epoll函数所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048。

epoll函数介绍:http://www.cnblogs.com/qiaoconglovelife/p/5503473.html

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 != 2)
{
printf("Usage: echo <port>\n");
exit(1);
} listenfd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, 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[1])); if (bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error\n");
if (listen(serv_addr, 5) == -1)
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, -1);
if (event_cnt == -1)
error_handling("epoll_wait() error\n");
for (int i = 0; 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 == 0)
{
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 0;
} void error_handling(char* buf)
{
printf("%s\n", buf);
exit(1);
}

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 != 2)
{
printf("Usage: echo <port>\n");
exit(1);
} listenfd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, 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[1])); if (bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error\n");
if (listen(serv_addr, 5) == -1)
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, -1);
if (event_cnt == -1)
error_handling("epoll_wait() error\n");
printf("event_cnt() return\n"); //指示一次返回
for (int i = 0; i < event_cnt; ++i)
{
if (ep_events[i].data.fd == listenfd)
{
connfd = accept(listenfd, NULL, NULL);
//设置为非阻塞I/O
int flag = fcntl(fd, F_GETFL, 0);
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 (1)
{
int nread = read(ep_events[i].data.fd, buf, BUF_SIZE);
if (nread == 0)
{
close(ep_events.data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
printf("disconnect with a client\n");
}
else if (nread < 0)
{
//errno为EAGAIN,则缓冲区内已没有数据
if (errno == EAGAIN)
break;
}
else
{
write(ep_events[i].data.fd, buf, nread);
}
} }
}
}
close(listenfd);
close(epfd);
return 0;
} void error_handling(char* buf)
{
printf("%s\n", buf);
exit(1);
}

几个说明:

  (1)在使用epoll_ctl注册事件的时候,选择边缘触发,|EPOLLET

  (2)处理已发生的边缘触发的事件时,要处理完所有的数据再返回。例中,使用了循环的方式读取了套接字中的所有数据

  (3)读/写套接字的时候采用非阻塞式I/O。为何?边缘触发方式下,以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿。(我理解,是因为要把所有的数据都处理完)

那么边缘触发好不好?有什么优点呢?书上说,边缘触发可以分离接收数据和处理数据的时间点。也就是说,在事件发生的时候,我们只记录事件已经发生,而不去处理数据,等到以后的某段时间才去处理数据,即分离接收数据和处理数据的时间点。(注:意思是处理一次,epoll_wait就不会再触发,可以记下来后续再处理)

好奇的我一定会问:条件触发没办法分离接收数据和处理数据的时间点吗?答案是可以的。但存在问题:在数据被处理之前,每次调用epoll_wait都会产生相应的事件,在一个具有大量这样的事件的繁忙服务器上,这是不现实的。

可是。还没有说边缘触发和条件触发哪个更好呀?马克思说,要辩证地看问题。so,边缘触发更有可能带来高性能,但不能简单地认为“只要使用边缘触发就一定能提高速度”,要具体问题具体分析。好吧,马克思的这一个“具体问题具体分析”适用于回答绝大部分比较类问题,已和“多喝水”,“重启一下试试看”,“不行就分”并列成为最简单粗暴的4个通用回答。棒。

select、poll函数介绍:http://www.cnblogs.com/qiaoconglovelife/p/5488871.html

select函数与poll函数

1 区别

  同:(1)机制类似,本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理。

(2)包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

  异:poll没有最大文件描述符数量的限制。

2 select函数原型

  该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:

#include <sys/select.h>
#include <sys/time.h> int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
           返回值:就绪描述符的数目,超时返回0,出错返回-1

函数参数介绍如下:

(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。

因为文件描述符是从0开始的。

(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

          void FD_ZERO(fd_set *fdset);           //清空集合

          void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中

          void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除

          int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 

(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

         struct timeval{

                   long tv_sec;   //seconds

                   long tv_usec;  //microseconds

       };

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。

(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

select函数的使用场景,和流程。

2 poll函数原型

函数原型如下:

# include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
        成功时,返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1

pollfd结构体定义如下:

struct pollfd {

int fd;         /* 文件描述符 */
short events;         /* 等待的事件 */
short revents;       /* 实际发生了的事件 */
} ;

每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。

每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
events域中请求的任何事件都可能在revents域中返回。

合法的事件如下:

   POLLIN         有数据可读。

  POLLRDNORM       有普通数据可读。

  POLLRDBAND      有优先数据可读。

  POLLPRI         有紧迫数据可读。

  POLLOUT            写数据不会导致阻塞。

  POLLWRNORM       写普通数据不会导致阻塞。

  POLLWRBAND        写优先数据不会导致阻塞。

  POLLMSGSIGPOLL     消息可用。
此外,revents域中还可能返回下列事件:
  POLLER     指定的文件描述符发生错误。

  POLLHUP   指定的文件描述符挂起事件。

  POLLNVAL  指定的文件描述符非法。

这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。

使用poll()和select()不一样,你不需要显式地请求异常情况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT |POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。

例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。
如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。
如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。
这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。 timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。
timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;
timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

返回值和错误代码

成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
  EBADF   一个或多个结构体中指定的文件描述符无效。   EFAULTfds   指针指向的地址超出进程的地址空间。   EINTR     请求的事件之前产生一个信号,调用可以重新发起。   EINVALnfds  参数超出PLIMIT_NOFILE值。   ENOMEM   可用内存不足,无法完成请求。

【转载】epoll与select/poll的区别总结的更多相关文章

  1. Nginx之epoll和select poll

    epoll和 select poll 都是做I/O多路复用的. 区别在于: epoll较灵活,如果有一百万个链接状态同时保持,但是在某个时刻,只有几百个链接是活跃的.epoll的处理是通过epoll_ ...

  2. 知识联结梳理 : I/O多路复用、EPOLL(SELECT/POLL)、NIO、Event-driven、Reactor模式

    为了形成一个完整清晰的认识,将概念和关系梳理出来,把坑填平. I/O多路复用 I/O多路复用主要解决传统I/O单线程阻塞的问题.它通过单线程管理多个FD,当监听的FD有状态变化的时候的,调用回调函数, ...

  3. [转载] Linux下多路复用IO接口 epoll select poll 的区别

    原地址:http://bbs.linuxpk.com/thread-43628-1-1.html 废话不多说,一下是本人学习nginx 的时候总结的一些资料,比较乱,但看完后细细揣摩一下应该就弄明白区 ...

  4. Linux下多路复用IO接口epoll/select/poll的区别

    select比epoll效率差的原因:select是轮询,epoll是触发式的,所以效率高. Select: 1.Socket数量限制:该模式可操作的Socket数由FD_SETSIZE决定,内核默认 ...

  5. 我觉得epoll和select最大的区别

    最近在用epoll,网速资料很多,大家都说epoll和select的区别比较大,而且select要不停遍历所有的fd,效率要低,而且fd有限制. 但是我认为二者最大的区别在于 先看代码 while ( ...

  6. epoll 系列函数简介、与select、poll 的区别

    一.epoll 系列函数简介 #include <sys/epoll.h> int epoll_create(int size); int epoll_create1(int flags) ...

  7. select,poll.epoll区别于联系

    select,poll,epoll都是IO多路复用中的模型.再介绍他们特点时,先来看看多路复用的 模型. 同其他IO的不同的是,IO多路复用一次可以等多个文件描述符.大大提高了等待数据准备好的时间的效 ...

  8. Linux select/poll和epoll实现机制对比

    关于这个话题,网上已经介绍的比较多,这里只是以流程图形式做一个简单明了的对比,方便区分. 一.select/poll实现机制 特点: 1.select/poll每次都需要重复传递全部的监听fd进来,涉 ...

  9. select poll epoll Linux高并发网络编程模型

    0 发展历程 同步阻塞迭代模型-->多进程并发模型-->多线程并发模型-->select-->poll-->epoll-->... 1 同步阻塞迭代模型 bind( ...

随机推荐

  1. VBA Promming——入门教程

    VBA Visual Basic for Applications(VBA)是Visual Basic的一种宏语言,是微软开发出来在其桌面应用程序中执行通用的自动化(OLE)任务的编程语言.主要能用来 ...

  2. vuez init webStorm

    <!-- * @description text !--> <template> <div>#[[$END$]]#</div> </templat ...

  3. 把特征网络换成resnet-50

    从RFCN来看,Resnet-50和Resnet-101到最后一层卷积都是缩小到原来尺寸的16分之一,并且都用的7x7的格子去roi pooling. 看paper可以知道:resnet-50核心是由 ...

  4. 苹果面临起诉:App Store 涉嫌垄断吗?

    今日导读 现代社会,手机在手,啥也不愁.购物不用带钱包,网上支付可以搞定一切:旅游懒得背相机,修图 App 帮你打造最美的自己.看直播.学英语.玩游戏.听音乐,我们的生活已经充斥着各类 App.据悉, ...

  5. 暑假集训 || 树DP

    树上DP通常用到dfs https://www.cnblogs.com/mhpp/p/6628548.html POJ 2342 相邻两点不能同时被选 经典题 f[0][u]表示不选u的情况数,此时v ...

  6. treetable

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  7. 在window下搭建即时即用的hyperledger fabric 的环境

    有版本号的严格按要求,遇到不少坑 1)安装git  版本无要求 2)安装go  1.9   配置环境变量 3)安装Vagrant  1.9.4 4)安装VirtualBox  5.1.28 5)在go ...

  8. SQL Prompt 格式化SQL会自动插入分号的问题

    一.问题 安装新版SQL Prompt,格式化SQL都会自动在SQL末端插入分号 格式化前 格式化后 二.解决方法 选择SQL Prompt下的Options... 选择左侧的Format下Style ...

  9. MySQL中的事务日志

    一.事务日志的作用 事务日志在保证事务的特性的同时,提高事务的执行效率 二.事务日志的工作原理 使用事务日志时,存储引擎修改了表的数据时只需要修改其内存拷贝. 然后再将修改行为记录到持久在硬盘上的事务 ...

  10. qemu-img命令

    qemu-img是QEMU的磁盘管理工具,在qemu-kvm源码编译后就会默认编译好qemu-img这个二进制文件.qemu-img也是QEMU/KVM使用过程中一个比较重要的工具,本节对其用法和实践 ...