原文转载自:http://blog.csdn.net/drdairen/article/details/53896354
select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发
从表象看epoll的性能最好,但是在连接数少,并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多回调函数来完成。
epoll工作在两种触发模式下:
Level_triggered(水平触发): 这是epoll默认的触发方式,既支持阻塞模式,也支持非阻塞模式,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写
Edge_triggered(边缘触发): 这种模式下,epoll只支持非阻塞模式,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。Nginx默认采用ET模式来使用epoll。
二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只要监测描述符上有数据,epoll_wait就会返回该socket。
所以,在epoll的ET模式下,正确的读写方式为:
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
这里的错误码是指:
在一个非阻塞的socket上调用read/write函数, 返回的errno为EAGAIN或者EWOULDBLOCK
这个错误表示资源暂时不够,read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种情况,如果是阻塞socket,read/write就要阻塞掉。而如果是非阻塞socket,read/write立即返回-1, 同时errno设置为EAGAIN。
简单代码示例如下:
读:
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0)
{
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror("read error");
}
写:
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0)
{
nwrite = write(fd, buf + data_size - n, n);
if (nwrite < n) {
if (nwrite == -1 && errno != EAGAIN) {
perror("write error");
}
break;
}
n -= nwrite;
}
从以上我们就看出来了为何 epoll在ET模式下使用的是非阻塞fd ,由于边缘触发的模式,每次epoll_wait返回就绪的fd,必须读完读取缓冲区里的所有数据(直至接收数据返回EAGAIN),必须套上while循环,此时若使用阻塞的fd,当读取完缓冲区里的数据后,接受数据过程会阻塞,从而无法接受新的连接。
**//////////////////////割割割割///////////////////////////////割割割割////////////////////////**
上面介绍完了epoll的两种触发模式以及两种阻塞方式的使用,下面分析一下网络编程中accept函数使用fd时要如何选择触发方式以及阻塞方式。
listenfd阻塞还是非阻塞?
如果TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。
解决办法是把监听套接口listenfd设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。
ET还是LT?
ET:如果多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。
解决办法是用while循环包住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。
LT:在nigix的实现中,accept函数调用使用水平触发的fd,就是出于对丢失连接的考虑(边缘触发时,accept只会执行一次接收一个连接,内核不会再去通知有连接就绪),所以使用水平触发的fd就不存在丢失连接的问题。但是如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
所以服务器应该使用非阻塞的accept,并且在使用ET模式代码如下:
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0)
{
handle_client(conn_sock);
}
if (conn_sock == -1)
{
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
perror("accept");
}
一道腾讯后台开发的面试题:
使用Linuxepoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理?
解答:正如我们上面说的,LT模式下不需要读写的文件描述符仍会不停地返回就绪,这样就会影响我们监测需要关心的文件描述符的效率。
所以这题的解决方法就是:平时不要把该描述符放进eventpoll结构体中,当需要写该fd的时候,调用epoll_ctl把fd加入eventpoll里监听,可写的时候就往里写,写完再次调用epoll_ctl把fd移出eventpoll,这种方法在发送很少数据的时候仍要执行两次epoll_ctl操作,有一定的操作代价
改进一下就是:平时不要把该描述符放进eventpoll结构体中,需要写的时候调用write或者send写数据,如果返回值是EAGAIN(写缓冲区满了),那么这时候才执行第一种方法的步骤。
归纳如下:
1.对于监听的sockfd要设置成非阻塞类型,触发模式最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,网上有的方案是用while来循环accept()。
2.对于读写的connfd,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞。
3.对于读写的connfd,边缘触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据。、
下面看一个在边沿触发模式下使用epoll的http服务器代码,必要的讲解都在注释里。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_EVENTS 10
#define PORT 8080
//设置socket连接为非阻塞模式
void setnonblocking(int sockfd)
{
int opts;
opts = fcntl(sockfd, F_GETFL);
if(opts < 0)
{
perror("fcntl(F_GETFL)\n");
exit(1);
}
opts = (opts | O_NONBLOCK);
if(fcntl(sockfd, F_SETFL, opts) < 0)
{
perror("fcntl(F_SETFL)\n");
exit(1);
}
}
int main()
{
struct epoll_event ev, events[MAX_EVENTS];
int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;
struct sockaddr_in local, remote;
char buf[BUFSIZ];
//创建listen socket
{
perror("sockfd\n");
exit(1);
}
setnonblocking(listenfd);
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);;
local.sin_port = htons(PORT);
if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0)
{
perror("bind\n");
exit(1);
}
listen(listenfd, 20);//设置为监听套接字
epfd = epoll_create(MAX_EVENTS);
{
perror("epoll_create");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listenfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1)
{
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;)
{
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);//超时时间-1,永久阻塞直到有事件发生
if (nfds == -1)
{
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
for (i = 0; i < nfds; ++i)
{
fd = events[i].data.fd;
if (fd == listenfd) //如果是监听的listenfd,那就是连接来了,保存来的所有连接
{
//每次处理一个连接,while循环直到处理完所有的连接
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
(size_t *)&addrlen)) > 0)
{
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;//边沿触发非阻塞模式
ev.data.fd = conn_sock;
//把连接socket加入监听结构体
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: add");
exit(EXIT_FAILURE);
}
}
//已经处理完所有的连:accept返回-1,errno为EAGAIN
//出错:返回-1,errno另有其值
if (conn_sock == -1)
{
if (errno != EAGAIN && errno != ECONNABORTED
&& errno != EPROTO && errno != EINTR)
perror("accept");
}
continue;//直接开始下一次循环,也就是不执行这次循环后面的部分了
}
if (events[i].events & EPOLLIN) //可读事件
{
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0)
{
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror("read error");
}
ev.data.fd = fd;
ev.events = events[i].events | EPOLLOUT;
//修改该fd监听事件类型,监测是否可写
if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1)
{
perror("epoll_ctl: mod");
}
}
if (events[i].events & EPOLLOUT) //可写事件
{
sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0)
{
nwrite = write(fd, buf + data_size - n, n);
if (nwrite < n)
{
if (nwrite == -1 && errno != EAGAIN)
{
perror("write error");
}
break;
}
n -= nwrite;
}
//写完就关闭该连接socket
close(fd);
}
}
}
return 0;
}