Linux IO 多路转接详解:从 select、poll 到 epoll

引言

在 Linux 网络编程中,IO 多路转接是一个绕不开的话题。很多高性能服务器,比如 Web 服务器、网关服务器、长连接服务器,底层都离不开它。

所谓 IO 多路转接,本质上解决的是一个问题:

一个线程如何同时管理多个 socket 连接?

如果没有 IO 多路转接,我们很容易写出一个连接一个线程的服务器。刚开始看起来很自然,但连接数一多,线程数量、内存消耗和上下文切换都会变得很夸张。

所以 Linux 提供了 select、poll、epoll 这一类接口,让我们可以用一个线程同时监听多个文件描述符,哪个 fd 有事件,就处理哪个 fd。


一、为什么需要 IO 多路转接?

先看一个最普通的 TCP 服务器逻辑:

cpp 复制代码
while (true)
{
    int sock = accept(listenSock, nullptr, nullptr);

    char buffer[1024];
    read(sock, buffer, sizeof(buffer));

    // 处理客户端数据
}

这段代码的问题在于,acceptread 默认都是阻塞的。

如果服务器卡在某个客户端的 read 上,而这个客户端又迟迟不发数据,那么服务器就无法继续处理其他客户端连接。

于是有人会想到:那我每来一个连接,就创建一个线程处理它。

cpp 复制代码
while (true)
{
    int sock = accept(listenSock, nullptr, nullptr);

    std::thread t([sock]() {
        char buffer[1024];

        while (true)
        {
            ssize_t n = read(sock, buffer, sizeof(buffer));
            if (n <= 0) break;

            // 处理数据
        }

        close(sock);
    });

    t.detach();
}

这种方式在连接数少的时候没问题,但如果连接数达到几千、几万,就会出现明显问题:

问题 说明
线程数量太多 一个连接一个线程,连接数越多线程越多
内存占用大 每个线程都有自己的栈空间
上下文切换频繁 CPU 在大量线程之间切换,真正处理业务的时间变少
扩展性差 不适合高并发长连接场景

所以我们需要一种更好的方式:

不让每个连接都占用一个线程,而是让一个线程同时观察多个连接。

这就是 IO 多路转接。

可以简单理解成下面这样:
#mermaid-svg-HVvafWA1yPJM2xLO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HVvafWA1yPJM2xLO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HVvafWA1yPJM2xLO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HVvafWA1yPJM2xLO .error-icon{fill:#552222;}#mermaid-svg-HVvafWA1yPJM2xLO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HVvafWA1yPJM2xLO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HVvafWA1yPJM2xLO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HVvafWA1yPJM2xLO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HVvafWA1yPJM2xLO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HVvafWA1yPJM2xLO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HVvafWA1yPJM2xLO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HVvafWA1yPJM2xLO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HVvafWA1yPJM2xLO .marker.cross{stroke:#333333;}#mermaid-svg-HVvafWA1yPJM2xLO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HVvafWA1yPJM2xLO p{margin:0;}#mermaid-svg-HVvafWA1yPJM2xLO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HVvafWA1yPJM2xLO .cluster-label text{fill:#333;}#mermaid-svg-HVvafWA1yPJM2xLO .cluster-label span{color:#333;}#mermaid-svg-HVvafWA1yPJM2xLO .cluster-label span p{background-color:transparent;}#mermaid-svg-HVvafWA1yPJM2xLO .label text,#mermaid-svg-HVvafWA1yPJM2xLO span{fill:#333;color:#333;}#mermaid-svg-HVvafWA1yPJM2xLO .node rect,#mermaid-svg-HVvafWA1yPJM2xLO .node circle,#mermaid-svg-HVvafWA1yPJM2xLO .node ellipse,#mermaid-svg-HVvafWA1yPJM2xLO .node polygon,#mermaid-svg-HVvafWA1yPJM2xLO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HVvafWA1yPJM2xLO .rough-node .label text,#mermaid-svg-HVvafWA1yPJM2xLO .node .label text,#mermaid-svg-HVvafWA1yPJM2xLO .image-shape .label,#mermaid-svg-HVvafWA1yPJM2xLO .icon-shape .label{text-anchor:middle;}#mermaid-svg-HVvafWA1yPJM2xLO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HVvafWA1yPJM2xLO .rough-node .label,#mermaid-svg-HVvafWA1yPJM2xLO .node .label,#mermaid-svg-HVvafWA1yPJM2xLO .image-shape .label,#mermaid-svg-HVvafWA1yPJM2xLO .icon-shape .label{text-align:center;}#mermaid-svg-HVvafWA1yPJM2xLO .node.clickable{cursor:pointer;}#mermaid-svg-HVvafWA1yPJM2xLO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HVvafWA1yPJM2xLO .arrowheadPath{fill:#333333;}#mermaid-svg-HVvafWA1yPJM2xLO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HVvafWA1yPJM2xLO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HVvafWA1yPJM2xLO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HVvafWA1yPJM2xLO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HVvafWA1yPJM2xLO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HVvafWA1yPJM2xLO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HVvafWA1yPJM2xLO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HVvafWA1yPJM2xLO .cluster text{fill:#333;}#mermaid-svg-HVvafWA1yPJM2xLO .cluster span{color:#333;}#mermaid-svg-HVvafWA1yPJM2xLO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HVvafWA1yPJM2xLO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HVvafWA1yPJM2xLO rect.text{fill:none;stroke-width:0;}#mermaid-svg-HVvafWA1yPJM2xLO .icon-shape,#mermaid-svg-HVvafWA1yPJM2xLO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HVvafWA1yPJM2xLO .icon-shape p,#mermaid-svg-HVvafWA1yPJM2xLO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HVvafWA1yPJM2xLO .icon-shape .label rect,#mermaid-svg-HVvafWA1yPJM2xLO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HVvafWA1yPJM2xLO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HVvafWA1yPJM2xLO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HVvafWA1yPJM2xLO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户程序
select / poll / epoll
内核帮忙监听多个 fd
fd 1 可读
fd 2 无事件
fd 3 可写
处理 fd 1
处理 fd 3

也就是说,程序不再傻傻地阻塞在某一个 fd 上,而是把一批 fd 交给内核:

text 复制代码
这些 fd 你帮我看着,谁有事件了你告诉我。

二、select 和 poll 简单理解

Linux 下比较常见的 IO 多路转接接口有三个:

text 复制代码
select  ->  poll  ->  epoll

它们的目标是一样的,都是监听多个 fd 的事件。区别在于实现方式和效率不同。


1. select

select 是比较早期的 IO 多路转接接口。

函数原型如下:

cpp 复制代码
#include <sys/select.h>

int select(
    int nfds,
    fd_set* readfds,
    fd_set* writefds,
    fd_set* exceptfds,
    struct timeval* timeout
);

select 使用 fd_set 来保存要监听的文件描述符。

常用操作如下:

cpp 复制代码
FD_ZERO(fd_set* set);        // 清空集合
FD_SET(int fd, fd_set* set); // 添加 fd
FD_CLR(int fd, fd_set* set); // 删除 fd
FD_ISSET(int fd, fd_set* set); // 判断 fd 是否就绪

比如监听标准输入:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/select.h>

int main()
{
    while (true)
    {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(0, &readfds); // 0 表示标准输入

        int ret = select(1, &readfds, nullptr, nullptr, nullptr);

        if (ret < 0)
        {
            perror("select");
            break;
        }

        if (FD_ISSET(0, &readfds))
        {
            char buffer[1024] = {0};
            ssize_t n = read(0, buffer, sizeof(buffer) - 1);

            if (n > 0)
            {
                std::cout << "输入内容: " << buffer << std::endl;
            }
        }
    }

    return 0;
}

select 的大致流程是:
#mermaid-svg-9XflxxRHSGLBcBH1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9XflxxRHSGLBcBH1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9XflxxRHSGLBcBH1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9XflxxRHSGLBcBH1 .error-icon{fill:#552222;}#mermaid-svg-9XflxxRHSGLBcBH1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9XflxxRHSGLBcBH1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9XflxxRHSGLBcBH1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9XflxxRHSGLBcBH1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9XflxxRHSGLBcBH1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9XflxxRHSGLBcBH1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9XflxxRHSGLBcBH1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9XflxxRHSGLBcBH1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9XflxxRHSGLBcBH1 .marker.cross{stroke:#333333;}#mermaid-svg-9XflxxRHSGLBcBH1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9XflxxRHSGLBcBH1 p{margin:0;}#mermaid-svg-9XflxxRHSGLBcBH1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9XflxxRHSGLBcBH1 .cluster-label text{fill:#333;}#mermaid-svg-9XflxxRHSGLBcBH1 .cluster-label span{color:#333;}#mermaid-svg-9XflxxRHSGLBcBH1 .cluster-label span p{background-color:transparent;}#mermaid-svg-9XflxxRHSGLBcBH1 .label text,#mermaid-svg-9XflxxRHSGLBcBH1 span{fill:#333;color:#333;}#mermaid-svg-9XflxxRHSGLBcBH1 .node rect,#mermaid-svg-9XflxxRHSGLBcBH1 .node circle,#mermaid-svg-9XflxxRHSGLBcBH1 .node ellipse,#mermaid-svg-9XflxxRHSGLBcBH1 .node polygon,#mermaid-svg-9XflxxRHSGLBcBH1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9XflxxRHSGLBcBH1 .rough-node .label text,#mermaid-svg-9XflxxRHSGLBcBH1 .node .label text,#mermaid-svg-9XflxxRHSGLBcBH1 .image-shape .label,#mermaid-svg-9XflxxRHSGLBcBH1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-9XflxxRHSGLBcBH1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9XflxxRHSGLBcBH1 .rough-node .label,#mermaid-svg-9XflxxRHSGLBcBH1 .node .label,#mermaid-svg-9XflxxRHSGLBcBH1 .image-shape .label,#mermaid-svg-9XflxxRHSGLBcBH1 .icon-shape .label{text-align:center;}#mermaid-svg-9XflxxRHSGLBcBH1 .node.clickable{cursor:pointer;}#mermaid-svg-9XflxxRHSGLBcBH1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9XflxxRHSGLBcBH1 .arrowheadPath{fill:#333333;}#mermaid-svg-9XflxxRHSGLBcBH1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9XflxxRHSGLBcBH1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9XflxxRHSGLBcBH1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9XflxxRHSGLBcBH1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9XflxxRHSGLBcBH1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9XflxxRHSGLBcBH1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9XflxxRHSGLBcBH1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9XflxxRHSGLBcBH1 .cluster text{fill:#333;}#mermaid-svg-9XflxxRHSGLBcBH1 .cluster span{color:#333;}#mermaid-svg-9XflxxRHSGLBcBH1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9XflxxRHSGLBcBH1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9XflxxRHSGLBcBH1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-9XflxxRHSGLBcBH1 .icon-shape,#mermaid-svg-9XflxxRHSGLBcBH1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9XflxxRHSGLBcBH1 .icon-shape p,#mermaid-svg-9XflxxRHSGLBcBH1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9XflxxRHSGLBcBH1 .icon-shape .label rect,#mermaid-svg-9XflxxRHSGLBcBH1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9XflxxRHSGLBcBH1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9XflxxRHSGLBcBH1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9XflxxRHSGLBcBH1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没有

准备 fd_set
调用 select
内核检查 fd 集合
是否有 fd 就绪
阻塞等待
select 返回
用户遍历 fd_set
处理就绪 fd

select 的缺点也很明显:

第一,默认最多监听 1024 个 fd。

第二,每次调用 select 之前,都要重新设置 fd_set,因为 select 返回时会修改传入的集合。

第三,select 返回后,只告诉你"有 fd 就绪了",但是不会直接告诉你具体是哪几个 fd,所以还要自己遍历一遍。

cpp 复制代码
for (int fd = 0; fd <= maxfd; fd++)
{
    if (FD_ISSET(fd, &readfds))
    {
        // 处理就绪 fd
    }
}

fd 数量少的时候还好,fd 数量多的时候,这种线性遍历就比较浪费。


2. poll

poll 可以看成是 select 的改进版。

函数原型如下:

cpp 复制代码
#include <poll.h>

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

核心结构体是 pollfd

cpp 复制代码
struct pollfd
{
    int fd;         // 要监听的 fd
    short events;   // 关心的事件
    short revents;  // 实际发生的事件
};

常见事件如下:

事件 含义
POLLIN 可读
POLLOUT 可写
POLLERR 错误
POLLHUP 对端关闭或挂起
POLLNVAL 非法 fd

监听标准输入的例子:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <poll.h>

int main()
{
    struct pollfd fds[1];
    fds[0].fd = 0;
    fds[0].events = POLLIN;

    while (true)
    {
        int ret = poll(fds, 1, -1);

        if (ret < 0)
        {
            perror("poll");
            break;
        }

        if (fds[0].revents & POLLIN)
        {
            char buffer[1024] = {0};
            ssize_t n = read(0, buffer, sizeof(buffer) - 1);

            if (n > 0)
            {
                std::cout << "输入内容: " << buffer << std::endl;
            }
        }
    }

    return 0;
}

相比 selectpoll 的好处是:

  • 不再受 FD_SETSIZE 的限制;
  • 使用数组保存 fd,结构更清晰;
  • eventsrevents 分开,用户关心的事件和实际发生的事件不会混在一起。

但是 poll 仍然有一个核心问题:

poll 返回后,用户依然需要遍历整个数组,找出哪些 fd 就绪。

所以 poll 虽然比 select 好一些,但在高并发场景下仍然不够理想。


三、重点理解 epoll

epoll 是 Linux 下最常用的高性能 IO 多路转接接口。

它和 selectpoll 最大的区别是:

select 和 poll 每次都要把一批 fd 传给内核;

epoll 是先把 fd 注册到内核中,之后事件来了再通知用户。

这就像两种不同的工作方式。

select/poll 更像这样:

text 复制代码
用户:这些 fd 你帮我检查一下。
内核:好,我检查完了,有几个能用了。
用户:是哪几个?我自己再遍历看看。

epoll 更像这样:

text 复制代码
用户:这些 fd 以后都交给你管理。
内核:好。
用户:有事件了吗?
内核:有,fd 3 和 fd 7 就绪了,直接拿去处理。

1. epoll 的三个核心接口

使用 epoll 主要掌握三个函数:

text 复制代码
epoll_create / epoll_create1
epoll_ctl
epoll_wait

第一个是创建 epoll 实例:

cpp 复制代码
#include <sys/epoll.h>

int epfd = epoll_create1(0);

这里返回的 epfd 也是一个文件描述符,可以把它理解成一个"事件管理器"。

第二个是把 fd 添加、修改或者删除到 epoll 中:

cpp 复制代码
int epoll_ctl(
    int epfd,
    int op,
    int fd,
    struct epoll_event* event
);

常见操作有:

操作 说明
EPOLL_CTL_ADD 添加 fd
EPOLL_CTL_MOD 修改 fd 关心的事件
EPOLL_CTL_DEL 删除 fd

添加一个 socket 到 epoll 中:

cpp 复制代码
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sock;

epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);

第三个是等待事件发生:

cpp 复制代码
int epoll_wait(
    int epfd,
    struct epoll_event* events,
    int maxevents,
    int timeout
);

使用方式一般是:

cpp 复制代码
struct epoll_event events[1024];

int n = epoll_wait(epfd, events, 1024, -1);

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

    if (events[i].events & EPOLLIN)
    {
        // fd 可读
    }
}

注意这里非常关键:

epoll_wait 返回后,events 数组里放的就是已经就绪的事件。

这和 selectpoll 很不一样。

selectpoll 返回后,还要自己从一堆 fd 里面找谁就绪了。

epoll_wait 返回后,直接处理返回的这几个事件即可。


2. epoll 的工作流程

epoll 编程的基本流程如下:
#mermaid-svg-gkw1NQiLLJEVWgDK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gkw1NQiLLJEVWgDK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gkw1NQiLLJEVWgDK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gkw1NQiLLJEVWgDK .error-icon{fill:#552222;}#mermaid-svg-gkw1NQiLLJEVWgDK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gkw1NQiLLJEVWgDK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gkw1NQiLLJEVWgDK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gkw1NQiLLJEVWgDK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gkw1NQiLLJEVWgDK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gkw1NQiLLJEVWgDK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gkw1NQiLLJEVWgDK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gkw1NQiLLJEVWgDK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gkw1NQiLLJEVWgDK .marker.cross{stroke:#333333;}#mermaid-svg-gkw1NQiLLJEVWgDK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gkw1NQiLLJEVWgDK p{margin:0;}#mermaid-svg-gkw1NQiLLJEVWgDK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gkw1NQiLLJEVWgDK .cluster-label text{fill:#333;}#mermaid-svg-gkw1NQiLLJEVWgDK .cluster-label span{color:#333;}#mermaid-svg-gkw1NQiLLJEVWgDK .cluster-label span p{background-color:transparent;}#mermaid-svg-gkw1NQiLLJEVWgDK .label text,#mermaid-svg-gkw1NQiLLJEVWgDK span{fill:#333;color:#333;}#mermaid-svg-gkw1NQiLLJEVWgDK .node rect,#mermaid-svg-gkw1NQiLLJEVWgDK .node circle,#mermaid-svg-gkw1NQiLLJEVWgDK .node ellipse,#mermaid-svg-gkw1NQiLLJEVWgDK .node polygon,#mermaid-svg-gkw1NQiLLJEVWgDK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gkw1NQiLLJEVWgDK .rough-node .label text,#mermaid-svg-gkw1NQiLLJEVWgDK .node .label text,#mermaid-svg-gkw1NQiLLJEVWgDK .image-shape .label,#mermaid-svg-gkw1NQiLLJEVWgDK .icon-shape .label{text-anchor:middle;}#mermaid-svg-gkw1NQiLLJEVWgDK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gkw1NQiLLJEVWgDK .rough-node .label,#mermaid-svg-gkw1NQiLLJEVWgDK .node .label,#mermaid-svg-gkw1NQiLLJEVWgDK .image-shape .label,#mermaid-svg-gkw1NQiLLJEVWgDK .icon-shape .label{text-align:center;}#mermaid-svg-gkw1NQiLLJEVWgDK .node.clickable{cursor:pointer;}#mermaid-svg-gkw1NQiLLJEVWgDK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gkw1NQiLLJEVWgDK .arrowheadPath{fill:#333333;}#mermaid-svg-gkw1NQiLLJEVWgDK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gkw1NQiLLJEVWgDK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gkw1NQiLLJEVWgDK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gkw1NQiLLJEVWgDK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gkw1NQiLLJEVWgDK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gkw1NQiLLJEVWgDK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gkw1NQiLLJEVWgDK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gkw1NQiLLJEVWgDK .cluster text{fill:#333;}#mermaid-svg-gkw1NQiLLJEVWgDK .cluster span{color:#333;}#mermaid-svg-gkw1NQiLLJEVWgDK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gkw1NQiLLJEVWgDK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gkw1NQiLLJEVWgDK rect.text{fill:none;stroke-width:0;}#mermaid-svg-gkw1NQiLLJEVWgDK .icon-shape,#mermaid-svg-gkw1NQiLLJEVWgDK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gkw1NQiLLJEVWgDK .icon-shape p,#mermaid-svg-gkw1NQiLLJEVWgDK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gkw1NQiLLJEVWgDK .icon-shape .label rect,#mermaid-svg-gkw1NQiLLJEVWgDK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gkw1NQiLLJEVWgDK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gkw1NQiLLJEVWgDK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gkw1NQiLLJEVWgDK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建 listenSock
创建 epoll 实例
把 listenSock 加入 epoll
epoll_wait 等待事件
事件来自谁
listenSock 就绪
clientSock 就绪
accept 新连接
把 clientSock 加入 epoll
read 读取数据

代码层面可以理解成:

cpp 复制代码
int epfd = epoll_create1(0);

// 监听 socket 加入 epoll
epoll_ctl(epfd, EPOLL_CTL_ADD, listenSock, &ev);

while (true)
{
    int n = epoll_wait(epfd, events, 1024, -1);

    for (int i = 0; i < n; i++)
    {
        if (events[i].data.fd == listenSock)
        {
            // 有新连接,accept
        }
        else
        {
            // 普通客户端发数据,read
        }
    }
}

这就是 epoll 服务器的核心骨架。


3. epoll 为什么适合高并发?

epoll 的优势主要体现在三个方面。

第一,epoll 不需要每次都把所有 fd 重新传给内核。

selectpoll 每调用一次,都要把 fd 集合传进去;而 epoll 是通过 epoll_ctl 先把 fd 注册进去,后续只需要调用 epoll_wait 等事件。

第二,epoll 返回的就是就绪事件。

如果有 10000 个连接,但此时只有 50 个连接发数据,那么 epoll 返回的就是这 50 个就绪事件。程序只需要处理这 50 个,而不是把 10000 个连接全部扫一遍。

第三,epoll 更适合"连接很多,但活跃连接相对较少"的场景。

比如:

  • 聊天服务器;
  • 网关服务器;
  • 长连接服务;
  • 游戏服务器;
  • WebSocket 服务。

这些场景下,连接数量可能很多,但不是每个连接每一刻都在发数据。epoll 的优势就会比较明显。


4. epoll 的 LT 和 ET 模式

epoll 有两种常见工作模式:

text 复制代码
LT:Level Trigger,水平触发
ET:Edge Trigger,边缘触发
LT 模式

LT 是 epoll 的默认模式。

它的特点是:

只要 fd 上还有数据没读完,epoll_wait 就会一直提醒你。

比如 socket 缓冲区里有 100 字节数据,你这次只读了 50 字节,那么下次调用 epoll_wait 时,它还会继续通知你这个 fd 可读。

LT 模式比较容易使用,不容易漏事件,适合初学阶段。

ET 模式

ET 模式需要加上 EPOLLET

cpp 复制代码
ev.events = EPOLLIN | EPOLLET;

ET 的特点是:

只有 fd 状态发生变化时才通知一次。

也就是说,当 socket 从"没有数据"变成"有数据"时,epoll 通知你一次。

如果这次你没有把数据读完,后面它不一定继续通知你。

所以 ET 模式下,通常必须配合非阻塞 IO,并且一次性读到 EAGAIN

典型写法如下:

cpp 复制代码
while (true)
{
    ssize_t n = read(fd, buffer, sizeof(buffer));

    if (n > 0)
    {
        // 处理读取到的数据
    }
    else if (n == 0)
    {
        // 对端关闭连接
        close(fd);
        break;
    }
    else
    {
        if (errno == EAGAIN || errno == EWOULDBLOCK)
        {
            // 数据已经读完
            break;
        }
        else
        {
            // 读取出错
            close(fd);
            break;
        }
    }
}

LT 和 ET 可以这样对比:

模式 特点 优点 缺点
LT 只要有数据就一直通知 容易使用,不容易出错 通知次数可能更多
ET 状态变化时只通知一次 通知次数少,效率更高 写法要求高,必须读到 EAGAIN

刚开始学习 epoll 时,建议先理解 LT,再去写 ET。


5. 为什么 ET 一定要配合非阻塞?

这是 epoll 里非常容易踩坑的地方。

ET 模式要求我们一次性把数据读完,但是如果 fd 是阻塞的,就可能出现下面的问题:

text 复制代码
第一次 read:读到一部分数据
第二次 read:继续读到数据
第三次 read:缓冲区没数据了
程序阻塞住

程序一旦阻塞在某个 fd 上,就失去了 IO 多路转接的意义。

所以 ET 模式一般要满足三个条件:

text 复制代码
ET 模式
+
非阻塞 fd
+
循环读取直到 EAGAIN

设置非阻塞的代码如下:

cpp 复制代码
#include <fcntl.h>

int SetNonBlock(int fd)
{
    int flags = fcntl(fd, F_GETFL);
    if (flags < 0)
    {
        return -1;
    }

    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

当非阻塞 fd 没有数据可读时,read 不会卡住,而是返回错误,并设置 errnoEAGAINEWOULDBLOCK

这时我们就知道:

当前数据已经读完了,可以退出读取循环了。


四、epoll 服务器代码示例

下面写一个简单的 epoll echo 服务器。

它的功能很简单:

  • 监听指定端口;
  • 接收客户端连接;
  • 读取客户端发来的数据;
  • 把数据原样返回给客户端。

这份代码不是工业级服务器,但非常适合理解 epoll 的基本结构。

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>

#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

static const int MAX_EVENTS = 1024;
static const int BUFFER_SIZE = 4096;

int SetNonBlock(int fd)
{
    int flags = fcntl(fd, F_GETFL);
    if (flags < 0)
    {
        return -1;
    }

    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

bool AddFdToEpoll(int epfd, int fd, uint32_t events)
{
    epoll_event ev;
    ev.events = events;
    ev.data.fd = fd;

    return epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == 0;
}

void CloseFd(int epfd, int fd)
{
    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
    close(fd);
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
        return 1;
    }

    int port = std::stoi(argv[1]);

    int listenSock = socket(AF_INET, SOCK_STREAM, 0);
    if (listenSock < 0)
    {
        perror("socket");
        return 1;
    }

    int opt = 1;
    setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    sockaddr_in local;
    std::memset(&local, 0, sizeof(local));

    local.sin_family = AF_INET;
    local.sin_addr.s_addr = INADDR_ANY;
    local.sin_port = htons(port);

    if (bind(listenSock, reinterpret_cast<sockaddr*>(&local), sizeof(local)) < 0)
    {
        perror("bind");
        close(listenSock);
        return 1;
    }

    if (listen(listenSock, 128) < 0)
    {
        perror("listen");
        close(listenSock);
        return 1;
    }

    SetNonBlock(listenSock);

    int epfd = epoll_create1(0);
    if (epfd < 0)
    {
        perror("epoll_create1");
        close(listenSock);
        return 1;
    }

    if (!AddFdToEpoll(epfd, listenSock, EPOLLIN | EPOLLET))
    {
        perror("epoll_ctl listenSock");
        close(listenSock);
        close(epfd);
        return 1;
    }

    std::cout << "epoll server started, port: " << port << std::endl;

    epoll_event events[MAX_EVENTS];

    while (true)
    {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);

        if (n < 0)
        {
            if (errno == EINTR)
            {
                continue;
            }

            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < n; i++)
        {
            int fd = events[i].data.fd;
            uint32_t ev = events[i].events;

            if (ev & (EPOLLERR | EPOLLHUP))
            {
                CloseFd(epfd, fd);
                continue;
            }

            if (fd == listenSock)
            {
                while (true)
                {
                    sockaddr_in peer;
                    socklen_t len = sizeof(peer);

                    int clientSock = accept(
                        listenSock,
                        reinterpret_cast<sockaddr*>(&peer),
                        &len
                    );

                    if (clientSock < 0)
                    {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                        {
                            break;
                        }

                        perror("accept");
                        break;
                    }

                    SetNonBlock(clientSock);

                    std::cout << "new client: "
                              << inet_ntoa(peer.sin_addr)
                              << ":"
                              << ntohs(peer.sin_port)
                              << ", fd = "
                              << clientSock
                              << std::endl;

                    AddFdToEpoll(epfd, clientSock, EPOLLIN | EPOLLET);
                }
            }
            else if (ev & EPOLLIN)
            {
                char buffer[BUFFER_SIZE];

                while (true)
                {
                    std::memset(buffer, 0, sizeof(buffer));

                    ssize_t size = read(fd, buffer, sizeof(buffer));

                    if (size > 0)
                    {
                        std::cout << "client fd " << fd
                                  << " says: " << buffer;

                        send(fd, buffer, size, 0);
                    }
                    else if (size == 0)
                    {
                        std::cout << "client fd " << fd
                                  << " closed" << std::endl;

                        CloseFd(epfd, fd);
                        break;
                    }
                    else
                    {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                        {
                            break;
                        }

                        perror("read");
                        CloseFd(epfd, fd);
                        break;
                    }
                }
            }
        }
    }

    close(listenSock);
    close(epfd);

    return 0;
}

编译:

bash 复制代码
g++ epoll_server.cc -o epoll_server

运行:

bash 复制代码
./epoll_server 8080

另开一个终端连接:

bash 复制代码
nc 127.0.0.1 8080

输入:

text 复制代码
hello epoll

服务器收到数据后,会把内容原样返回。

这段代码的核心结构其实就是:

text 复制代码
创建监听 socket
创建 epoll
把 listenSock 加入 epoll
循环调用 epoll_wait
如果是 listenSock 就绪,就 accept 新连接
如果是 clientSock 就绪,就 read 数据

五、select、poll、epoll 对比总结

最后用一张表总结一下:

对比项 select poll epoll
数据结构 fd_set 位图 pollfd 数组 内核事件表
fd 数量限制 默认 1024 无固定小上限 无固定小上限
是否每次传入全部 fd
返回后是否遍历所有 fd 否,只遍历就绪事件
高并发能力 一般 一般
跨平台性 较好 较好 Linux 专属
使用难度 简单 中等 中等偏高

可以这样理解:

text 复制代码
select:比较老,简单但限制多。
poll:改进了 select 的 fd 数量限制,但仍然需要线性遍历。
epoll:Linux 下高并发服务器常用方案,更适合大量连接场景。

IO 多路转接的核心不是某个函数有多复杂,而是理解它背后的思想:

不要让程序阻塞在某一个 fd 上,而是让内核帮我们同时观察多个 fd。

而 epoll 的优势在于:

fd 先注册到内核,事件发生后,内核直接把就绪事件返回给用户。

所以在 Linux 高并发网络编程中,epoll 是非常重要的一块内容。

学会 epoll 之后,再去理解 Reactor 模式、线程池服务器、异步网络库,都会轻松很多。

复制代码
相关推荐
syagain_zsx1 小时前
Linux进程控制学习总结(2/2)
linux·运维·学习
珊瑚里的鱼1 小时前
C++14 和 C++17 的核心新特性
开发语言·c++
赵民勇1 小时前
wmctrl命令详解
linux·运维
utf8mb4安全女神1 小时前
shell脚本实现服务器免密登录
linux·运维·服务器
小欣加油1 小时前
leetcode169 多数元素
数据结构·c++·算法·leetcode·职场和发展
JD技术委员会1 小时前
TypeScript 在 MCP Server 开发中为什么受关注
linux·服务器·typescript
zhexiao271 小时前
ohmyzsh 安装与使用
linux
CHANG_THE_WORLD2 小时前
在 VS Code 中让终端显示简洁路径(告别冗长全路径)
linux
zxw6102 小时前
UFOMap代码Debug
c++