文章

网络编程高频总结六

为什么要用epoll

使用 epoll 的原因主要是因为它在处理大规模并发 I/O 操作时,提供了高效的事件通知机制。以下是选择 epoll 的一些关键原因:


1. 高效处理大量文件描述符

  • 传统方法(如 selectpoll)的缺陷:
    • selectpoll 都需要每次调用时重新传入所有的文件描述符(FD),无论这些描述符是否活跃。这会带来较高的开销,尤其是文件描述符数量很大时。
    • select 受文件描述符数量的限制(通常是 1024 或 2048)。
  • epoll 的优势:
    • epoll 不需要重复传入文件描述符,而是通过内核维护一个 epoll 实例,可以动态地添加、修改和删除关注的文件描述符,极大地减少了系统调用的开销。
    • 支持海量文件描述符(理论上只受限于系统的内存)。

2. 事件驱动机制(避免轮询)

  • 传统方法的劣势:
    • pollselect 都是基于线性扫描的,每次调用时都需要遍历整个描述符列表。
    • 当活跃的文件描述符(即有 I/O 事件的描述符)数量较少时,效率非常低。
  • epoll 的改进:
    • epoll 采用了 事件驱动模型,只有发生事件的文件描述符才会被通知,避免了不必要的遍历,提高了性能。
    • 提供了 ET(边缘触发)LT(水平触发) 模式,进一步优化了事件的通知机制。

3. 内存效率

  • selectpoll
    • 每次调用都需要将文件描述符列表从用户空间复制到内核空间,占用大量内存,尤其是高并发场景。
  • epoll
    • epoll 的文件描述符集合存储在内核空间,用户空间只需通过事件通知进行交互,避免了不必要的内存拷贝。

4. 灵活性和可扩展性

  • epoll 提供了三种操作模式:
    1. EPOLL_CTL_ADD:添加文件描述符。
    2. EPOLL_CTL_MOD:修改文件描述符的监听事件。
    3. EPOLL_CTL_DEL:删除文件描述符。
  • 可轻松动态管理关注的事件列表,灵活应对高并发 I/O 的需求。

5. 节省 CPU 资源

  • 通过减少不必要的遍历和轮询,epoll 极大地降低了 CPU 的消耗。
  • 尤其在网络服务器中(如 Web 服务器、聊天室等),epoll 可以显著提升吞吐量和响应速度。

典型应用场景

  • 高并发服务器:如 HTTP 服务器、WebSocket 服务、聊天室、游戏服务器等。
  • 网络编程:在需要同时处理大量连接的场景中(如长连接、大量客户端访问时)。
  • 实时性要求高的系统:需要快速响应 I/O 事件的系统,如交易系统、监控系统。

简单示例:使用 epoll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <sys/epoll.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    int epoll_fd = epoll_create1(0); // 创建 epoll 实例
    if (epoll_fd == -1) {
        perror("epoll_create1");
        return -1;
    }

    struct epoll_event event;
    event.events = EPOLLIN; // 监听读事件
    event.data.fd = STDIN_FILENO; // 标准输入

    // 添加文件描述符到 epoll 实例
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) {
        perror("epoll_ctl");
        return -1;
    }

    struct epoll_event events[10];
    while (1) {
        int num_ready = epoll_wait(epoll_fd, events, 10, -1); // 等待事件
        for (int i = 0; i < num_ready; ++i) {
            if (events[i].events & EPOLLIN) {
                char buf[128];
                ssize_t n = read(events[i].data.fd, buf, sizeof(buf));
                if (n > 0) {
                    buf[n] = '\0';
                    printf("Read: %s", buf);
                }
            }
        }
    }

    close(epoll_fd);
    return 0;
}

总结

epoll 是 Linux 提供的高效 I/O 复用接口,在高并发和低延迟场景下显著优于传统的 selectpoll。它的事件驱动模型、动态管理能力和内核优化使得它成为现代高性能网络编程的首选工具之一。

epoll实现原理,epoll使用的哪种模式,除了epoll,了解select/poll吗

1. epoll 的实现原理

1.1 epoll 的核心组件

  • 红黑树(RB-Tree):
    • 用于存储所有被监听的文件描述符(FD)。这是一个高效的数据结构,支持快速的插入、删除和查找操作。
    • 通过 epoll_ctl 添加或删除 FD 时,内核会在红黑树上进行相关操作。
  • 就绪列表(Ready List):
    • 用于存储当前就绪的事件(即有 I/O 事件的 FD)。
    • 这个列表是一个链表,仅存储那些有事件发生的 FD,避免了遍历所有 FD 的开销。
  • 内核回调机制:
    • 当某个 FD 有事件发生时,通过内核回调将其添加到就绪列表中。这种方式使得 epoll 不需要主动轮询,采用事件驱动模式。

1.2 epoll 的两种触发模式

  1. LT(Level Triggered,水平触发)
    • 默认模式。
    • 如果一个文件描述符有事件发生,只要应用程序没有处理完,epoll_wait 每次都会返回这个事件。
    • 简单易用,但可能导致重复通知。
  2. ET(Edge Triggered,边缘触发)
    • 只在文件描述符的状态从未就绪变为就绪时通知一次。
    • 必须非阻塞地读写文件描述符,否则可能丢失事件。
    • 高效,但对开发者要求更高,需要更加仔细地处理事件。

1.3 epoll 的工作流程

  1. 创建 epoll 实例:
    • 调用 epoll_create1 创建一个 epoll 实例,内核为此分配数据结构。
  2. 管理事件:
    • 通过 epoll_ctl 添加、修改或删除文件描述符到 epoll 实例的红黑树中。
  3. 等待事件:
    • 调用 epoll_wait 等待事件的发生。
    • 内核通过回调机制将有事件的 FD 添加到就绪列表中,epoll_wait 直接返回这些事件。

2. epoll 使用的模式

epoll 使用的是 事件驱动模式,即:

  • 应用程序只需告诉内核它关心的文件描述符和事件。
  • 内核会通过回调通知机制,自动将发生事件的文件描述符加入就绪列表。
  • 应用程序调用 epoll_wait 时,仅处理已经准备好的文件描述符。

这种模式相对于 select/poll 的轮询模式更加高效,尤其是在高并发场景下。


3. select 和 poll 的对比

3.1 select 的特点

  1. 机制
    • 应用程序传入一个固定长度的位图,表示感兴趣的文件描述符。
    • 每次调用 select 都需要重新设置和传递文件描述符集。
  2. 缺点
    • 文件描述符数量有限(通常为 1024 或 2048)。
    • 每次调用时,需要将文件描述符集从用户态复制到内核态,开销大。
    • 需要遍历整个文件描述符集,即使只有少量事件发生,效率低下。
  3. 优点
    • 实现简单,跨平台(POSIX 标准)。

3.2 poll 的特点

  1. 机制
    • 使用一个数组表示文件描述符及其关注的事件。
    • 没有文件描述符数量限制(受系统内存限制)。
    • 每次调用 poll,都需要重新传入整个文件描述符列表。
  2. 缺点
    • 每次调用都需要遍历整个文件描述符数组。
    • select 一样,内核需要拷贝整个数组到用户态。
  3. 优点
    • 支持大于 1024 的文件描述符。
    • 更灵活,易用。

3.3 epoll 与 select/poll 的对比

特性selectpollepoll
文件描述符限制1024 或 2048(固定)无限制(动态分配)无限制(动态分配)
事件处理机制轮询所有描述符轮询所有描述符事件驱动,仅处理就绪事件
数据结构位图(固定大小)数组(动态分配)红黑树 + 就绪列表
性能遍历所有描述符,低效遍历所有描述符,低效就绪事件,性能高
触发模式水平触发水平触发水平触发 + 边缘触发
内存拷贝每次调用都拷贝位图每次调用都拷贝数组拷贝仅发生在添加或修改

4. 为什么要用 epoll

  • 高效:只处理有事件的文件描述符,避免了全量轮询。
  • 可扩展:支持大量文件描述符,不受硬限制。
  • 低开销:内核直接维护事件列表,减少用户态与内核态之间的数据拷贝。
  • 灵活性:支持水平触发(LT)和边缘触发(ET),适合不同场景。

5. 总结

  • selectpoll:适合处理少量文件描述符的简单场景,但随着文件描述符数量的增加,其性能会急剧下降。
  • epoll:专为高并发和大规模 I/O 设计,尤其适合现代网络编程(如高并发服务器、实时应用)。

怎么理解多路复用机制的

多路复用机制的理解

多路复用机制(I/O Multiplexing)是操作系统提供的一种技术,用于在单个线程(或进程)中同时监视多个文件描述符(FD),以便高效处理 I/O 操作。这种机制使得程序不需要为每个文件描述符创建独立的线程或进程,从而有效节省系统资源。


多路复用的核心思想

“一个管理多个”

  • 通过一个统一的接口,监控多个文件描述符的状态(如可读、可写、异常等)。
  • 程序可以一次性获取多个文件描述符的事件,而不是轮流检查每个描述符的状态。

这种机制特别适合网络编程中高并发的场景,例如需要同时处理多个客户端的连接。


多路复用的工作原理

  1. 注册文件描述符:
    • 应用程序向操作系统注册需要关注的文件描述符以及关注的事件类型(如 EPOLLIN 表示可读事件)。
  2. 等待事件:
    • 程序调用操作系统的多路复用接口(如 selectpollepoll)阻塞地等待事件发生。
  3. 事件发生时通知:
    • 操作系统检测到某个文件描述符上发生了指定的事件后,将事件通知应用程序。
  4. 处理就绪事件:
    • 应用程序根据通知的文件描述符进行读写操作,处理完成后继续监听。

常见的多路复用实现

1. select

  • 实现机制
    • 通过固定长度的位图记录需要监控的文件描述符。
    • 每次调用都需要将位图从用户态复制到内核态。
    • 内核依次检查每个文件描述符是否有事件发生。
  • 优缺点
    • 优点:简单易用,支持跨平台。
    • 缺点:受文件描述符数量限制(通常是 1024 或 2048),性能较差(线性扫描所有文件描述符)。

2. poll

  • 实现机制
    • 使用一个动态数组记录需要监控的文件描述符。
    • 每次调用将整个数组从用户态复制到内核态。
    • 内核逐一检查文件描述符的状态。
  • 优缺点
    • 优点:支持任意数量的文件描述符,克服了 select 的限制。
    • 缺点:仍然需要线性遍历文件描述符,性能不够高效。

3. epoll

  • 实现机制
    • 使用内核中的红黑树记录需要监控的文件描述符。
    • 通过事件驱动的回调机制,仅在文件描述符状态发生变化时将其加入就绪列表。
    • 应用程序调用 epoll_wait 时直接获取就绪的事件,避免了全量扫描。
  • 优缺点
    • 优点:高效、支持大规模文件描述符管理,采用事件驱动机制。
    • 缺点:复杂度相对较高,仅适用于 Linux。

多路复用的优势

  1. 资源节省
    • 多路复用允许单线程同时处理多个 I/O,避免为每个文件描述符创建独立线程或进程。
  2. 提高性能
    • 减少线程上下文切换的开销。
    • 在高并发场景中能够显著提升 I/O 处理能力。
  3. 统一接口
    • 程序只需与操作系统的多路复用接口交互,而无需手动管理每个文件描述符的状态。

多路复用的适用场景

  1. 高并发服务器
    • 如 Web 服务器、游戏服务器、聊天服务等。
    • 需要同时处理大量客户端连接。
  2. 事件驱动的应用
    • 如实时监控、消息推送服务等。
  3. 长连接场景
    • 如 WebSocket、在线通信等。

多路复用与阻塞/非阻塞 I/O 的关系

  1. 阻塞 I/O
    • 调用 readwrite 时,线程会被阻塞,直到操作完成。
    • 在需要同时处理多个 I/O 的场景下效率较低。
  2. 非阻塞 I/O
    • 调用 readwrite 时立即返回,即使数据不可用。
    • 需要不断轮询文件描述符状态,增加了 CPU 开销。
  3. 多路复用
    • 结合了阻塞和非阻塞的优点:
      • epoll_wait 等函数中阻塞等待事件。
      • 一旦有事件发生,立即非阻塞地处理对应的文件描述符。

总结

多路复用机制的核心在于高效地管理和处理多个文件描述符的 I/O 事件。通过统一接口和事件驱动模型,它避免了传统轮询方法的低效,同时显著降低了系统资源的开销。selectpollepoll 是多路复用的常见实现,其中 epoll 是现代高性能网络编程的首选。

reactor和proactor的好处和坏处。为什么要用reactor而不是proactor

Reactor 和 Proactor 模式

Reactor 和 Proactor 是两种常见的 I/O 模型设计模式,它们的核心目的是高效地处理 I/O 操作,但它们在处理 I/O 的责任分配和执行机制上存在根本区别。


1. Reactor 模式

工作原理

  • I/O 多路复用:Reactor 使用 I/O 多路复用(如 selectpollepoll)来监听多个文件描述符的状态。
  • 事件驱动:一旦某个文件描述符就绪,Reactor 会将事件通知到应用程序,并由应用程序主动完成 I/O 操作(如 readwrite)。
  • 控制权:事件检测由框架完成,实际的 I/O 操作由应用程序控制。

优点

  1. 灵活性高:
    • 应用程序可以完全控制 I/O 操作的行为(如读取多少数据、写入什么数据),更适合复杂的业务逻辑。
  2. 跨平台性好:
    • Reactor 主要依赖于 I/O 多路复用接口(select/poll/epoll),这些接口在大多数操作系统中都支持。
  3. 适合高并发:
    • 单线程就可以监听大量的文件描述符,非常适合处理高并发 I/O 场景。

缺点

  1. 开发复杂性高:
    • 应用程序需要自己处理 I/O 操作,设计和实现中需要考虑各种边界条件(如缓冲区大小、部分读写等)。
  2. 性能受限于用户态:
    • 用户程序需要从内核获取事件后自行执行 I/O 操作,增加了用户态与内核态之间的上下文切换。

2. Proactor 模式

工作原理

  • 异步 I/O:Proactor 使用操作系统提供的异步 I/O 接口(如 Windows 的 IOCP,POSIX 的 aio_* 系列函数)。
  • 事件完成驱动:应用程序只需提交异步 I/O 请求,操作系统会在后台完成 I/O 操作,并在操作完成时通知应用程序。
  • 控制权:I/O 操作由操作系统完成,应用程序只需要处理结果。

优点

  1. 开发简单:
    • 操作系统直接负责完成所有 I/O 操作,应用程序只需处理完成事件,简化了开发流程。
  2. 性能高:
    • 操作系统直接在内核态完成 I/O 操作,减少了用户态与内核态之间的切换。
  3. 适合 I/O 密集型任务:
    • 操作系统可以高效地处理异步 I/O 请求,适合文件传输、大量数据处理等场景。

缺点

  1. 跨平台性差:
    • 异步 I/O 的实现依赖于操作系统的特性,不同平台的接口差异较大(如 Windows 的 IOCP 和 Linux 的 POSIX AIO)。
  2. 实现复杂性高:
    • 操作系统的异步 I/O 实现相对复杂,部分平台的实现效率较低或支持有限(如 Linux 的 POSIX AIO 在某些情况下性能欠佳)。
  3. 不灵活:
    • I/O 操作的行为由操作系统决定,应用程序无法灵活调整。

3. 为什么使用 Reactor 而不是 Proactor

  1. 跨平台性
    • Reactor 模式依赖于通用的 I/O 多路复用接口(如 epollpoll),这些接口在所有主流操作系统上都有支持。
    • Proactor 模式依赖于操作系统特定的异步 I/O 实现,如 Windows 的 IOCP 和 Linux 的 AIO,跨平台兼容性较差。
  2. 灵活性
    • 在 Reactor 中,应用程序可以完全控制 I/O 操作的细节,比如根据业务需求调整读写的逻辑。
    • Proactor 模式将控制权交给操作系统,应用程序只能处理完成的结果,缺乏灵活性。
  3. 性能与支持
    • 虽然理论上 Proactor 的性能可能更高,但在实际中,Linux 的异步 I/O 支持并不完善(如 POSIX AIO 的效率较低)。
    • Reactor 在 Linux 上结合 epoll 实现时,性能已经非常高,足够应对绝大多数高并发场景。
  4. 复杂性
    • Reactor 的实现虽然对开发者要求较高,但框架(如 libevent、libuv、Boost.Asio 等)已经封装了大部分复杂逻辑,开发者只需关注核心业务逻辑。
    • Proactor 依赖操作系统内核完成 I/O,如果底层实现效率不高,可能引入额外的复杂性和不确定性。

4. 典型应用场景

Reactor

  • 高并发服务器:如 Web 服务器、游戏服务器。
  • 实时通信:如聊天室、实时推送服务。
  • 跨平台应用:如 Nginx 和 Node.js 使用 Reactor。

Proactor

  • I/O 密集型任务:如文件传输、视频流处理。
  • 特定平台:在 Windows 上实现高性能服务器,常使用 IOCP(Proactor 模式)。
  • 异步文件系统操作:需要操作系统直接管理文件 I/O。

5. 总结

特性ReactorProactor
控制权应用程序负责实际 I/O 操作操作系统负责实际 I/O 操作
事件类型事件通知(I/O 就绪)事件通知(I/O 完成)
跨平台性好,支持所有主流操作系统差,依赖于操作系统的支持
性能高,结合 epoll 可处理高并发理论上更高,但依赖实现质量
开发难度较高,需自行处理 I/O 操作细节较低,仅处理完成事件
典型应用Web 服务器(Nginx、Node.js)高性能文件传输(Windows 上)

总结

  • 使用 Reactor 的理由:
    • 跨平台支持更好。
    • 在 Linux 等系统上性能稳定,已被验证。
    • 灵活性更高,适合复杂应用场景。
  • Proactor 的局限:
    • 依赖操作系统支持,跨平台开发困难。
    • 在某些平台(如 Linux)上的实现效率有限。

select怎么用。底层原理

1. 什么是 select

select 是一种 I/O 多路复用机制,允许应用程序同时监听多个文件描述符(FD),以检测它们是否处于可读、可写或异常状态。它适用于处理有限数量的并发连接。


2. select 的使用方法

函数原型

1
2
3
4
#include <sys/select.h>
#include <sys/time.h>

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

参数说明

  1. nfds:
    • 文件描述符集合中最大值加 1(max_fd + 1)。
    • 内核只检测编号为 [0, nfds-1] 的文件描述符。
  2. readfds:
    • 指向一个文件描述符集合,表示需要监视可读事件的文件描述符。
    • 传入 NULL 表示不关心可读事件。
  3. writefds:
    • 指向一个文件描述符集合,表示需要监视可写事件的文件描述符。
    • 传入 NULL 表示不关心可写事件。
  4. exceptfds:
    • 指向一个文件描述符集合,表示需要监视异常事件的文件描述符。
    • 传入 NULL 表示不关心异常事件。
  5. timeout:
    • 超时时间。可以是:
      • NULL:无限期等待。
      • {0, 0}:立即返回。
      • 指定超时时间:阻塞等待至超时。

返回值

  • 正数:表示就绪的文件描述符数量。
  • 0:超时,没有任何文件描述符就绪。
  • -1:出现错误。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>

int main() {
    fd_set readfds;
    struct timeval timeout;
    int fd = 0; // 标准输入

    FD_ZERO(&readfds);         // 清空文件描述符集合
    FD_SET(fd, &readfds);      // 将标准输入加入集合

    timeout.tv_sec = 5;        // 超时时间为 5 秒
    timeout.tv_usec = 0;

    printf("Waiting for input...\n");
    int ret = select(fd + 1, &readfds, NULL, NULL, &timeout);

    if (ret > 0) {
        if (FD_ISSET(fd, &readfds)) {
            char buffer[128];
            read(fd, buffer, sizeof(buffer));
            printf("Input: %s", buffer);
        }
    } else if (ret == 0) {
        printf("Timeout!\n");
    } else {
        perror("select");
    }

    return 0;
}

3. select 的底层原理

3.1 工作流程

  1. 拷贝文件描述符集合
    • 用户空间将 readfdswritefdsexceptfds 集合拷贝到内核空间。
  2. 等待事件
    • 内核会遍历 nfds 指定的文件描述符范围,依次检查它们的状态(是否可读、可写或异常)。
    • 如果没有任何文件描述符就绪,内核会根据 timeout 的值进入阻塞或超时等待。
  3. 事件检测
    • 内核通过轮询每个文件描述符对应的状态位,判断它是否有事件发生。
    • 如果有事件发生,将文件描述符标记为就绪。
  4. 返回就绪文件描述符
    • 内核将检测到的结果拷贝回用户空间,更新 readfdswritefdsexceptfds 集合。

3.2 核心机制

  • 位图表示文件描述符集合
    • fd_set 是一个位图(bitmap),每个位对应一个文件描述符的状态(可读、可写或异常)。
    • 例如,FD_SET(fd, &fdset) 会将 fd 对应的位置为 1。
  • 轮询(Polling)
    • 内核遍历所有的文件描述符,通过检查每个文件描述符的状态位来判断是否有事件发生。
    • 即使只有一个文件描述符就绪,也需要线性扫描所有文件描述符。
  • 内存拷贝
    • 每次调用 selectfd_set 都需要从用户态拷贝到内核态;调用返回时,检测结果又需要从内核态拷贝回用户态。

4. select 的优缺点

优点

  1. 简单易用
    • 接口设计简单,适合快速开发小型 I/O 程序。
  2. 跨平台支持
    • 属于 POSIX 标准,几乎所有主流操作系统都支持。
  3. 适合少量文件描述符
    • 在低并发场景下表现良好,开销可控。

缺点

  1. 性能问题
    • 线性扫描:即使只有一个文件描述符就绪,也需要遍历所有描述符。
    • 内存拷贝:每次调用都需要将文件描述符集合在用户态和内核态之间拷贝。
  2. 文件描述符限制
    • 位图长度通常固定(如 1024 或 2048),导致最大支持的文件描述符数量受限。
  3. 阻塞模型
    • 如果文件描述符集合很大,长时间阻塞在 select 上会拖慢其他任务的执行。

5. 与其他多路复用机制的对比

特性selectpollepoll
文件描述符限制固定大小(如 1024)无限制无限制
事件处理方式轮询所有描述符轮询所有描述符事件驱动,仅处理就绪事件
效率低(线性扫描 + 拷贝)较低(线性扫描)高效(事件驱动 + 无拷贝)
适用场景少量文件描述符中小规模文件描述符大量并发文件描述符

6. 总结

  • select 是最早的多路复用机制,适合简单、低并发场景。
  • 由于性能限制,现代高并发程序中更多使用 epollkqueue
  • 在需要跨平台支持的程序中,select 仍然是一个可靠的选择。

select为什么只能支持1024个。poll和epoll是怎么解决这个问题的。

1. 为什么 select 只能支持 1024 个文件描述符

1.1 核心原因:固定大小的文件描述符集合

  • select 使用 fd_set 数据结构来表示文件描述符集合。
  • fd_set 是一个位图(bitmap),每个位对应一个文件描述符的状态(0 表示未设置,1 表示已设置)。
  • 位图的大小由宏 FD_SETSIZE决定,默认值是 1024。
    • 即位图的长度是 1024 位,最大只能表示文件描述符 0 到 1023。

1.2 如何突破 1024 的限制

  • 用户可以通过在编译时重新定义

    FD_SETSIZE

    来扩展此限制,例如:

    1
    
    #define FD_SETSIZE 2048
    

    但这种方式可能会增加不必要的内存占用,并且不能动态扩展集合大小。

1.3 文件描述符限制的额外影响

  • 即使调整了 FD_SETSIZE,文件描述符集合的大小仍需在每次调用 select 时从用户态复制到内核态,内存拷贝和线性扫描的开销随着文件描述符数量增加而显著增长。

2. pollepoll 如何解决文件描述符限制

2.1 poll 的解决方式

poll 使用了动态大小的数据结构来表示文件描述符集合:

  1. 动态数组

    • poll 不再使用固定大小的位图,而是使用一个动态数组 struct pollfd 表示文件描述符集合。

    • struct pollfd 包括文件描述符和事件信息,每个文件描述符对应一个数组元素。

    • 例如:

      1
      2
      3
      
      struct pollfd fds[10];
      fds[0].fd = sock1;
      fds[0].events = POLLIN;
      
  2. 无硬编码限制

    • 动态数组的大小由用户程序决定,受限于系统的内存容量,而不是固定的常量。
  3. 缺点

    • 每次调用 poll,动态数组仍需从用户态复制到内核态。
    • 内核仍然采用线性扫描检查所有文件描述符的状态,性能不够高效。

2.2 epoll 的解决方式

epoll 从根本上改变了文件描述符集合的管理方式:

  1. 内核管理集合
    • epoll 创建一个 epoll 实例(通过 epoll_create),内核为此分配一个专用的数据结构。
    • 应用程序通过 epoll_ctl 将文件描述符添加、修改或删除到这个集合中。
  2. 红黑树存储文件描述符
    • 内核使用高效的红黑树存储文件描述符集合,不存在固定大小的限制。
    • 红黑树允许快速的插入、删除和查找操作。
  3. 就绪列表(Ready List)
    • 内核还维护一个就绪列表,仅存储发生事件的文件描述符。
    • 应用程序调用 epoll_wait 时,直接从就绪列表获取事件,避免线性扫描所有文件描述符。
  4. 性能优势
    • 无上限:文件描述符集合仅受限于系统的资源(如内存、最大文件描述符数)。
    • 事件驱动epoll 仅处理发生事件的文件描述符,避免了无关文件描述符的检查,性能远高于 pollselect

3. 对比:selectpollepoll 的文件描述符管理

特性selectpollepoll
数据结构位图动态数组红黑树 + 就绪列表
文件描述符限制固定(FD_SETSIZE,默认 1024)无固定限制,取决于内存大小无固定限制,取决于内存大小
性能瓶颈线性扫描 + 拷贝线性扫描 + 拷贝事件驱动,仅处理就绪事件
内存使用固定大小动态分配内核动态管理

4. 总结

为什么 select 有文件描述符限制

  • 因为 select 使用固定大小的位图(由 FD_SETSIZE 定义)来管理文件描述符集合。

pollepoll 的改进

  1. poll
    • 使用动态数组管理文件描述符集合,避免了固定大小的限制。
    • 但仍需线性扫描所有文件描述符,性能没有根本性改进。
  2. epoll
    • 使用红黑树存储文件描述符集合,理论上支持任意数量的文件描述符。
    • 结合事件驱动机制,只处理发生事件的文件描述符,大幅提升性能。

选择建议

  • 如果需要同时监听超过 1024 个文件描述符或追求高性能,pollepoll 是更好的选择,尤其是在高并发场景下推荐使用 epoll
本文由作者按照 CC BY 4.0 进行授权