网络编程:epoll、accept触发模式及阻塞方式的选择

原文转载自: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");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

写:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

从以上我们就看出来了为何 epoll在ET模式下使用的是非阻塞fd ,由于边缘触发的模式,每次epoll_wait返回就绪的fd,必须读完读取缓冲区里的所有数据(直至接收数据返回EAGAIN),必须套上while循环此时若使用阻塞的fd,当读取完缓冲区里的数据后,接受数据过程会阻塞,从而无法接受新的连接。

**//////////////////////割割割割///////////////////////////////割割割割////////////////////////**
  • 1

上面介绍完了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");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

一道腾讯后台开发的面试题: 
使用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;
}

你可能感兴趣的:(Linux下开发)