当前位置: 首页 > article >正文

Linux高级--2.1.2 select poll epoll reactor

1. select 的使用方法

fd_set rdset;
FD_ZERO(&rdset);  // 清空 rdset
rdset = fdset;    // 将 fdset 拷贝到 rdset,准备传给 select
select(maxFd + 1, &rdset, NULL, NULL, NULL);
  • 参数说明
    • maxFd: 被监控的文件描述符中最大的一个。
    • maxFd + 1: 因为文件描述符从 0 开始,需要在最大值基础上加 1。
    • &rdset: 监控读事件的文件描述符集合。
    • NULL: 表示不监控写事件和异常事件。
    • NULL: 表示不设置超时时间,阻塞等待。

2. poll 的使用方法

数据结构
struct pollfd {
    int   fd;         /* 文件描述符 */
    short events;     /* 请求的事件:常用 POLLIN | POLLOUT */
    short revents;    /* 返回的事件 */
};
函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 参数说明
    • fds: 指向一个 pollfd 结构体数组或链表,存放需要监控的文件描述符及其事件。
    • nfds: fds 中的文件描述符数量。
    • timeout: 超时时间(毫秒)。
注意事项
  • events 描述需要监控的事件。
  • revents 返回实际发生的事件。
  • 如果 fds 中的文件描述符需要动态增删,建议使用链表以便管理;若为数组,删除操作会涉及数组元素移动。

3. epoll 的使用方法

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

struct epoll_event {
    uint32_t events;  /* 监控的事件 */
    epoll_data_t data; /* 用户自定义数据 */
};
函数原型
int epoll_create(int size); // size 为历史遗留参数,只需 >0
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
示例代码
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* 设置监听 socket (listen_sock),省略 socket(), bind(), listen() 等代码 */
epollfd = epoll_create(1);
if (epollfd == -1) {
    perror("epoll_create1");
    exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}

for (;;) {
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_wait");
        exit(EXIT_FAILURE);
    }

    for (int n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock, (struct sockaddr *)&addr, &addrlen);
            if (conn_sock == -1) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
            do_use_fd(events[n].data.fd);
        }
    }
}
注意
  • 边缘触发 (ET)
    • 提高性能,避免频繁切换状态。
    • 需要非阻塞的文件描述符,并且在返回 EAGAIN 后继续读取/写入。
  • 监听多个事件
    • epoll_ctl 中可指定 EPOLLIN | EPOLLOUT,避免频繁切换。

4. epoll_ctldata 的作用

数据结构
typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
用途
  1. 数据关联
    • 将文件描述符与用户数据关联,在 epoll_wait 中可以直接访问。
  2. 灵活性
    • 支持存储指针(ptr)、文件描述符(fd)或整数数据(u32/u64)。
  3. 事件处理上下文传递
    • 存储指针时可在事件处理中快速获取上下文信息。
示例

假设有一个自定义结构体 my_data

struct my_data {
    int id;
    // 其他数据
};

// 创建 epoll_event
struct epoll_event ev;
struct my_data *data = malloc(sizeof(struct my_data));
data->id = 123;
ev.events = EPOLLIN;
ev.data.ptr = data;

// 添加文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev);

// 事件触发后处理
int n = epoll_wait(epoll_fd, events, maxevents, timeout);
for (int i = 0; i < n; i++) {
    struct my_data *data = (struct my_data *)events[i].data.ptr;
    printf("触发事件的 ID: %d\n", data->id);
}

总结
方法特点使用场景
select简单、支持多平台,需多次拷贝 fd_set适合少量文件描述符监控
poll动态数组,支持更多事件类型适合动态增加/删除描述符
epoll高效、支持边缘触发,用户可存储上下文信息,适合大量并发场景高性能服务器、并发场景

5.epoll 的优势

1. 性能优势(O(1)复杂度)
  • select 和 poll:
    • 性能随监控的文件描述符数量增加而线性下降(O(n)),因为每次调用时,内核需要遍历整个文件描述符集,检查每一个文件描述符的状态。
    • 当有大量文件描述符(如几千或几万个)时,这种线性扫描会导致性能大幅下降,尤其是当大多数文件描述符可能处于空闲状态时。
  • epoll:
    • epoll 的设计避免了线性扫描,采用事件驱动机制。当文件描述符状态变化时,内核会通知应用程序。
    • 性能与文件描述符数量无关,复杂度为 O(1)。
    • 使用基于回调机制的红黑树管理事件,只有发生事件的文件描述符才会被处理,大大减少了不必要的遍历。
2. 支持大量文件描述符
  • select:
    • 文件描述符集大小有限,在许多系统(如 Linux)上默认最多处理 1024 个文件描述符。虽然可以通过修改系统配置或编译选项增加限制,但仍是额外负担。
  • poll:
    • 无硬性限制,但性能瓶颈仍存在。监控大量文件描述符时,需要对整个文件描述符集进行线性遍历。
  • epoll:
    • 没有硬性限制(受限于系统允许的最大文件描述符数量),且处理大量文件描述符时性能几乎不受影响。
3. 事件驱动机制
  • select 和 poll:
    • 采用轮询方式,每次调用时必须检查所有文件描述符的状态,即使大部分文件描述符没有事件发生,也需要遍历整个列表。
  • epoll:
    • 提供事件通知机制,只有发生事件时,相关文件描述符才会被加入到就绪列表中。只有当事件发生时,epoll 才会处理相应文件描述符,避免不必要的轮询。
4. 持久模式 vs. 边缘触发模式
  • select 和 poll:
    • 都是水平触发(Level-triggered),每次调用都需要检查所有文件描述符的状态,即使事件已被处理完,下次调用时仍然返回。
  • epoll:
    • 支持两种模式:
      1. 水平触发(Level-triggered,默认): 与 select 和 poll 类似,当文件描述符有数据可读时,它会在每次 epoll_wait 时返回,直到数据被完全处理。
      2. 边缘触发(Edge-triggered): 文件描述符从空闲变为有事件时才会通知,不会重复通知。此模式效率高,但要求程序一次性处理所有数据,否则可能丢失事件。
5. 避免频繁的文件描述符集重建
  • select 和 poll:
    • 每次调用都需重新构建文件描述符集,带来额外开销。尤其在大量文件描述符的场景下,这种开销非常显著。
  • epoll:
    • 文件描述符集在调用 epoll_ctl 时构建并持久化,无需每次调用 epoll_wait 时重新构建。只有在增加、删除或修改感兴趣事件时才需调用 epoll_ctl 更改。
6. 内存复制开销
  • select 和 poll:
    • 每次调用时,文件描述符集需从用户空间复制到内核空间,结果再复制回用户空间。这种频繁的内存复制会带来较大开销。
  • epoll:
    • 仅在 epoll_ctl 调用时传递文件描述符信息给内核,epoll_wait 只返回就绪文件描述符,避免频繁的大量内存复制。
总结
  • 性能: epoll 在大量文件描述符场景下表现优越,特别是大多数文件描述符处于空闲状态时。
  • 可扩展性: epoll 可处理大量文件描述符,无硬性限制。
  • 事件驱动: epoll 通过事件通知机制避免了轮询的开销。
  • 模式选择: epoll 支持边缘触发模式,进一步提升效率。

6.epoll 的水平触发和边缘触发配置

1. 水平触发(Level-triggered)
  • epoll 的默认模式。
  • 只要文件描述符处于就绪状态,epoll_wait 每次调用都会返回该文件描述符,直到事件被处理(如数据被读取完毕)。
2. 边缘触发(Edge-triggered)
  • 当文件描述符状态从未就绪变为就绪时,epoll_wait 才会通知。
  • 更加高效,但要求程序能够一次性处理所有就绪数据或事件,避免数据残留在缓冲区中。
配置方法

使用 epoll_ctl 添加或修改文件描述符时,通过设置事件标志启用对应模式。

水平触发示例(默认模式):

struct epoll_event ev;
ev.events = EPOLLIN;  // 监听可读事件(默认水平触发)
ev.data.fd = fd;      // 文件描述符
// 将 fd 添加到 epoll 实例中
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev);

边缘触发示例(启用 EPOLLET):

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // 启用边缘触发模式,并监听可读事件
ev.data.fd = fd;                // 文件描述符
// 将 fd 添加到 epoll 实例中
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev);
注意事项
  1. 边缘触发必须一次性读取所有数据:
    • 若数据未全部读取,后续 epoll_wait 不会再通知该文件描述符有未处理数据。
  2. 使用非阻塞 I/O:
    • 避免因数据不足或缓冲区不足而阻塞程序。
    • 设置文件描述符为非阻塞模式:
      int flags = fcntl(fd, F_GETFL, 0);
      fcntl(fd, F_SETFL, flags | O_NONBLOCK);  // 设置为非阻塞模式
      
总结
  • 水平触发: 默认模式,无需额外配置。
  • 边缘触发: 在 epoll_event 的 events 字段中添加 EPOLLET 标志即可启用。

7. reactor 模式:

1 附件说明:

.c为 epoll改造实现的reactor代码例程  

图片是验证效果

2 reactor设计流程
  1. 基于把网络IO的操作 和 上层业务代码隔离的思想,通过epoll_wait监控所有的listen_fd和client_fd,利用监控的事件类型events(IN、OUT)回调每个fd对应的回调函数。
  2. 基于上面所说,所有的fd都要绑定在一套结构体类型上,这个结构体中有IN事件对应的recv回调函数,有OUT事件对应的send回调函数。还需要定义接收数据和发送数据的缓存区,以及两个缓存区大小的变量。还有fd。
  3. 每次epoll_ctl设置EPOLLIN/EPOLLOUT只能设置一种,因为需要通过每一次的设置触发一次接收或是发送的回调

8.为什么 select 调用之前要把 rdset 清空重置?

在每次调用 select 之前,必须清空并重置 rdset,这是因为 select 会修改传入的文件描述符集(fd_set),以反映哪些文件描述符变得可读、可写或发生了异常。当 select 返回时,传入的 rdset 会只保留那些已经就绪的文件描述符,其它未就绪的文件描述符会被清除。

因此,在每次调用 select 前,需要重置 rdset,使其包含所有需要监控的文件描述符。否则,如果直接使用上一次 select 调用后被修改的 rdset,就会丢失未被清除的文件描述符信息。

简化流程:

  1. fdset 是原始的文件描述符集,包含所有需要监控的描述符(如 sockfd 和各个客户端 clientfd)。
  2. 每次调用 select 前,将 fdset 复制到 rdset(用于可读性检查)。
  3. select 会修改 rdset,清除未准备好的文件描述符,只保留那些就绪的文件描述符。
  4. 为了下一次 select 调用,必须再次用 fdset 重新初始化 rdset,否则上次修改的 rdset 会影响新的结果。

示例代码:

fd_set rdset;
FD_ZERO(&rdset);  // 清空 rdset
rdset = fdset;    // 将 fdset 拷贝到 rdset,准备传给 select
select(maxFd + 1, &rdset, NULL, NULL, NULL);
fd_set 类型

fd_set 是一个结构体类型,用于表示一组文件描述符集,通常与 select 函数配合使用,用于监控文件描述符是否就绪(可读、可写或有异常)。

在 POSIX 标准中,fd_set 通常被实现为一个位图(bit mask),每个文件描述符(整型值)对应一个位,表示该文件描述符是否在集合中。

常用宏:

  • 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 是否在文件描述符集中。

内部实现通常如下:

typedef struct {
    unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(unsigned long))];
} fd_set;
  • fds_bits 是一个位数组,每一位代表一个文件描述符是否处于就绪状态。
  • __FD_SETSIZE 通常是 1024,表示可以同时监控的文件描述符数量。

9.select/poll/epoll 的超时参数是相对时间还是绝对时间?

select超时机制

select 函数的最后一个参数 timeout 是一个 struct timeval* 类型,表示超时时间。这个时间是相对时间,而不是绝对时间,因此系统时间的跳变不会影响 select 的超时行为。

具体说明:

  1. 相对时间struct timeval 定义的是相对时间,表示从调用 select 开始计算的等待时间,是以经过的时间为基准,而不是某个特定的系统时刻。

    struct timeval 结构如下:

    struct timeval {
        long tv_sec;  // 秒
        long tv_usec; // 微秒
    };
    

    select 使用的是相对时间,并不会受到系统时钟跳变的影响。

  2. 系统时间跳变的影响: 因为 select 使用的是相对时间,基于单调计时器(monotonic clock),所以即使系统时间发生跳变(例如系统时间调整或 NTP 时间同步),也不会影响 select 的超时计算。例如,如果设置超时为 5 秒,系统时钟被调整了 1 小时,select 依然会在约 5 秒后返回。

  3. timeout 参数解释

    • 如果 timeout 设置为 NULLselect 将无限等待,直到有文件描述符变为就绪。
    • 如果 timeout 设置为一个 struct timeval 结构指针,则 select 最多等待给定的相对时间。
    • 如果 timeouttv_sectv_usec 都为 0,select 将立即返回,完成一个非阻塞的检查。

示例:

struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;

select(maxfd + 1, &readfds, NULL, NULL, &timeout);

即使系统时间发生跳变,select 依然会在大约 5 秒后超时返回。

pollepoll 的超时机制

pollepoll 的超时机制与 select 类似,它们也使用相对时间,因此系统时间的跳变不会影响它们的超时行为。

  1. poll 的超时机制poll 函数的超时参数是一个以毫秒为单位的整数,表示相对时间,系统时间的跳变不会影响超时行为。

    poll 的原型:

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    
    • timeout:表示等待的相对时间(毫秒)。如果 timeout 是 0,表示立即返回;如果是负数,表示无限期等待。

    示例:

    poll(fds, nfds, 5000);  // 等待 5000 毫秒(5 秒)
    
  2. epoll 的超时机制epoll_wait 的超时参数同样是相对时间,单位是毫秒。系统时间跳变不会影响其超时行为。

    epoll_wait 的原型:

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • timeout:表示相对时间(毫秒)。如果为 -1,表示无限等待;如果为 0,表示立即返回;为正数则等待指定的时间(毫秒)。

    示例:

    epoll_wait(epfd, events, maxevents, 5000);  // 等待 5000 毫秒(5 秒)
    
小结:
  • selectpollepoll 使用相对时间作为超时计算基准。
  • 系统时间的跳变不会影响它们的超时行为,因为它们依赖于单调计时器。

10.平时使用中该怎么选择使用select / poll/ epoll哪个接口

在选择 select、poll 或 epoll 作为 I/O 多路复用的接口时,取决于应用场景的需求和具体系统资源的使用情况。以下是关于如何选择这三者的一些建议:

1. select 适用场景:

select 是最早被引入的 I/O 多路复用机制,使用最广泛,适合以下情况:

  • 少量文件描述符:select 的文件描述符集有硬性限制(通常是 1024),因此适合监控文件描述符数量较少的应用(如几十个)。
  • 跨平台兼容性:如果应用需要在不支持 epoll 的平台上运行(如某些 BSD 系统、macOS 等),select 是一个通用选择。
  • 简单场景:对于一些简单的、少量的 I/O 监控任务,select 足够使用,且其 API 更为广泛被了解。

优势:

  • 兼容性好:几乎所有的 Unix 系统和 Windows 系统都支持。

劣势:

  • 性能较差:当需要监控大量文件描述符时,select 每次都需要线性扫描整个文件描述符集,性能会迅速下降。
  • 文件描述符限制:不能监控超过 1024 个文件描述符,除非手动修改系统配置。
  • 每次调用时需要重建文件描述符集:select 会修改原始的文件描述符集,因此在每次调用前都需要重新设置该集。
2. poll 适用场景:

poll 是对 select 的增强,它消除了 select 的文件描述符数量限制,但其内部实现仍然是线性扫描,因此在大规模场景中仍然性能不足。

  • 适中数量的文件描述符:如果你需要监控的文件描述符数量中等,poll 可以替代 select,因为它没有文件描述符数量的硬限制。
  • 跨平台并支持大规模描述符:poll 在大多数平台上支持且没有 1024 文件描述符的限制,相对适合一些描述符数量介于几十到几百的场景。
  • 复杂 I/O 场景:相比 select,poll 支持更多灵活的事件标志(如 POLLERR、POLLHUP 等),适合需要监听多种事件的场景。

优势:

  • 无硬性文件描述符数量限制
  • 支持更多事件:相较于 select,poll 能处理更多种类的事件,能够更灵活地监控 I/O 状态。

劣势:

  • 性能问题:当文件描述符数量增加时,poll 和 select 一样,每次调用都需要遍历整个文件描述符列表,性能会随着文件描述符数量线性下降。
  • 每次调用仍需要重建文件描述符集:和 select 类似,每次调用 poll 都需要重新设置感兴趣的文件描述符集。
3. epoll 适用场景:

epoll 是 Linux 提供的高性能 I/O 多路复用机制,特别适用于需要处理大量并发连接的大规模应用场景。

  • 大量文件描述符:如果你的应用需要监控几千甚至几万个文件描述符,epoll 是最佳选择。它通过事件驱动的机制,避免了每次都要遍历整个文件描述符集。
  • 长连接场景:如服务器端的网络应用,尤其是需要处理大量空闲连接(如长连接或 WebSocket),epoll 能在这种场景下表现出色,因为它只会在有事件发生时通知,而不是不断轮询空闲的连接。
  • 高性能和低延迟需求:对于一些需要低延迟、高吞吐量的系统,epoll 的边缘触发模式能进一步提高效率,避免无意义的系统调用。

优势:

  • 高效处理大规模并发连接:即使有大量文件描述符,epoll 也能高效处理,避免了 select 和 poll 的线性性能瓶颈。
  • 事件驱动模型:epoll 只在有事件发生时返回,性能更加优越。
  • 无描述符数量限制:epoll 没有文件描述符数量的硬性限制,取决于系统的最大打开文件数(ulimit -n)。

劣势:

  • 仅在 Linux 平台支持:epoll 是 Linux 专有的 API,如果需要跨平台支持,它就不适合。
  • 复杂度稍高:与 select 和 poll 相比,epoll 的 API 需要管理文件描述符的注册、修改、删除,有些应用场景下实现起来可能稍显复杂。
4.选择总结:
  • 选择 select
    • 文件描述符数量少于 1024 个,并且需要跨平台支持。
    • 场景简单,性能要求不高。
    • 几十个描述符的场景
  • 选择 poll
    • 文件描述符数量中等,跨平台需求。
    • 需要处理多种 I/O 事件且不希望受限于 select 的 1024 限制。
    • 几十几百文件描述符的场景
  • 选择 epoll
    • 处理大量文件描述符(成千上万个,如果硬件支持可达百万),并且系统为 Linux。
    • 需要处理高并发的网络服务,且希望性能高效,延迟低。
    • 希望使用事件驱动模型而不是轮询。

应用场景举例:

  • 高性能 Web 服务器或代理服务器(如 Nginx):使用 epoll 处理大规模并发连接。
  • 简单的命令行工具或小型服务器:可以使用 select 或 poll。
  • 跨平台的网络应用:使用 poll 或 select。

未来扩展:

在现代 Linux 系统中,epoll 是处理大规模并发和高性能网络应用的首选。如果你的应用主要在 Linux 环境运行,建议优先考虑 epoll。如果需要跨平台兼容性,poll 会比 select 更适合。


https://github.com/0voice


http://www.kler.cn/a/453094.html

相关文章:

  • MySQL 锁概述
  • 浅谈ORACLE中间件SOA BPM,IDM,OID,UCM,WebcenterPortal服务器如何做迁移切换
  • 【唐叔学算法】第19天:交换排序-冒泡排序与快速排序的深度解析及Java实现
  • Kubernetes 常用的网络插件
  • 《Web 搜索引擎优化》
  • 重温设计模式--观察者模式
  • 中科岩创边坡自动化监测解决方案
  • 34.正则表达式
  • 打包springBoot程序为exe(案例教程)
  • 每天40分玩转Django:实操在线商城
  • Spring Task的使用
  • 小程序canvas画环形百分比进度图
  • uni-app:监听页面返回,禁用返回操作
  • 【数据库初阶】数据库基础知识
  • 无人零售及开源 AI 智能名片 S2B2C 商城小程序的深度剖析
  • 怎么学习数据结构与算法?
  • 【前端实现pdf导出】
  • GESP202309 二级【小杨的 X 字矩阵】题解(AC)
  • 【大语言模型】ACL2024论文-35 WAV2GLOSS:从语音生成插值注解文本
  • Android使用辅助服务AccessibilityService实现自动化任务
  • 力扣11. 盛最多水的容器
  • 【Pytorch实用教程】PyTorch 自带的数据集全面解读
  • 消息队列(一)消息队列的工作流程
  • 地理数据库Telepg面试内容整理-基础技术栈
  • 重温设计模式----装饰模式
  • SSE(Server-Sent Events)返回n ,前端接收数据时被错误的截断【如何避免SSE消息中的换行符或回车符被解释为事件消息的结束】