Linux网络编程---I/O复用模型之epoll

本文深入解析Linux下的epoll I/O复用模型,介绍其API、工作模式,并提供服务端及客户端代码实例,对比Level Triggered与Edge Triggered两种模式。

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

Linux网络编程—I/O复用模型之epoll

1. epoll模型简介

epoll是Linux多路服用IO接口select/poll的加强版,e对应的英文单词就是enhancement,中文翻译为增强,加强,提高,充实的意思。所以epoll模型会显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

  • epoll把用户关心的文件描述符上的时间放在内核的一个事件表中,无需像select和poll那样每次调用都重复传入文件描述符集。
  • epoll在获取事件的时候,无需遍历整个被监听的文件描述符集合,而是遍历那些被内核IO事件异步唤醒而加入ready队列的描述符集合。

所以,epoll是Linux大规模高并发网络程序的首选模型。

2.epoll模型的API

epoll使用一组函数来完成任务

2.1 函数epoll_create

创建一个epoll句柄,句柄的英文是handle,相通的意思是把手,把柄。

#include <sys/epoll.h>

int epoll_create(int size);
//返回值:若成功,返回一个非负的文件描述符,若出错,返回-1。
  • 该函数返回一个文件描述符,用来唯一标示内核中这个事件表,sizeof参数提示内核要监听的文件描述符个数,这与内存大小有关。
  • 返回的文件描述符将是其他所有epoll系统调用的第一个参数,以指定要访问的内核时间表,所以用该返回的文件描述符相当与其他epoll调用的把手、把柄一样。

查看进程能够打开的最大数目的文件描述符

➜  ~ cat /proc/sys/fs/file-max
1215126
//该值与内存大小有关

修改最大文件描述符限制

➜  ~ sudo vim /etc/security/limits.conf
//重启生效

2.2 函数epoll_ctl

该函数用来操作epoll的内核事件表

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//返回值:若成功,返回0,若出错返回-1。
  • epfd就是函数epoll_create创建的句柄。
  • op是指定操作类型,有一下三种
    • EPOLL_CTL_ADD,向epfd注册fd的上的event
    • EPOLL_CTL_MOD,修改fd已注册的event
    • EPOLL_CTL_DEL,从epfd上删除fd的event
      1. fd是操作的文件描述符
      2. event指定内核要监听事件,它是struct epoll_event结构类型的指针。epoll_event定义如下:
struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};
  • events成员描述事件类型,将以下宏定义通过位或方式组合

    • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
    • POLLOUT:表示对应的文件描述符可以写
    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
    • EPOLLERR:表示对应的文件描述符发生错误
    • EPOLLHUP:表示对应的文件描述符被挂断;
    • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
    • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  • data用于存储用户数据,是epoll_data_t结构类型,该结构定义如下:

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;
  • epoll_data_t是一个联合体,fd指定事件所从属的目标文件描述符。ptr可以用来指定fd相关的用户数据,但两者不能同时使用。

2.3 函数epoll_wait

函数epoll_wait用来等待所监听文件描述符上有事件发生

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//返回值:若成功,返回就绪的文件描述符个数,若出错,返回-1,时间超时返回0
  • epfd就是函数epoll_create创建的句柄
  • timeout是超时事件,-1为阻塞,0为立即返回,非阻塞,大于0是指定的微妙
  • events是一个 传入传出参数,它是epoll_event结构的指针,用来从内核得到事件的集合
  • maxevents告知内核events的大小,但不能大于epoll_create()时创建的size

3. LT和ET模式

  • LT(Level Triggered,电平触发):LT模式是epoll默认的工作模式,也是select和poll的工作模式,在LT模式下,epoll相当于一个效率较高的poll。
    • 采用LT模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理此事件,当下一次调用epoll_wait是,epoll_wait还会将此事件通告应用程序。
  • ET(Edge Triggered,边沿触发):当调用epoll_ctl,向参数event注册EPOLLET事件时,epoll将以ET模式来操作该文件描述符,ET模式是epoll的高效工作模式.
    • 对于采用ET模式的文件描述符,当epoll_wait检测到其上有事件发生并将此通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不在向应用程序通知这一事件。ET模式降低了同意epoll事件被触发的次数,效率比LT模式高。

4. LT和ET的服务端和客户端代码

4.1 服务器端

#include <sys/epoll.h>
#include <fcntl.h>
#include "wrap.h"

#define MAX_EVENT_NUM           1024
#define BUFFER_SIZE             10
#define true                    1
#define false                   0

int setnonblocking(int fd)
{
        int old_opt = fcntl(fd, F_GETFD);
        int new_opt = old_opt | O_NONBLOCK;
        fcntl(fd, F_SETFD, new_opt);

        return old_opt;
}//将文件描述符设置为非阻塞的

void addfd(int epollfd, int fd, int enable_et)
{
        struct epoll_event event;
        event.data.fd = fd;
        event.events = EPOLLIN;
        if(enable_et){
                event.events |= EPOLLET;
        }
        epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
//      setnonblocking(fd);
}//将文件描述符fd的EPOLLIN注册到epollfd指示的epoll内核事件表中,enable_et表示是否对fd启用ET模式

void lt(struct epoll_event *events, int num, int epollfd, int listenfd)
{
        char buf[BUFFER_SIZE];
        for(int i = 0; i < num; i++){
                int sockfd = events[i].data.fd;
                if(sockfd == listenfd){
                        struct sockaddr_in clientaddr;
                        socklen_t clilen = sizeof(clientaddr);
                        int connfd = Accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);
                        addfd(epollfd, connfd, false);//对connfd使用默认的lt模式
                }else if(events[i].events & EPOLLIN){//只要socket读缓存中还有未读的数据,这段代码就会触发
                        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 of content:%s\n", ret, buf);
                }else{
                        printf("something else happened\n");
                }
        }
}

void et(struct epoll_event *event, int num, int epollfd, int listenfd)
{
        char buf[BUFFER_SIZE];
        for(int i = 0; i < num; i++){
                int sockfd = event[i].data.fd;
                if(sockfd == listenfd){
                        struct sockaddr_in clientaddr;
                        int clilen = sizeof(clientaddr);
                        int connfd = Accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);
                        addfd(epollfd, connfd, true);//多connfd开启ET模式
                }else if(event[i].events & EPOLLIN){
                        printf("event trigger once\n");
                        while(1){//这段代码不会重复触发,所以要循环读取数据
                                memset(buf, '\0', BUFFER_SIZE);
                                int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
                                if(ret < 0){
                                        if((errno == EAGAIN) || (errno == EWOULDBLOCK)){
                                                printf("read later\n");
                                                break;
                                        }
                                        Close(sockfd);
                                        break;
                                }else if(ret == 0){
                                        Close(sockfd);
                                }else{
                                        printf("get %d bytes of content:%s\n", ret, buf);
                                }
                        }
                }else{

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

int start_ser(char *ipaddr, char *port)
{
        int sock = Socket(AF_INET, SOCK_STREAM, 0);

        struct sockaddr_in serveraddr;
        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(port));
        inet_pton(AF_INET, ipaddr, &serveraddr.sin_addr);

        Bind(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

        Listen(sock, 128);

        return sock;
}

int main(int argc, char *argv[])
{
        int listenfd = start_ser(argv[1], argv[2]);

        struct epoll_event events[MAX_EVENT_NUM];
        int epollfd = epoll_create(5);
        if(epollfd < 0){
                perr_exit("epoll_create err");
        }
        addfd(epollfd, listenfd, true);
        while(1){
                int ret = epoll_wait(epollfd, events, MAX_EVENT_NUM, -1);
                if(ret < 0){
                        printf("epoll failure\n");
                        break;
                }

                lt(events, ret, epollfd, listenfd);//lt模式
                //et(events, ret, epollfd, listenfd);//et模式
        }
        Close(listenfd);
        return 0;
}
//warp.h文件是将socket,bind,listen等函数封装为第一个字母大写的头文件

4.2 客户端

#include "wrap.h"                                                            

int main(int argc, char *argv[])
{
        int connfd;
        struct sockaddr_in serveraddr;
        char buf[1024];

        connfd = Socket(AF_INET, SOCK_STREAM, 0);

        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);

        Connect(connfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

        while(fgets(buf, 1024, stdin) != NULL){
                Write(connfd, buf, strlen(buf));
        }

        Close(connfd);
        return 0;
}

4.3 两种模式结果对比

ET模式

LT模式
当发送超过缓冲区大小的数据量,LT会多次调用epoll_wait函数接受数据,则打印了多次“event level once”,而ET则是循环读取数据知道读完,打印了一次“event trigger once”。

<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在大规模并发下更高效。 最后,提醒用户在实际开发中需要注意的事项,比如正确处理错误码,避免文件描述符泄漏,以及合理设置事件类型等。</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 <sys/epoll.h> #include <fcntl.h> #include <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<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模式需要更严谨的代码设计,建议在性能关键路径使用。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值