epoll的两种触发模式ET、LT

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了epoll的两种触发模式ET、LT相关的知识,希望对你有一定的参考价值。

参考技术A epoll的两种触发模式分别是ET(edge trigger)边缘触发和LT(level triggered)水平触发。
epoll的默认触发模式是LT,select、poll都是LT触发。

缓冲区只要有数据未读就会导致epoll_wait返回。
上次读数据未读完仍会导致epoll_wait返回。
水平触发模式下阻塞和非阻塞并没有什么区别,因为没有可读时间就绪的话epoll_wait不会返回。

缓冲区出现新未读数据才会导致epoll_wait返回。
上次读数据未读完不会导致epoll_wait返回。

边缘触发模式下事件就绪只会通知一次,为了保证数据成功被读取或写入,在非阻塞模式下,采用循环的方式进行读写,直到完成或出现异常时退出。

如果不采用循环的方式进行读写,就会造成数据读/写不完的情况,因为下一次再调用epoll_wait就不会再通知了,所以职能采用循环的方式进行读写。但是如果尝试采用循环的方式进行读写,则会造成永久阻塞。
造成阻塞的原因只有没有数据可读/可写,在非阻塞模式下出现没有数据可读/可写可以返回相应的错误信息设置errno(EWOULDBLOCK),但是阻塞模式就会进入阻塞状态,而处理的该fd永远也不可能再有可读数据了,所以就被永久阻塞了。

epoll 服务端 ET模式

windows下IOCP, linux下 epoll。

epoll模型其实也是一个同步模型,ET是epoll里面的一种模式,叫 边缘触发。 个人理解,类似于 windows下的事件选择模型。代码如下:

 

#include <unistd.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <fcntl.h> 
#include <netinet/in.h>

#include <errno.h>

#define MAX_BACKLOG 256
#define MAX_EVENTS 1024


// 如EPOLL的作者Davide Libenzi所说,如果你对一fd同时注册EPOLLIN | EPOLLOUT事件,
// 即使发送缓冲区并非由满变空,也会触发EPOLLOUT事件
// 使用epoll的最好是用ATM模式,当真正需要用到EPOLLOUT时才注册。我理解的ATM模式就是读、写、读、写这样的循环。


// ET模式下的读写
// 只要可读, 就一直读, 直到返回 0, 或者 errno = EAGAIN
// 只要可写, 就一直写, 直到数据发送完, 或者 errno = EAGAIN

// 保证对非阻塞的套接字写够请求的字节数才返回
ssize_t socket_write(int sockfd, const char* buffer, size_t buflen)
{
    ssize_t tmp = 0;
    size_t total = buflen;
    const char* p = buffer;
    while (1)
    {
        tmp = write(sockfd, p, total);
        if(tmp < 0)
        {
            // 当send收到信号时,可以继续写,但这里返回-1.
            if(errno == EINTR)
            {
                return -1;
            }
            // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满,在这里做延时后再重试.
            if(errno == EAGAIN)
            {
                usleep(1000);
                continue;
            }

            return -1;
        }

        if((size_t)tmp == total)
        {
            return buflen;
        }

        total -= tmp;
        p += tmp;
    }    

    return tmp;     // 返回已写的字节数
}





// 设定socket句柄为非阻塞
static int SetNonBlock(int nFd)
{
    int nOldOpt = fcntl(nFd, F_GETFL, 0);
    int nNewOpt = nOldOpt | O_NONBLOCK;


    return fcntl(nFd, F_SETFL, nNewOpt);
}

// 为套接字追加事件
//static void AddEvent(int nEpfd, int nFd)
//{
//    struct epoll_event event;
//    event.data.fd = nFd;
//    //event.events = EPOLLIN | EPOLLOUT | EPOLLHUP | EPOLLET;
//    event.events = EPOLLIN | EPOLLOUT | EPOLLET;
//    epoll_ctl(nEpfd, EPOLL_CTL_ADD, nFd, &event);
//}



static void add_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state | EPOLLET;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

static void delete_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state | EPOLLET;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

static void modify_event(int epollfd,int fd,int state)
{
    struct epoll_event ev;
    ev.events = state | EPOLLET;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}


// 主函数
int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("usage: CMD Port\\n");
        exit(-1);
    }
    
    int nPort = atoi(argv[1]);
    if (nPort < 0)
    {
        printf("Port Invalid\\n");
        exit(-1);
    }

    int nSvrFd = socket(AF_INET, SOCK_STREAM, 0);
    // 设置非阻塞
    SetNonBlock(nSvrFd);

    // 绑定地址
    struct sockaddr_in addr;
    //bzero(&addr,sizeof(addr));
    memset(&addr,0x00, sizeof(addr));
    addr.sin_port = htons(nPort);
    inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr);
    addr.sin_family = (AF_INET);

    if (0 != bind(nSvrFd, (struct sockaddr*)&addr, sizeof(addr)))
    {
        perror("Bind Failure:");
        exit(-1);
    }
    
    //监听端口
    if (0 != listen(nSvrFd, MAX_BACKLOG))
    {
        perror("Listen Failure:");
        exit(-1);
    }
    // 创建epoll句柄
    int nEpfd = epoll_create(1024);

    struct epoll_event events[MAX_EVENTS];
//     AddEvent(nEpfd, nSvrFd);

    add_event(nEpfd,nSvrFd,EPOLLIN);

    while (1)
    {
        //等待事件到来
        int nReadyNums = epoll_wait(nEpfd, events, MAX_EVENTS, -1);
        int nClientFd = -1;

        struct sockaddr_in cliaddr;
        socklen_t  cliaddrlen = sizeof(cliaddr);

        for (int i = 0; i < nReadyNums; ++i)
        {
             if (events[i].data.fd == nSvrFd)
             {
                // accept放到线程中,可以使用while循环
                // 多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪
                // 连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理。
                // 解决办法是用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。
                 while ((nClientFd = accept(nSvrFd,(struct sockaddr*)&cliaddr,&cliaddrlen)) > 0)
                // if ((nClientFd = accept(nSvrFd,(struct sockaddr*)&cliaddr,&cliaddrlen)) > 0)
                {
                    printf("建立连接\\n");
                     //设置为非阻塞
                     SetNonBlock(nClientFd);
                     //添加事件监听
                     // AddEvent(nEpfd, nClientFd);
                     add_event(nEpfd,nClientFd,EPOLLIN);
                }

             }
            else 
            { 
                // 处理FD事件
                if (events[i].events & EPOLLIN)
                {
                     int n = 0;
                     int nread = 0;
                     char buf[BUFSIZ];
                     memset(buf, 0x00, sizeof(buf));
                     while ((nread = read(events[i].data.fd, buf + n, BUFSIZ-1)) > 0)
                     {
                      n += nread;
                     }
                     
                     if (nread == -1 && errno != EAGAIN)
                     {
                      perror("read error");
                     }
                      printf("read----%s\\n", buf);   

                    // 直接去写,不需要EPOLLOUT事件
                    const char* p = "epoll_server.cpp answer....\\n";
                    socket_write(events[i].data.fd, p, strlen(p) + 1 );
                    
                    // 后面代码无效,ET模式下,EPOLLOUT事件不响应或者出错,没搞通,实际代码中也不会用到,忽略。
                    continue;

                     // modify_event(nEpfd,nClientFd,EPOLLOUT);

                    //  continue;
                     // 强制触发一次
                    struct epoll_event ev;
                    int fd = events[i].data.fd;
                    ev.events = events[i].events | EPOLLOUT;
                    if(-1 == epoll_ctl(nEpfd, EPOLL_CTL_MOD, fd, &ev))
                    {
                        perror("epoll_ctl: mod");
                    }
                }
                // 可写
                if (events[i].events & EPOLLOUT)
                {
                    const char* p = "epoll_server.cpp answer....";
                    socket_write(events[i].data.fd, p, strlen(p) + 1 );
                    
                    //modify_event(nEpfd,nClientFd,EPOLLIN);
                   // 强制触发一次
                   // struct epoll_event ev;
                   // int fd = events[i].data.fd;
                   // ev.events = events[i].events | EPOLLIN;
                   // if(-1 == epoll_ctl(nEpfd, EPOLL_CTL_MOD, fd, &ev))
                   // {
                   //     perror("epoll_ctl: mod");
                   // }
                }
            }

           
        }
    }

    
}

使用telnet 命令模拟客户端进行测试,结果如下:

服务端:

 

客户端:

 

比较好的linux 网络编程文章 http://www.cnblogs.com/Anker/p/3265058.html

 

以上是关于epoll的两种触发模式ET、LT的主要内容,如果未能解决你的问题,请参考以下文章

epoll中et+多线程模式中很重要的EPOLL_ONESHOT实验

Epoll的使用详解

epoll 服务端 ET模式

高效IO——多路转接epoll

水平触发和边沿触发(IO复用)

为何 epoll 的 ET 模式一定要设置为非阻塞IO