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

本文深入探讨epoll的两种事件模型:边缘触发(ET)和水平触发(LT),并提供了基于管道和网络C/S模型的epoll ET触发模式的示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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为同一个

&lt;think>好的,用户想制作一个关于哔哩哔哩UP主C++网络编程教学视频的思维导图,内容涵盖Socket基础到Epoll。首先,我需要确定这个思维导图的主要模块子主题。 根据用户提供的引用内容,特别是引用[1]、[4]、[5],这些涉及到网络编程的IO模型、计算机基础课程以及异步回调机制。所以思维导图需要包含网络编程的基础知识、Socket编程的具体步骤、IO多路复用技术(如select/poll/epoll),以及高性能处理相关的异步机制。 首先,网络编程基础部分应该包括OSI模型、TCP/IP协议栈,特别是传输层的TCPUDP协议的区别应用场景。这部分可以引用[4]提到的计算机网络知识作为基础。 然后是Socket编程基础,需要涵盖创建Socket、绑定地址、监听连接、接受连接等步骤,以及TCPUDP的不同使用方式。这里可能需要结合实际的代码示例,比如socket()、bind()、listen()、accept()等函数的用法。 接下来是IO多路复用的部分,这是重点,特别是Epoll。根据引用[1],select/poll/epoll的区别原理需要详细说明,包括它们的优缺点适用场景。Epoll的水平触发边缘触发模式也是关键点,需要单独列出。 此外,引用[5]提到的异步回调机制可能与Epoll事件驱动模型相关,可以考虑将异步IO回调函数的设计融入高性能处理部分,说明如何通过Epoll实现非阻塞IO,提升服务器性能。 还需要考虑实际应用案例,比如实现一个简单的HTTP服务器或者聊天室程序,来展示Epoll的实际应用。同时,性能优化部分可以参考引用[3]中的高并发技术学习路线,分析Epoll在高并发场景下的优势。 最后,思维导图的结构需要层次分明,每个主要模块下再细分知识点,比如在Epoll部分包括工作原理、API函数、触发模式、与select/poll的对比等。同时,可以加入学习资源推荐,比如相关书籍(UNP、APUE)实战项目,帮助学习者巩固知识。 可能还需要注意用户提到的教学视频内容,确保思维导图覆盖视频中的重点,比如从基础到进阶,逐步深入。检查是否有遗漏的部分,比如错误处理、多线程与Epoll的结合等,这些也是实际开发中需要注意的。 总结下来,思维导图的大纲大致分为以下几个部分:网络编程基础、Socket编程详解、IO多路复用技术、Epoll深入解析、高性能编程技巧、实战案例与优化。每个部分下再展开具体的子主题,确保结构清晰,内容全面。&lt;/think># C++网络编程(SocketEpoll)教学视频思维导图大纲 ## 1. 网络编程基础 - **OSI模型与TCP/IP协议栈** - 传输层核心协议:TCP vs UDP - 三次握手与滑动窗口机制$^{[^4]}$ - **网络字节序与地址转换** - `htonl()/htons()`函数应用场景 ## 2. Socket编程核心 - **基础API操作流程** ```c++ // TCP服务端典型代码结构 int sockfd = socket(AF_INET, SOCK_STREAM, 0); bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(sockfd, 5); ``` - **TCP粘包问题解决方案** - 定长报文头设计 - 特殊结束符检测 ## 3. IO多路复用演进 - **select/poll局限性** - 时间复杂度$O(n)$遍历 - 1024文件描述符限制$^{[^1]}$ - **epoll突破性设计** ```c++ epoll_create1(0); epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); epoll_wait(epfd, events, MAX_EVENTS, -1); ``` - 红黑树存储描述符集合 - 事件回调机制避免轮询$^{[^5]}$ ## 4. Epoll高级特性 - **触发模式对比** - 水平触发(LT) vs 边缘触发(ET) - ET模式下的非阻塞读写最佳实践 - **惊群问题解决方案** - SO_REUSEPORT套接字选项 - EPOLLEXCLUSIVE标志 ## 5. 高性能架构设计 - **Reactor模式实现** - 事件分发核心流程 - 多线程epoll负载均衡 - **内存管理优化** - 对象池技术避免频繁分配 - 零拷贝技术应用场景 ## 6. 实战案例模块 - **百万连接压测方案** - 文件描述符上限调整 - TIME_WAIT状态优化 - **Web服务器实现** - HTTP协议解析模块 - 长连接keep-alive支持
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值