Linux高性能服务器之I/O复用之实例 ET AND LT(图像理解)(14)

本文详细介绍了epoll的两种工作模式:LT(Level Triggered)和ET(Edge Triggered),并提供了实验代码对比这两种模式的区别。文章还强调了在ET模式下,文件描述符应该设置为非阻塞模式的重要性。

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

LT和ET模式

前言

epoll对文件描述符的操作有两种模式: LT (Level Trigger,电平触发〉模式和ET(EdgeTrigger,边沿触发〉模式。LT模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。

对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。

而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

(部分)实验代码之LT与ET差别

    #include<sys/socket.h>
    #include<sys/types.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    #include<assert.h>
    #include<stdio.h>
    #include<unistd.h>
    #include<stdlib.h>
    #include<fcntl.h>
    #include<string.h>
    #include<errno.h>
    #include<sys/epoll.h>
    #include<pthread.h>
    
    #define MAX_EVENT_NUMBER 10
    #define BUFFER_SIZE   1024
    /*
    parameter meaning
    fd        文件描述符
    function performance:
    将文件描述符设置成非阻塞
    function idea:
    从一个模板出去 可以不改变一个文件描述符的特性
    */
    int setnoblocking(int fd)
    {
        int old_option=fcntl(fd,F_GETFD);//得到属性
        int new_option=old_option|O_NONBLOCK;
        fcntl(fd,F_SETFD,new_option);
        return old_option;
    }

    /*
    parameter        meaning
    epollfd          epoll事件表唯一地址
    fd               文件描述符
    enable_et        是否启用ET模式
    function performance:
    将文件描述符fd上的EPOLLIN注册到epollfd上的epoll内核事件表上
    参数enable_et指定fd是否启用ET模式
    */ 
     
    void addfd(int epollfd,int fd,bool enable_et)
    {
        struct epoll_event event;
        event.data.fd=fd;//bind fd
        event.events=EPOLLIN;//普通数据可读
        if(enable_et)
        {
            event.events|=EPOLLET;
        }
        epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);//将fd上的事情注册到epollfd上
        setnoblocking(fd);
    }

 /*
    parameter   meaning
    events      注册表
    number      用户数据 fd的数量
    function proformance:
    LT工作模式
    */
    void lt(epoll_event* events,int number,int epollfd,int sockfd)
    {
        char buf[BUFFER_SIZE];
        for(int i=0;i<number;i++)
        {
            int fd=events[i].data.fd;
            if(fd==sockfd)
            {
                struct sockaddr_in client;
                socklen_t client_length=sizeof(client);
                int connfd=accept(sockfd,(struct sockaddr*)&client,&client_length);
                addfd(epollfd,sockfd,false);
    
            }
            else if(events[i].data.fd&EPOLLIN)
            {  
                printf("event trigger once\n");
                memset(buf,'\0',BUFFER_SIZE);
                int ret=recv(sockfd,buf,BUFFER_SIZE-1,0);
                if(ret<=0)
                {
                    close(sockfd);
                    continue;
                }
                 printf("get %d bytes pg content %s\n",ret,buf);
                
            }
            else 
            { printf("something else happened\n");

            }
        }
    }
    
    /*
    parameter   meaning
    events      注册表
    number      用户数据 fd的数量
    function proformance:
    ET工作模式
    */
    void et(epoll_event* events,int number,int epollfd,int sockfd)
    {
       char buf[BUFFER_SIZE];
       int i;
       for(i=0;i<number;++i)
       {
         int fd=events[i].data.fd;//触发
         if(fd==sockfd)
         {
           struct sockaddr_in client;
           socklen_t client_length=sizeof(client);
           int connfd=accept(fd,(struct sockaddr*)&client,&client_length);
           addfd(epollfd,connfd,true);
           
         }
         else if(events[i].events&EPOLLIN)
         {
            /*/*这段代码不会被重复触发,所以我们循环读取数据,
            以确保把socket读缓存中的所有数据读出*/
            printf("event trigger once\n");
            while(1)
            {
                memset(buf,'\0',BUFFER_SIZE);
                int ret=recv(sockfd,buf,BUFFER_SIZE-1,0);
                if(ret<0)
                {
                    /*/*对于非阻塞Io,下面的条件成立表示数据已经全部读取
                    完毕。此后,epol1就能再次触发sockfd 上的EPOLLIN事件,
                    以驱动下一次读操作*/
                    if((errno==EAGAIN)||(errno==EWOULDBLOCK))
                    {
                        printf("read later\n");
                        break;
                    }
                    close(fd);
                    break;
                    
                }
                else if(ret==0)
                {
                    close(fd);
                }
                else
                {
                    printf("get %d bytes pg content %s\n",ret,buf);
                }
            }

         }
         else
         {
            printf("something else happened\n");
         }
       }
    }


    int main(int argc,char* argv[])
    {
        if(argc<=2)
        {
            printf("Usage :%s ip_address port_number\n",basename(argv[0]));
            return 1;
        }

        char* ip=argv[1];
        int port=atoi(argv[2]);
         
         int ret=0;
         struct sockaddr_in address;
         bzero(&address,sizeof(address));
         address.sin_family=AF_INET;
         address.sin_port=htons(port);
         inet_pton(AF_INET,ip,&address.sin_addr);

         int sockfd=socket(PF_INET,SOCK_STREAM,0);
         assert(sockfd!=-1);

         ret=bind(sockfd,(struct sockaddr*)&address,sizeof(address));
         assert(ret!=-1);

         ret=listen(sockfd,5);
         assert(ret-1);

         struct epoll_event event[MAX_EVENT_NUMBER];
         int epollfd=epoll_create(5);
         assert(epollfd!=-1);
         addfd(epollfd,sockfd,true);
         
         while(1)
         {
            int ret=epoll_wait(epollfd,event,MAX_EVENT_NUMBER,-1);
            if(ret<0)
            {
                printf("epoll failure\n");
                break;
            }
            lt(event,ret,epollfd,sockfd);
            
         }
         close(sockfd);
         return 0;

        
    }

程序流图

                                                               LT工作流图                                                                                                              

           

  ET工作流图

答疑:

对于非阻塞Io,下面的条件成立表示数据已经全部读取 完毕。此后,epol1就能再次触发sockfd 上的EPOLLIN事件, 以驱动下一次读操作?

 当应用程序在socket中设置O_NONBLOCK属性后,如果发送缓存被占满,send就会返回EAGAIN或EWOULDBLOCK 的错误。在将socket设置O_NONBLOCK属性后,通过socket发送一个100K大小的数据,第一次成功发送了13140数据,之后继续发送并未成功,errno数值为EAGAIN错误。
 

切记:

注意︰每个使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态(饥渴状态)。
 

&lt;think>好的,我现在需要为用户介绍Linux下的epoll多路I/O复用模型。首先,用户可能已经了解过select和poll,所以需要对比说明epoll的优势。接下来,要解释epoll的三个主要函数:epoll_create、epoll_ctl、epoll_wait,以及它们的作用。 然后,需要详细说明epoll的工作模式,LT(水平触发)和ET(边缘触发)的区别,以及各自的适用场景。用户可能对这两种模式的实际应用不太清楚,所以需要举例子说明,比如在LT模式下,如果没有处理完数据,会反复通知,而ET模式则只在状态变化时通知一次,需要一次处理完所有数据。 还要提到epoll的高效性,比如使用红黑树管理文件描述符,以及就绪列表的结构,这样在处理大量连接时性能更好。同时,需要强调epoll适合高并发的场景,比如Web服务器。 可能用户对epoll的使用步骤不太清楚,需要分步骤说明:创建epoll实例、注册感兴趣的事件、等待事件发生并处理。每个步骤对应的函数和参数需要简要说明。 另外,用户可能会遇到常见问题,比如ET模式下的非阻塞IO为什么是必须的,需要解释ET模式下如果不一次读完数据,可能因为没有新事件到来而导致数据滞留。这时候需要提醒用户使用非阻塞socket,并在读取时循环读取直到EAGAIN错误。 还需要提到epoll的优点和缺点,比如优点是可扩展性好,适合大量连接,缺点是在连接数少时可能不如select和poll高效,或者实现相对复杂。 最后,给出一个简单的代码示例,帮助用户理解epoll的基本用法,比如创建epoll实例,添加监听socket,然后进入事件循环处理连接和读写事件。代码中要注意错误处理,以及ET模式的设置方法。 要确保语言通俗易懂,避免过多术语堆砌,结构清晰,分点说明。同时,检查是否有技术细节错误,比如epoll的数据结构确实是红黑树和就绪链表,函数参数是否正确等。还要注意编译时需要链接的库,比如是否需要-lrt或者其他选项。 可能还需要对比epoll与其他IO复用机制的差异,突出其优势,比如时间复杂度更低,不需要每次调用都传递全部fd集合。这样用户能更好地理解为什么epoll在大规模并发下更高效。 最后,提醒用户在实际开发中需要注意的事项,比如正确处理错误码,避免文件描述符泄漏,以及合理设置事件类型等。&lt;/think>### Linux epoll多路I/O复用模型详解 --- #### 一、核心概念对比 | 特性 | select | poll | **epoll** | |--------------------|-------------------------------------|------------------------------------|------------------------------------| | 时间复杂度 | $O(n)$ 线性扫描 | $O(n)$ 线性扫描 | $O(1)$ 事件驱动 | | 最大文件描述符数 | FD_SETSIZE(1024) | 无限制 | 系统级限制(百万级) | | 内存管理 | 每次传递完整fd_set | 动态数组 | 内核红黑树+就绪链表 | | 触发方式 | 水平触发(LT) | 水平触发(LT) | 支持LT/ET两种模式 | --- #### 二、epoll三大核心函数 1. **epoll_create** ```c int epoll_create(int size); // 创建epoll实例(size在Linux 2.6.8后已弃用) ``` - 返回:epoll文件描述符(需手动关闭) - 内核数据结构:红黑树(存储监控fd) + 就绪链表(存储活跃事件) 2. **epoll_ctl** ```c int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); ``` - 操作类型: - `EPOLL_CTL_ADD` 添加监控 - `EPOLL_CTL_MOD` 修改事件 - `EPOLL_CTL_DEL` 移除监控 - 事件结构: ```c struct epoll_event { uint32_t events; // EPOLLIN | EPOLLOUT | EPOLLET等 epoll_data_t data; // 用户数据(常存fd) }; ``` 3. **epoll_wait** ```c int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); ``` - 返回:就绪事件数量 - 参数: - `events`:预分配的接收缓冲区 - `timeout`:-1阻塞 / 0立即返回 / >0毫秒超时 --- #### 三、工作模式详解 1. **水平触发(LT)** - 特性:只要fd处于就绪状态,持续通知 - 行为模式: - 读:缓冲区有未读数据时反复触发 - 写:缓冲区可写入时反复触发 - 优点:编程简单,兼容select/poll行为 - 适用场景:传统服务器开发 2. **边缘触发(ET)** - 特性:仅在fd状态变化时通知一次 - 必须配合: - 非阻塞IO(`fcntl(fd, F_SETFL, O_NONBLOCK)`) - 循环读取直到`EAGAIN`错误 - 优点:减少系统调用次数,更高性能 - 适用场景:高频交易系统、高性能服务器 --- #### 四、完整代码示例 ```c #include &lt;sys/epoll.h> #include &lt;fcntl.h> #include &lt;unistd.h> #define MAX_EVENTS 10 int main() { int epoll_fd = epoll_create1(0); struct epoll_event ev, events[MAX_EVENTS]; // 添加监听socket到epoll(示例) int listen_sock = socket(...); fcntl(listen_sock, F_SETFL, O_NONBLOCK); // 非阻塞 ev.events = EPOLLIN | EPOLLET; // ET模式 ev.data.fd = listen_sock; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &ev); while(1) { int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for(int i=0; i&lt;n; i++) { if(events[i].data.fd == listen_sock) { // 处理新连接 while(1) { // 必须循环处理ET模式 int conn_fd = accept(listen_sock, ...); if(conn_fd == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) break; // 已无新连接 // 设置新连接为非阻塞并加入epoll监控 } } else { // 处理数据I/O(必须循环读取到EAGAIN) char buf[1024]; while(read(events[i].data.fd, buf, sizeof(buf)) > 0); } } } close(epoll_fd); } ``` --- #### 五、性能优化要点 1. **事件注册策略** - 读事件:`EPOLLIN | EPOLLRDHUP`(检测对端关闭) - 写事件:仅在需要时注册,避免频繁触发 2. **惊群问题** - 解决方案:Linux 3.9+ 使用`EPOLLEXCLUSIVE`标志 3. **内存管理** - 避免每次`epoll_wait`都动态分配事件数组 4. **超时设置** - 定时器整合:配合`timerfd`实现精确超时控制 --- #### 六、适用场景分析 | 场景 | 推荐模式 | 说明 | |---------------------|---------------|------------------------------| | HTTP服务器 | LT模式 | 简单可靠,兼容短连接 | | 实时消息推送 | ET模式 | 高频事件处理更高效 | | 文件传输服务 | ET+非阻塞IO | 大文件传输需高效处理 | | 金融交易系统 | ET+多epoll实例| 超低延迟要求,多核负载均衡 | --- #### 七、调试与监控 1. **状态查看** ```bash lsof -p [pid] | grep epoll # 查看epoll实例 cat /proc/[pid]/fdinfo/[epoll_fd] # 查看监控fd列表 ``` 2. **性能分析** ```bash perf record -e syscalls:sys_enter_epoll* # 跟踪epoll系统调用 strace -e epoll_ctl,epoll_wait ./program # 监控函数调用 ``` --- **选择建议**:在连接数大于1000的高并发场景优先选择epoll,传统场景可考虑select/poll。ET模式需要更严谨的代码设计,建议在性能关键路径使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值