一、epoll含义

        epoll 是 Linux 特有的高性能 I/O 事件通知机制,主要用来替代传统的 select/poll,尤其在连接数非常多时性能优势极其明显,被几乎所有高并发网络服务器广泛使用。

二、为什么需要epoll?

机制 最大描述符数限制 每次调用时间复杂度 是否需要每次都拷贝 fd 集合到内核 返回后是否需要遍历全部 fd
select 通常 1024(可改) O(n) 是(fd_set 拷贝) 是(O(n) 轮询)
poll 无硬性限制 O(n) 是(pollfd 数组拷贝) 是(O(n) 轮询)
epoll 无硬性限制 O(1) 注册,O(k) 等待(k 为就绪事件数) 只需要注册一次

只需要遍历就绪的 k 个

三、epoll的三种核心系统调用

        

// 1. 创建一个 epoll 实例(返回一个 epoll 文件描述符)
int epoll_create(int size);        // size 只是提示,内核现在忽略它
int epoll_create1(int flags);      // 推荐使用,flags 可设 EPOLL_CLOEXEC

// 2. 往 epoll 实例中添加/修改/删除感兴趣的文件描述符和事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

op 可取值:
    EPOLL_CTL_ADD    // 注册
    EPOLL_CTL_MOD    // 修改已注册 fd 的事件
    EPOLL_CTL_DEL    // 删除(event 可为 NULL)

struct epoll_event {
    uint32_t     events;   // 要监听的事件位掩码
    epoll_data_t data;     // 用户自定义数据
};

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

// 常用 events 标志(可 | 组合)
EPOLLIN      // 可读(包括对端正常关闭)
EPOLLOUT     // 可写
EPOLLERR     // 错误
EPOLLHUP     // 挂起(对端异常关闭)
EPOLLRDHUP   // 半关闭(对端 shutdown WR 或关闭,Linux 2.6.17+)
EPOLLPRI     // 带外数据
EPOLLONESHOT // 一旦触发后自动禁用,需重新 EPOLL_CTL_MOD 启用(防惊群)
EPOLLET      // 边缘触发 Edge Triggered(关键!默认是水平触发 LT)

// 3. 等待事件发生(类似 select)
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);
// timeout: -1 永久阻塞,0 立即返回,>0 毫秒
// 返回值:就绪的事件个数(0 表示超时)

四、两种工作模式

特性 水平触发 LT(默认) 边缘触发 ET(需显式设置 EPOLLET)
触发条件 只要缓冲区还有数据可读/可写就会一直通知 只有在状态变化时才通知一次(比如从不可读→可读)
是否需要读完所有数据 不需要,读一部分下次还会再通知 必须一次读完(用非阻塞 + 循环读到 EAGAIN 为止)
编程难度 简单,和 select/poll 几乎一样 复杂,容易漏事件,必须配合非阻塞 I/O
性能 稍低(可能产生更多系统调用) 更高(系统调用次数最少)
典型使用场景 初学者、逻辑简单、少量连接 高并发服务器(Nginx、Redis 都是 ET 模式)

ET 模式必备技巧

        把 socket 设置为非阻塞(fcntl(fd, F_SETFL, O_NONBLOCK))

        读时循环 read/recv 直到返回 EAGAIN

        写时同理(通常只有在 write 返回 EAGAIN 时才关注 EPOLLOUT)

五、高并发tcp服务器

        

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#define MAXFD 10
int socket_init();
void setnonblock(int fd)
{
    int oldfl = fcntl(fd,F_GETFL);
    int newfl = oldfl | O_NONBLOCK;
    if( fcntl(fd,F_SETFL,newfl) == -1)
    {
        printf("fcntl err\n");
    }
}
void epoll_add(int epfd, int fd)
{
    struct epoll_event ev;
    ev.data.fd = fd;
    ev.events = EPOLLIN | EPOLLET;//开启ET模式
    setnonblock(fd);//设置非阻塞
    if( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
    {
        printf("epoll add err\n");
    }
}
void epoll_del(int epfd, int fd)
{
    if( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1)
    {
        printf("epoll del err\n");
    }
}

void accept_client(int sockfd,int epfd)
{
    int c = accept(sockfd,NULL,NULL);
    if( c < 0 )
    {
        return;
    }

    printf("accept c=%d\n",c);
    epoll_add(epfd,c);//把新接受的连接添加到内核事件表(红黑树)
}

void recv_data(int c,int epfd)
{
    while( 1 )
    {
        char buff[128] = {0};
        int n = recv(c,buff,1,0);
        if( n == -1 )
        {
            if( errno != EAGAIN && errno != EWOULDBLOCK )
            {
                printf("recv err");
            }
            else
            {
                send(c,"ok",2,0);
            }
            break;
        }
        else if( n == 0 )
        {
            epoll_del(epfd,c);
            close(c);
            printf("client close\n");
            break;
        }
        else
        {
            printf("buff=%s\n",buff);
        }
    }
}

int main()
{
    int sockfd = socket_init();
    if( sockfd == -1)
    {
        exit(1);
    }

    int epfd = epoll_create(MAXFD);//创建内核事件表(收集描述符)   红黑树
    if( epfd == -1)
    {
        exit(1);
    }

    epoll_add(epfd,sockfd);//将监听套接字sockfd添加到内核事件表
    struct epoll_event evs[MAXFD];//存放就绪描述符
    while(1)
    {
        int n = epoll_wait(epfd,evs,MAXFD,5000);//获取就绪描述符 可能阻塞
        printf("------wait----\n");
        if( n == -1 )
        {
            printf("epoll err\n");
        }
        else if ( n == 0 )
        {
            printf("time out\n");
        }
        else
        {
            for(int i = 0; i < n; i++)
            {
                int fd = evs[i].data.fd;
                if( evs[i].events & EPOLLIN)
                {
                    if( fd == sockfd )
                    {
                        accept_client(sockfd,epfd);
                    }
                    else
                    {
                        recv_data(fd,epfd);
                    }
                }
                //if( evs[i].events & EPOLLOUT)
            }
        }
    }

}

int socket_init()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        return -1;
    }

    struct sockaddr_in saddr;
    memset(&saddr, 0, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(6000);
    saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (res == -1)
    {
        printf("bind err\n");
        return -1;
    }

    res = listen(sockfd, 5);
    if (res == -1)
    {
        return -1;
    }

    return sockfd;
}

六、epoll内部

        红黑树:管理所有被监听的 fd(epoll_ctl 的 O(log n) → 实际几乎 O(1))

        rdlist(就绪链表):双向链表,存放已经就绪的 fd,epoll_wait 直接返回这个链表(O(1))

        回调机制:当 fd 状态变化时,内核调用 ep_insert 把 fd 插入 rdlist(这就是为什么 ET 只需要通知一次)

七、总结

时间复杂度优秀

        select/poll:每次调用都需要把整个关注的文件描述符集合(fd set)从用户态拷贝到内核态,并且内核需要线性扫描整个集合来找出哪些 fd 就绪,时间复杂度是 O(n)。

        epoll:只需要在初次注册兴趣事件时(epoll_ctl)拷贝一次,之后每次 epoll_wait 调用时,内核直接返回已经就绪的事件列表,时间复杂度接近 O(1)(实际上是 O(就绪事件数量)),不需要再扫描全部 fd。

文件描述符数量限制极高

        select:受限于 FD_SETSIZE(通常是 1024),并且 fd 必须是低位编号。

        poll:理论上没有 fd 个数限制,但内部用数组线性存储,fd 越多内存和扫描开销越大。

        epoll:使用红黑树 + 就绪链表(或回调)结构管理关注的 fd,单个 epoll 实例可以轻松支持数十万甚至上百万个连接(只受系统内存和 max user watches 限制)。

无需每次都重新传入感兴趣的 fd 集合

        select/poll:每次调用都要把整个 fd 集合和感兴趣的事件重新传给内核(即使大部分都没变化)。

        epoll:通过 epoll_ctl 一次性注册/修改/删除感兴趣的事件,后续 epoll_wait 只负责取结果,极大减少系统调用和数据拷贝开销。

支持两种高效触发模式

        水平触发(LT):默认模式,只要 fd 可读/可写就会一直通知,和 poll 行为类似,适合大多数场景。

        边缘触发(ET):只有状态发生变化时才通知一次,能极大减少事件通知次数(高并发服务器常用),配合非阻塞 I/O 可以达到最高性能。

事件就绪通知机制更高效(内核回调 + 链表)

        当某个 fd 就绪时,内核会直接把它挂到 epoll 实例的就绪链表(rdlist)上,epoll_wait 只需要把这条链表 splice 到用户空间即可,几乎没有额外开销。

支持 EPOLLONESHOT 和线程安全

        EPOLLONESHOT:可以让某个 fd 在被某个线程处理后自动禁用,避免多个线程同时处理同一个 socket 的“惊群”问题。

        多个线程/进程可以安全地对同一个 epoll 实例调用 epoll_wait(底层用了 mutex + wait queue)。

内存拷贝开销极低

        select/poll 每次都要把几千甚至几万个 fd 的 bitmap/数组拷贝来拷贝去,而 epoll 只需要在注册时拷贝一次 struct epoll_event,之后只拷贝真正就绪的那几个。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐