因为这道题目经常被问到。干脆总结一下,免得遗漏了。
参考文章: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
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 可用内存不足,无法完成请求。