epoll水平触发(Level Triggered)和边缘触发(Edge Triggered)详解

在Linux中,epoll 是一种高效的I/O多路复用机制,支持两种工作模式:LT(Level Triggered,水平触发)和ET(Edge Triggered,边缘触发)。

概念和原理

LT 模式(Level Triggered)

LT 模式是 epoll 的默认模式。在 LT 模式下,当某个文件描述符就绪时,epoll_wait 函数会立即返回,通知应用程序有事件发生。即使应用程序没有立即处理完这些事件,下次调用 epoll_wait 时仍会再次返回这些就绪的文件描述符。

ET 模式(Edge Triggered)

ET 模式要求应用程序在处理文件描述符的就绪事件时,必须确保将其处理完毕,否则 epoll_wait 将不会重复通知该文件描述符的就绪状态。ET 模式通过设置 epoll_event 结构体中的 EPOLLET 标志来启用。

区别

  1. 通知机制

    • LT 模式:每当文件描述符就绪时,epoll_wait 将通知应用程序,即使应用程序没有处理完该事件。
    • ET 模式:只有在文件描述符状态发生变化时,epoll_wait 才会通知应用程序。如果应用程序没有处理完事件,将不会重复通知该文件描述符的就绪状态。
  2. 效率

    • LT 模式:由于每次文件描述符就绪时都会通知应用程序,因此可能会引起频繁的上下文切换,影响效率。
    • ET 模式:只在状态变化时通知应用程序,可以减少不必要的上下文切换,提高效率,特别适合处理大量事件和高并发的场景。

适用场景

  • LT 模式适用场景

    • 对实时性要求不是非常高的应用,例如普通的网络服务器或者需要周期性处理数据的情况。
    • 适合处理一般的数据读取、写入等操作。
  • ET 模式适用场景

    • 对事件响应速度要求较高的应用,例如高性能网络服务器,需要快速处理大量连接或数据的情况。
    • 适合处理大数据流、高并发请求等场景,可以减少因为频繁通知而引起的性能开销。

2.如何设置epoll的ET模式

epoll 中设置成 ET(Edge Triggered,边缘触发)模式,需要在使用 epoll_ctl 函数添加或修改事件时,设置 struct epoll_event 结构体中的 EPOLLET 标志位。

2.1 创建 epoll 实例

int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
    perror("epoll_create1");
    exit(EXIT_FAILURE);
}

2.2 准备事件结构体

struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 使用 ET 模式
event.data.fd = sockfd; // sockfd 是需要监听的文件描述符

2.3 添加或修改事件

if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
    perror("epoll_ctl");
    exit(EXIT_FAILURE);
}

非阻塞模式

  • 使用 ET 模式通常需要将套接字设置为非阻塞模式,以充分发挥 ET 模式的优势。
  • 可以通过 fcntl 函数设置套接字的非阻塞属性,如:
int flags = fcntl(sockfd, F_GETFL, 0);
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
    perror("fcntl");
    exit(EXIT_FAILURE);
}

3.两种模式的比较

示例代码:

epoll的ET模式下的回显服务器

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

// 设置文件描述符为非阻塞
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl F_GETFL 错误");
        exit(1);
    }
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl F_SETFL 错误");
        exit(1);
    }
}

// 服务器主函数
int main(int argc, const char* argv[])
{
    // 创建监听套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket 错误");
        exit(1);
    }
    //printf("监听套接字:%d\n", lfd);        //调试信息
    // 设置监听套接字为非阻塞
    set_nonblocking(lfd);  // 注释掉这行,测试阻塞点

    // 绑定服务器地址和端口
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(9999);  // 监听端口9999
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 绑定所有网络接口的IP地址
    
    // 设置端口复用
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 将套接字绑定到指定地址
    int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    if(ret == -1)
    {
        perror("绑定错误");
        exit(1);
    }

    // 开始监听连接请求
    ret = listen(lfd, 64);
    if(ret == -1)
    {
        perror("监听错误");
        exit(1);
    }

    // 创建一个 epoll 实例
    int epfd = epoll_create(100);
    if(epfd == -1)
    {
        perror("epoll_create 错误");
        exit(1);
    }

    // 将监听套接字 lfd 加入 epoll 实例,监听读事件,使用ET模式
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;    // 监听读事件,ET模式
    ev.data.fd = lfd;       // 数据是监听套接字 lfd
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    if(ret == -1)
    {
        perror("epoll_ctl 错误");
        exit(1);
    }

    // 用于存放触发事件的数组
    struct epoll_event evs[1024];
    int size = sizeof(evs) / sizeof(struct epoll_event);
    
    int count = 0;      //调试信息,测试外循环触发次数

    printf("开始监听客户端...\n");      //调试信息
    // 进入事件处理循环
    while(1)
    {
        count++;
        printf("外循环次数:%d\n", count);      //调试信息
        // 等待事件触发
        int num = epoll_wait(epfd, evs, size, -1);
        printf("num: %d\n", num);
        if(num == -1)
        {
            perror("epoll_wait 错误");
            exit(1);
        }

        // 处理所有触发的事件
        for(int i = 0; i < num; ++i)
        {
            printf("处理触发事件。。。\n");         //调试信息
            int curfd = evs[i].data.fd;  // 获取当前事件对应的文件描述符

            // 如果是监听套接字 lfd 有事件发生,表示有新连接
            if(curfd == lfd)
            {
                printf("正在处理新连接...\n"); // 调试输出
                // 接受所有新连接
                while (1) {
                    printf("进入连接循环\n");         //调试信息
                    int cfd = accept(lfd, NULL, NULL);
                    if(cfd == -1)
                    {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            // 所有连接都已处理
                            break;
                        } else {
                            perror("accept 错误");
                            continue;
                        }
                    }

                    printf("新连接 %d 加入\n", cfd);

                    // 设置新连接为非阻塞
                    set_nonblocking(cfd);  // 注释掉这行,测试阻塞点

                    // 将新连接 cfd 添加到 epoll 实例中监听其读事件,使用ET模式
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = cfd;
                    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                    if(ret == -1)
                    {
                        perror("epoll_ctl-accept 错误");
                        exit(1);
                    }
                }
            }
            else
            {
                printf("正在处理读事件...\n"); // 调试输出
                // 处理已连接套接字的数据收发
                char buf[1024];
                int len;

                // 使用循环确保将缓冲区中所有数据读取完毕
                while ((len = recv(curfd, buf, sizeof(buf), 0)) > 0) {
                    printf("客户端 %d 说: %s", curfd, buf);
                    send(curfd, buf, len, 0);
                    memset(buf, 0, sizeof(buf));
                }

                if(len == -1 && (errno != EAGAIN && errno != EWOULDBLOCK))
                {
                    perror("recv 错误");
                    // 出错时关闭连接,并从 epoll 实例中删除
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
                else if(len == 0)
                {
                    // 客户端断开连接
                    printf("客户端 %d 已断开连接\n", curfd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
            }
        }
    }

    close(lfd);
    return 0;
}
  • 如果监听套接字lfd和通信套接字cfd,都设置成了ET模式,那么这两类套接字都需要设置成非阻塞模式,这样程序才不会在accept和recv处阻塞,这样服务端才可以同时处理多个客户端的连接和通信。相当与服务器与客户端一对多通信

客户端1连接,发送数据成功

客户端2连接,发送数据成功

服务端打印的信息:

  • 如果监听套接字lfd和通信套接字cfd,都设置成了ET模式,如果监听套接字lfd没有设置成非阻塞,通信套接字cfd设置成了非阻塞(没起作用因为阻塞在了accept处),程序会阻塞在accept处,服务端只能接收到客户端的连接无法收到客户端发送的数据,因为程序阻塞在了accept处,accept在一个循环里面。此时所有客户端都无法通信。

服务端:

客户端1:

客户端2:

  • 如果监听套接字lfd和通信套接字cfd,都设置成了ET模式,如果监听套接字lfd设置成非阻塞,通信套接字cfd没有设置成了非阻塞,那么第一个客户端可以连接和通信,但是程序阻塞在了recv处,后续客户端的连接和通信都会被阻塞。相当于是变成了服务器和先到的客户端一对一,其他的客户端只能排队在。

服务端:

客户端1:(可以连接通信)

客户端2:(被阻塞,因为客户端1的cfd未被设置成非阻塞,阻塞在了recv处)

服务端:(客户端1退出后,可以接收客户端2的连接和通信)

客户端2:

总结:

  • 如果监听套接字和通信套接字都被设置成了ET模式,那么程序就会在accept和recv处阻塞,因为ET模式下当有事件触发时只会通知一次,解决办法是把这两种套接字都是设置成非阻塞。这样就可以避免在这两处位置阻塞。
  • 一般情况下,监听套接字不需要设置成ET模式,只需要把通信套接字设置成ET模式即可,但是accept函数需要放到一个循环中。

思考题? epoll监控监听文件描述符可以设置成ET模式吗??

答案: 可以. 但是如果设置成ET模式以后, 当调用epoll_wait函数的时候, 每次只能accept一个连接(该连接在已连接队列当中, 而调用一次accept只能已连接队列中获取一个连接), 如果同时有多个连接到来, 就得epoll_wait再次返回之后才能继续accept下一个连接, 所以如果设置成了ET模式且有多个连接请求的话, 应该将accept写在循环当中, 一次epoll_wait后循环accept所有的连接.

所以一般不会在epoll中将监听的文件描述符设置为ET模式, 使用默认的LT模式即可; 而对通信的文件描述符一般采用非阻塞模式的ET模式.

示例代码:

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

#define MAX_EVENTS 1024
#define MAX_BUF_SIZE 1024

int main() {
    int lfd, cfd, epfd, nready, sockfd, n;
    struct sockaddr_in svraddr;
    struct epoll_event ev, events[MAX_EVENTS];
    char buf[MAX_BUF_SIZE];

    // 创建监听套接字
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

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

    // 绑定地址和端口
    memset(&svraddr, 0, sizeof(svraddr));
    svraddr.sin_family = AF_INET;
    svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    svraddr.sin_port = htons(8888);
    if (bind(lfd, (struct sockaddr *)&svraddr, sizeof(svraddr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 监听套接字
    if (listen(lfd, 128) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 创建 epoll 实例
    epfd = epoll_create(1024);
    if (epfd == -1) {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    }

    // 将监听套接字 lfd 添加到 epoll 实例中
    ev.events = EPOLLIN;  // LT 模式
    ev.data.fd = lfd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev) == -1) {
        perror("epoll_ctl");
        exit(EXIT_FAILURE);
    }

    printf("Server started. Waiting for connections...\n");

    while (1) {
        // 等待事件发生
        nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (nready == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < nready; ++i) {
            sockfd = events[i].data.fd;

            // 处理新的客户端连接
            if (sockfd == lfd) {
                cfd = accept(lfd, NULL, NULL);
                if (cfd == -1) {
                    perror("accept");
                    continue;
                }

                // 设置客户端套接字为非阻塞模式
                int flags = fcntl(cfd, F_GETFL, 0);
                fcntl(cfd, F_SETFL, flags | O_NONBLOCK);

                // 将客户端套接字 cfd 添加到 epoll 实例中,使用 ET 模式
                ev.events = EPOLLIN | EPOLLET;  // ET 模式
                ev.data.fd = cfd;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev) == -1) {
                    perror("epoll_ctl");
                    exit(EXIT_FAILURE);
                }

                printf("New connection established. Client connected on socket %d.\n", cfd);
            } else {
                // 处理客户端发送的数据
                while (1) {
                    n = read(sockfd, buf, MAX_BUF_SIZE);
                    if (n == -1) {
                        if (errno != EAGAIN && errno != EWOULDBLOCK) {
                            perror("read");
                            close(sockfd);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
                        }
                        break;
                    } else if (n == 0) {
                        printf("Client closed connection. Socket %d closed.\n", sockfd);
                        close(sockfd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
                        break;
                    } else {
                        // 处理接收到的数据
                        printf("Received from socket %d: %.*s", sockfd, n, buf);

                        // 将接收到的数据转换为大写
                        for (int j = 0; j < n; ++j) {
                            buf[j] = toupper(buf[j]);
                        }

                        // 发送转换后的数据给客户端
                        write(sockfd, buf, n);
                    }
                }
            }
        }
    }

    // 关闭监听套接字和 epoll 实例
    close(lfd);
    close(epfd);

    return 0;
}

  • 14
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值