epoll进阶--事件模型(LT/ET)

1 事件模型

EPOLL事件有两种模型:

  1. Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
  2. Level Triggered (LT) 水平触发只要有数据都会触发。(epoll默认为LT模式)


思考如下步骤:

  1. 假定我们已经把一个用来从管道中读取数据的文件描述符(rfd)添加到epoll描述符。
  2. 管道的另一端写入了2KB的数据
  3. 调用epoll_wait,并且它会返回rfd,说明它已经准备好读取操作
  4. 读取1KB的数据
  5. 调用epoll_wait……

在这个过程中,有两种工作模式:

ET模式

ET模式即Edge Triggered工作模式。

如果我们在第1步将rfd添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

  1. 基于非阻塞文件句柄
  2. 只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

LT模式

LT模式即Level Triggered工作模式。

与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。



2 ET和LT比较

LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

ET(edge-triggered):ET是高速工作方式,只支持no-block socket(需要忙轮询读)。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).



3 示例

3.1 基于管道epoll ET触发模式

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>

int main()
{
    int fd[2] = {0};    // 管道的读写两端

    int ret = pipe(fd); // 创建管道
    if (-1 == ret)
    {   
        perror("pipe error");
        exit(1);
    }

    pid_t pid = fork();
    if (-1 == pid)
    {   
        perror("fork error");
        exit(1);
    }
    else if (0 == pid)  // 子进程
    {   
        close(fd[0]);   // 关闭管道读端
        
        char ch = 'a';
        while (1)
        {   
            char buf[10] = {0};
            
            int ii = 0; 
            for(ii = 0; ii < 4; ++ii)
            {   
                buf[ii] = ch;
            } 
            buf[ii] = '\n';     // aaaa\n
            
            ch += 1;    // b
            
            for(ii = 5;ii < 9; ++ii)
            {   
                buf[ii] = ch;
            }
            buf[ii] = '\n';     // bbbb\n
            // buf  aaaa\nbbbb\n

            write(fd[1], buf, sizeof(buf));

            ch += 1;
            sleep(5);
        }
    }
    else    // 父进程
    {
        close(fd[1]);   // 关闭管道写端

        int epfd = epoll_create(1);             // 创建一颗红黑树
        if (-1 == epfd)
        {
            perror("epoll_create error");
            exit(1);
        }

        struct epoll_event tep{};
        struct epoll_event ep{};

        //tep.events = EPOLLIN;             // LT模式(水平触发)
        tep.events = EPOLLIN | EPOLLET;     // ET模式(边缘触发)
        tep.data.fd = fd[0];

        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd[0], &tep);  // 将fd[0]添加到红黑树上
        if (-1 == ret)
        {
            perror("epoll_ctl error");
            exit(1);
        }

        while (1)
        {
            int nReady = epoll_wait(epfd, &ep, 1, -1);          // 阻塞监听fd[0]的读事件
            if (-1 == nReady)
            {
                perror("epoll_wait error");
                exit(1);
            }

            char buf[10] = {0};
            int n = read(fd[0], buf, 5);
            if (-1 == n)
            {
                perror("read error");
                exit(1);
            }

            printf("%s", buf);
        }
    }

    return 0;
}


3.2 基于网络C/S模型的epoll ET触发模式

服务端

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

int main()
{
    int listenfd, connfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);  // 创建socket
    if (-1 == listenfd)
    {
        perror("socket error");
        exit(1);
    }

    // 端口复用
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in saddr, caddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port   = htons(8000);
    saddr.sin_addr.s_addr = inet_addr("192.168.71.132");

    int ret = bind(listenfd, (struct sockaddr*)&saddr, sizeof(saddr));  // 将socket与服务端的地址结构进行绑定
    if (-1 == ret)
    {
        perror("bind error");
        close(listenfd);
        exit(1);
    }

    ret = listen(listenfd, 5);  // 设置监听上限
    if (-1 == ret)
    {
        perror("listen error");
        exit(1);
    }

    int epfd = epoll_create(1024);  // 创建红黑树
    if (-1 == epfd)
    {
        perror("epoll_create error");
        exit(1);
    }

    struct epoll_event tep{};
    struct epoll_event ep[1024]{};

    tep.events = EPOLLIN;
    tep.data.fd = listenfd;

    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &tep);   // 将listenfd添加到红黑树上
    if (-1 == ret)
    {
        perror("epoll_ctl error");
        exit(1);
    }

    while (1)
    {
        int nReady = epoll_wait(epfd, ep, 1024, -1);        // 阻塞监听
        if (-1 == nReady)
        {
            perror("epoll_wait error");
            exit(1);
        }

        for (int ii = 0; ii < nReady; ++ii)
        {
            if (ep[ii].data.fd == listenfd)     // 有客户端连接上来
            {
                socklen_t caddrLen = sizeof(caddr);
                connfd = accept(listenfd, (struct sockaddr*)&caddr, &caddrLen);
                if (-1 == connfd)
                {
                    perror("accept error");
                    exit(1);
                }
                printf("建立连接成功:%s:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

                //tep.events = EPOLLIN;
                tep.events = EPOLLIN | EPOLLET;     // ET模式
                tep.data.fd = connfd;

                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &tep);   // 将connfd添加到红黑树上
                if (-1 == ret)
                {
                    perror("epoll_ctl error");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, listenfd, NULL);
                    close(connfd);  close(listenfd);
                    exit(1);
                }
            }
            else    // 有客户端发送数据
            {
                int sockfd = ep[ii].data.fd;

                char buf[10] = {0};
                int n = read(sockfd, buf, 5);
                if (-1 == n)
                {
                    perror("read error");
                    exit(1);
                }
                else if (0 == n)    // 如果客户端断开连接不会到此处 bug(因为客户端已经断开,没有条件再触发读事件)
                {
                    printf("断开连接\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
                    close(sockfd);
                }
                else
                {
                    printf("%s", buf);
                }
            }
        }
    }

    close(listenfd);
    return 0;
}


客户端

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <ctype.h>

int main()
{
    int cfd;
    // 1.创建socket
    cfd = socket(AF_INET, SOCK_STREAM, 0); 
    if (-1 == cfd)
    {   
        perror("socket error");
        exit(1);
    }   

    struct sockaddr_in saddr;

    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port   = htons(8000);
    inet_pton(AF_INET, "192.168.71.132", &saddr.sin_addr.s_addr);

    // 2.与服务端建立连接
    int ret = connect(cfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if (-1 == ret)
    {   
        perror("connect error");
        exit(1);
    }   

    char ch = 'a';
    while (1) 
    {   
        char buf[10] = {0};

        int ii = 0;
        for(ii = 0; ii < 4; ++ii)
        {   
            buf[ii] = ch; 
        }   
        buf[ii] = '\n';     // aaaa\n

        ch += 1;    // b

        for(ii = 5;ii < 9; ++ii)
        {
            buf[ii] = ch;
        }
        buf[ii] = '\n';     // bbbb\n
        // buf  aaaa\nbbbb\n

        write(cfd, buf, sizeof(buf));

        ch += 1;
        sleep(5);
    }


    // 4.关闭socket
    close(cfd);

    return 0;
}


3.3 基于网络C/S非阻塞模型的epoll ET触发模式 ★★

服务端

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

int main()
{
    int listenfd, connfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);  // 创建socket
    if (-1 == listenfd)
    {
        perror("socket error");
        exit(1);
    }

    // 端口复用
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in saddr, caddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port   = htons(8000);
    saddr.sin_addr.s_addr = inet_addr("192.168.71.132");

    int ret = bind(listenfd, (struct sockaddr*)&saddr, sizeof(saddr));  // 将socket与服务端的地址结构进行绑定
    if (-1 == ret)
    {
        perror("bind error");
        close(listenfd);
        exit(1);
    }

    ret = listen(listenfd, 5);  // 设置监听上限
    if (-1 == ret)
    {
        perror("listen error");
        exit(1);
    }
    int epfd = epoll_create(1024);  // 创建红黑树
    if (-1 == epfd)
    {
        perror("epoll_create error");
        exit(1);
    }

    struct epoll_event tep{};
    struct epoll_event ep[1024]{};

    tep.events = EPOLLIN;
    tep.data.fd = listenfd;

    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &tep);   // 将listenfd添加到红黑树上
    if (-1 == ret)
    {
        perror("epoll_ctl error");
        exit(1);
    }

    while (1)
    {
        int nReady = epoll_wait(epfd, ep, 1024, -1);        // 阻塞监听
        if (-1 == nReady)
        {
            perror("epoll_wait error");
            exit(1);
        }

        for (int ii = 0; ii < nReady; ++ii)
        {
            if (ep[ii].data.fd == listenfd)     // 有客户端连接上来
            {
                socklen_t caddrLen = sizeof(caddr);
                connfd = accept(listenfd, (struct sockaddr*)&caddr, &caddrLen);
                if (-1 == connfd)
                {
                    perror("accept error");
                    exit(1);
                }
                printf("建立连接成功:%s:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

                // 设置文件描述符非阻塞
                int flag = fcntl(connfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(connfd, F_SETFL, flag);


                //tep.events = EPOLLIN;
                tep.events = EPOLLIN | EPOLLET;     // ET模式
                tep.data.fd = connfd;

                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &tep);   // 将connfd添加到红黑树上
                if (-1 == ret)
                {
                    perror("epoll_ctl error");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, listenfd, NULL);
                    close(connfd);  close(listenfd);
                    exit(1);
                }
            }
            else    // 有客户端发送数据
            {
                int sockfd = ep[ii].data.fd;

                while (1) // 非阻塞,轮询读
                {
                    char buf[10] = {0};
                    int n = read(sockfd, buf, 5);
                    if (-1 == n)
                    {
                        if (errno == EAGAIN) // 非阻塞读没有数据
                            break;

                        perror("read error");
                        exit(1);
                    }
                    else if (0 == n)
                    {
                        printf("断开连接\n");
                        epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
                        close(sockfd);
                        break;
                    }
                    else
                    {
                        printf("%s", buf);
                    }
                }
            }
        }
    }

    close(listenfd);
    return 0;
}

客户端与示例3.2为同一个

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值