zoukankan      html  css  js  c++  java
  • epoll EPOLLL、EPOLLET模式与阻塞、非阻塞

    EPOLLLT,EPOLLET是epoll两种不同的模式,前面已经讲过他们的区别:触发的时机不一致。读取数据的方式因此也不一样,下面我们分别讨论。

    在EPOLLLT(水平触发)模式下,也就是默认的模式,epoll_wait返回可读事件,表明socket一定收到了数据,我们可以调用read函数来读取数据。如果指定读取的数据大于缓冲区数据,无论socket是阻塞还是非阻塞的,read不会阻塞,read返回读取的真实数据。在read之后再次调用read,如果socket是阻塞的,read将阻塞,再次收到数据read才返回。此时如果指定读取的数据大于缓冲区,epoll_wait则不再触发,否则epoll_wait将再次触发,因为还有未读完的数据在缓冲区。

    在EPOLLET(电平触发)模式下,只有新的数据来到时才会触发,因此在这种情况下,有数据时必须循环读取数据直到read返回-1,并且错误码为EAGAIN,才算读取了全部的缓冲区数据。

    我突然想到一个问题,就是使用epoll时一定要将socket设置为非阻塞吗?正好知乎上有关于和这个的讨论:使用epoll时需要将socket设为非阻塞吗

    发现看来看去仍然得不到正解。俗话说的好,纸上得来终觉浅,要知此事须躬行。自己实现一遍,答案自然有了。以下是我验证后画的思维导图,很能够说明各种模式下sokcet的动作:

    上面的再次read指epoll触发后调用一次read后再调用一次,在具体的情况中可以看作while 循环读取数据。

    通过上面的图,我们可以得出结论:

    我觉得只有边沿触发才必须设置为非阻塞。

    边沿触发的问题:

    1. sockfd 的边缘触发,高并发时,如果没有一次处理全部请求,则会出现客户端连接不上的问题。不需要讨论 sockfd 是否阻塞,因为 epoll_wait() 返回的必定是已经就绪的连接,所以不管是阻塞还是非阻塞,accept() 都会立即返回。

    2. 阻塞 connfd 的边缘触发,如果不一次性读取一个事件上的数据,会干扰下一个事件,所以必须在读取数据的外部套一层循环,这样才能完整的处理数据。但是外层套循环之后会导致另外一个问题:处理完数据之后,程序会一直卡在 recv() 函数上,因为是阻塞 IO,如果没数据可读,它会一直等在那里,直到有数据可读。但是这个时候,如果用另一个客户端去连接服务器,服务器就不能受理这个新的客户端了。

    3. 非阻塞 connfd 的边缘触发,和阻塞版本一样,必须在读取数据的外部套一层循环,这样才能完整的处理数据。因为非阻塞 IO 如果没有数据可读时,会立即返回,并设置 errno。这里我们根据 EAGAIN 和 EWOULDBLOCK 来判断数据是否全部读取完毕了,如果读取完毕,就会正常退出循环了。

    总结一下:

    1. 对于监听的 sockfd,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,可以用 while 来循环 accept()。

    2. 对于读写的 connfd,水平触发模式下,阻塞和非阻塞效果都一样,因为在阻塞模式下,如果数据读取不完全则返回继续触发,反之读取完则返回继续等待。全建议设置非阻塞。

    3. 对于读写的 connfd,边缘触发模式下,必须使用非阻塞 IO,并要求一次性地完整读写全部数据。

    附上代码:

    #include <sys/socket.h>
    #include <sys/epoll.h>
    #include <arpa/inet.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdio.h>
     
     
    #define MAX_LINE     10
    #define MAX_EVENTS   500
    #define MAX_LISTENFD 5
     
    int createAndListen() {
        int on = 1;
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);
        struct sockaddr_in servaddr;
        fcntl(listenfd, F_SETFL, O_NONBLOCK);
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(5859);
     
        if (-1 == bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)))  {
            printf("bind errno, errno : %d 
    ", errno); 
        }
     
        if (-1 == listen(listenfd, MAX_LISTENFD))  {
            printf("listen error, errno : %d 
    ", errno); 
        }
        printf("listen in port 5859 !!!
    ");
        return listenfd;
    }
     
     
    int main(int argc, char const *argv[])
    {
        struct epoll_event ev, events[MAX_EVENTS];
        int epollfd = epoll_create(1);     //这个参数已经被忽略,但是仍然要大于
        if (epollfd < 0)  {
            printf("epoll_create errno, errno : %d
    ", errno);
        }
        int listenfd = createAndListen();
        ev.data.fd = listenfd;
        ev.events = EPOLLIN;
        epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
     
        for ( ;; )  {
            int fds = epoll_wait(epollfd, events, MAX_EVENTS, -1);   //时间参数为0表示立即返回,为-1表示无限等待
            if (fds == -1)  {
                printf("epoll_wait error, errno : %d 
    ", errno);
                break;
            }
            else {
                printf("trig %d !!!
    ", fds);
            }
     
            for (int i = 0; i < fds; i++)  {
                if (events[i].data.fd == listenfd)  {
                    struct sockaddr_in cliaddr;
                    socklen_t clilen = sizeof(struct sockaddr_in);
                    int connfd = accept(listenfd, (sockaddr*)&cliaddr, (socklen_t*)&clilen);
                    if (connfd > 0)  {
                        printf("new connection from %s : %d, accept socket fd: %d 
    ", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), connfd);
                    }
                    else  {
                        printf("accept error, connfd : %d, errno : %d 
    ", connfd, errno);
                    }
                    fcntl(connfd, F_SETFL, O_NONBLOCK); 
                    ev.data.fd = connfd;
                    ev.events = EPOLLIN | EPOLLET;
                    if (-1 == epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev))  {
                        printf("epoll_ctl error, errno : %d 
    ", errno);
                    }
                }
                else if (events[i].events & EPOLLIN)  {
                    int sockfd;
                    if ((sockfd =events[i].data.fd) < 0)  {
                        printf("EPOLLIN socket fd < 0 error 
    ");
                        continue;
                    }
                    char szLine[MAX_LINE + 1] ;
                    int readLen = 0;
                    bzero(szLine, MAX_LINE + 1);
                    if ((readLen = read(sockfd, szLine, MAX_LINE)) < 0)  {
                        printf("readLen is %d, errno is %d 
    ", readLen, errno);
                        if (errno == ECONNRESET)  {
                            printf("ECONNRESET closed socket fd : %d 
    ", events[i].data.fd);
                            close(sockfd);
                        }
                    }
                    else if (readLen == 0)  {
                        printf("read 0 closed socket fd : %d 
    ", events[i].data.fd);
                        //epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd , NULL);  
                        //close(sockfd);
                    }
                    else  {
                        printf("read %d content is %s 
    ", readLen, szLine);
                    }
     
                    bzero(szLine, MAX_LINE + 1);
                    if ((readLen = read(sockfd, szLine, MAX_LINE)) < 0)  {
                        printf("readLen2 is %d, errno is %d , ECONNRESET is %d 
    ", readLen, errno, ECONNRESET);
                        if (errno == ECONNRESET)  {
                            printf("ECONNRESET2 closed socket fd : %d 
    ", events[i].data.fd);
                            close(sockfd);
                        }
                    }
                    else if (readLen == 0)  {
                        printf("read2 0 closed socket fd : %d 
    ", events[i].data.fd);
                    }
                    else  {
                        printf("read2 %d content is %s 
    ", readLen, szLine);
                    }
                }
            }
     
        }
        return 0;
    }

    再补充一个关于EPOLLONESHOT的选项的问题,该选项是指epoll触发一次之后再也不会触发,即使水平模式下没有完全读取缓冲区的数据,再也不会有触发,更别提电平模式下了。

    在水平模式下添加了写事件,只要写缓冲还有空间,那么会一直触发。一般来说写缓冲区不会满,所以导致连接的socket一直触发写事件,这点会不会有损效率?因为我看redis源码中,连接的socket一直触发了写事件,虽然写的回调函数会判断没有要写的数据,但是这仍然会空转cpu。这是我学习redis源码想到的一个问题,不知大家怎么看。

    在recv读取数据时,指定要读取的大小大于缓冲区数据大小时,即使是阻塞socket也会返回。如果想要读取指定大小数据才让返回,recv函数必须加一个标志MSG_WAITALL。

    还有就是epoll_wait函数的阻塞与在其队列中socket是否阻塞没有关系,即添加在epoll中的socket全为阻塞,epoll_wait也不会阻塞,除非有事件触发或timeout。

  • 相关阅读:
    HDU4857 逃生 拓扑排序
    HDU1285 确定名次 拓扑排序
    【noip模拟赛4】找啊找啊找BF 拓扑排序
    拓扑排序基础
    【noip模拟赛5】任务分配 降维dp
    【noip模拟赛6】收入计划 最大值的最小值 二分答案
    【noip模拟赛5】水流
    标记预期失败
    跳过:
    pytest配置文件:
  • 原文地址:https://www.cnblogs.com/lxykl/p/10112570.html
Copyright © 2011-2022 走看看