Linux 网络编程:select、poll 与 epoll 深度解析 —— 从基础到高并发实战

一、IO 多路复用:解决并发 IO 的核心技术

在网络编程中,当需要同时处理大量客户端连接时,传统阻塞式 IO 会导致程序卡在单个操作上,造成资源浪费。IO 多路复用技术允许单线程监听多个文件描述符(FD),当任意 FD 就绪(可读 / 可写 / 异常)时,程序能立即响应,是高效处理并发的关键。
Linux 提供了三种主流实现:selectpoll 和 epoll,其中 epoll 是高并发场景的首选方案。

二、select:经典多路复用接口(适用于小规模并发)

1. 核心原理与数据结构

(1)核心设计思想

select 是 Linux 早期实现的 IO 多路复用接口,通过 位掩码集合 监听多个文件描述符(FD)的可读、可写或异常事件。其核心是将用户空间的 FD 集合复制到内核空间,由内核检测哪些 FD 就绪,最后将就绪状态返回给用户空间。

(2)数据结构:fd_set 位掩码
  • 本质:一个固定大小的位掩码(数组),每一位对应一个 FD。
  • 默认限制:受限于系统宏 FD_SETSIZE(通常为 1024),即最多监听 1024 个 FD(FD 范围:0~1023)。
  • 操作函数
    #include <sys/select.h>  
    void FD_ZERO(fd_set *set);        // 清空集合(所有位设为 0)  
    void FD_SET(int fd, fd_set *set);  // 将 FD 添加到集合(对应位设为 1)  
    void FD_CLR(int fd, fd_set *set);  // 将 FD 从集合移除(对应位设为 0)  
    int  FD_ISSET(int fd, fd_set *set); // 检查 FD 是否在集合中(对应位是否为 1)  
    
(3)核心函数:select
#include <sys/select.h>  
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);  

参数解释
maxfd监听的最大 FD 值 + 1(例如 FD 为 3、5,则 maxfd=6),确保覆盖所有监听的 FD。
readfds可读事件集合(监听哪些 FD 有数据可读)。
writefds可写事件集合(监听哪些 FD 可无阻塞写入,较少使用)。
exceptfds异常事件集合(如带外数据,通常设为 NULL)。
timeout超时时间:
NULL:永久阻塞,直到任意 FD 就绪
{0, 0}:立即返回
{tv_sec, tv_usec}:指定超时时间(秒 + 微秒)
返回值就绪 FD 数量;0 表示超时;-1 表示错误(如被信号中断,errno 查看具体原因)。
(4)触发模式:水平触发(LT, Level Triggered)
  • 核心逻辑:只要 FD 的事件条件满足(如数据可读),就会持续触发事件,直到数据被处理。
  • 示例场景:客户端发送 10KB 数据,select 会多次触发 EPOLLIN 事件,直到数据被完全读取。

2. 使用步骤(监听多个客户端:从初始化到事件处理)

步骤 1:初始化事件集合
fd_set read_fds;  
FD_ZERO(&read_fds);  // 清空集合(必须第一步,避免脏数据)  
FD_SET(server_fd, &read_fds);  // 添加服务器监听 FD(如 socket 描述符)  

  • 关键点:服务器启动时,先将监听套接字(server_fd)加入 readfds,用于检测新客户端连接。
步骤 2:计算 maxfd
int maxfd = server_fd;  // 初始时只有服务器 FD  
// 若有客户端 FD(如 client_fd=5),则更新为 maxfd = client_fd  

  • 为什么 + 1?select 函数需要检测从 0 到 maxfd 的所有 FD,因此传入参数为 maxfd + 1
步骤 3:等待事件就绪(阻塞或超时)
struct timeval timeout = {2, 0};  // 2 秒超时(2 秒内无事件则返回)  
int ready_count = select(maxfd + 1, &read_fds, NULL, NULL, &timeout);  

  • 三种状态
    • ready_count > 0:有 ready_count 个 FD 就绪。
    • ready_count == 0:超时,无事件发生(可用于定时轮询任务)。
    • ready_count == -1:错误(如 EINTR 表示被信号中断,需重新调用)。
步骤 4:遍历检查就绪 FD(线性扫描)
for (int fd = 0; fd <= maxfd; fd++) {  if (FD_ISSET(fd, &read_fds)) {  // 检查 FD 是否在就绪集合中  if (fd == server_fd) {  // 处理新客户端连接(accept)  int client_fd = accept(server_fd, ...);  FD_SET(client_fd, &read_fds);  // 将新客户端 FD 添加到下次监听集合  maxfd = (client_fd > maxfd) ? client_fd : maxfd;  // 更新 maxfd  } else {  // 处理客户端数据(recv)  char buf[1024];  ssize_t recv_len = recv(fd, buf, sizeof(buf), 0);  if (recv_len == 0) {  // 客户端关闭连接,移除 FD  FD_CLR(fd, &read_fds);  close(fd);  }  }  }  
}  

  • 核心缺陷:无论是否就绪,都需从 0 到 maxfd 逐个检查(时间复杂度 O (n)),FD 越多性能越差。

3. 完整示例:select 实现简易 TCP 服务器

#include <sys/select.h>  
#include <sys/socket.h>  
#include <arpa/inet.h>  
#include <unistd.h>  
#include <string.h>  #define PORT 8080  
#define MAX_FD 1024  // 受限于 FD_SETSIZE  int main() {  // 1. 创建服务器套接字  int server_fd = socket(AF_INET, SOCK_STREAM, 0);  struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(PORT), .sin_addr.s_addr = INADDR_ANY};  bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));  listen(server_fd, 5);  fd_set read_fds;  int maxfd = server_fd;  while (1) {  FD_ZERO(&read_fds);  FD_SET(server_fd, &read_fds);  // 2. 添加所有客户端 FD 到集合(假设客户端 FD 存储在数组 clients[] 中)  for (int i = 0; i < MAX_FD; i++) {  int client_fd = clients[i];  if (client_fd > 0) FD_SET(client_fd, &read_fds);  }  // 3. 等待事件(永久阻塞)  int ready = select(maxfd + 1, &read_fds, NULL, NULL, NULL);  if (ready < 0) { perror("select"); continue; }  // 4. 处理就绪 FD  for (int fd = 0; fd <= maxfd; fd++) {  if (FD_ISSET(fd, &read_fds)) {  if (fd == server_fd) {  // 处理新连接  int client_fd = accept(server_fd, NULL, NULL);  clients[client_idx++] = client_fd;  // 假设 clients 是全局数组  maxfd = (client_fd > maxfd) ? client_fd : maxfd;  } else {  // 处理数据接收  char buf[1024];  if (recv(fd, buf, sizeof(buf), 0) <= 0) {  close(fd);  FD_CLR(fd, &read_fds);  // 从集合中移除失效 FD  } else {  send(fd, buf, strlen(buf), 0);  // 简单回显  }  }  }  }  }  close(server_fd);  return 0;  
}  

4. 优缺点对比与适用场景

(1)优点:入门友好,跨平台
  • 跨平台支持:Windows(select)和 Linux 均支持,适合需要跨平台的轻量级程序(如简单代理工具)。
  • 接口简单:仅需操作 fd_set 集合,适合新手快速入门多路复用概念。
  • 小规模场景适用:当 FD 数量较少(如 < 100)时,开发成本低,无需复杂配置。
(2)缺点:性能瓶颈明显
  • FD 数量限制:受 FD_SETSIZE 限制(默认 1024),无法处理大规模并发(如万级连接)。
  • 内核拷贝开销:每次调用 select 都需将整个 fd_set 从用户空间拷贝到内核空间,FD 越多开销越大。
  • 线性扫描效率低:通过 FD_ISSET 逐个检查 FD,时间复杂度为 O (n),高并发时 CPU 占用率飙升。
  • 集合重置麻烦:内核会修改 fd_set 集合(移除未就绪的 FD),每次调用前需重新调用 FD_ZERO/FD_SET 重置。
(3)适用场景
  • 小规模并发:如聊天工具(客户端数量 < 100)、简单日志服务器。
  • 跨平台开发:需要同时支持 Windows 和 Linux 时,select 是唯一选择。
  • 学习阶段:作为理解 IO 多路复用的入门接口,帮助掌握事件驱动基本思想。

5. 错误处理与最佳实践

(1)常见错误码处理
  • EINTRselect 被信号中断(如 SIGINT),可忽略并重新调用。
  • EBADF:集合中包含无效 FD(如已关闭的 FD),需在 FD_ISSET 前检查 FD 有效性。
(2)优化技巧
  • 预分配 FD 数组:用数组存储所有监听的 FD,避免遍历时检查无效 FD(如 FD=0 可能是标准输入)。
  • 限制超时时间:避免永久阻塞(timeout=NULL),可设置短超时(如 1 秒),期间穿插其他任务(如定时心跳)。
(3)新手常见问题
  • Q:为什么每次调用 select 前要重置 fd_set?
    A:内核会修改 fd_set 集合,移除未就绪的 FD,因此下次调用前需重新添加所有监听的 FD。
  • Q:如何监听可写事件?
    A:将目标 FD 添加到 writefds 集合,检测是否可无阻塞写入(如发送缓冲区未满)。

6. 总结:select 的 “利” 与 “弊”

select 作为经典多路复用接口,是理解 IO 并发的重要起点,但其设计缺陷使其在高并发场景中逐渐被淘汰。对于新手,掌握 select 的核心在于理解位掩码集合的操作和水平触发机制,为后续学习 poll 和 epoll 打下基础。在下一节中,我们将对比 poll 接口,了解其如何改进 select 的 FD 数量限制问题。

三、poll:改进的多路复用接口(适用于中规模并发)

1. 核心原理与数据结构

1.1 核心原理

poll 是 Linux 系统中用于实现 I/O 多路复用的系统调用,它改进了 select 存在的一些问题。poll 的核心原理是通过一个 struct pollfd 数组来管理多个文件描述符(FD),并允许内核监听这些文件描述符上的特定事件。当其中任何一个文件描述符上的指定事件发生时,poll 函数会返回,通知程序哪些文件描述符已经就绪。

1.2 数据结构:struct pollfd

poll 使用 struct pollfd 数组来动态管理文件描述符,这个数组没有固定的大小限制,仅受系统资源的约束。以下是 struct pollfd 的定义:

struct pollfd {int fd;            // 文件描述符(-1 表示忽略)short events;      // 监听事件(如 POLLIN 可读)short revents;     // 就绪事件(内核填充)
};

  • fd:要监听的文件描述符。如果设置为 -1,则表示忽略该条目,poll 函数不会对其进行检查。
  • events:指定要监听的事件类型。可以使用按位或(|)运算符组合多个事件。常见的事件类型如下:
    • POLLIN:文件描述符有普通数据可读。例如,对于一个套接字,当有新的数据到达接收缓冲区时,就会触发 POLLIN 事件。
    • POLLOUT:文件描述符可写,即发送缓冲区有空间可以写入数据。比如在网络编程中,当套接字的发送缓冲区有空闲空间时,就会触发 POLLOUT 事件。
    • POLLERR:文件描述符发生错误。这可能是由于网络连接中断、文件损坏等原因导致的。
    • POLLHUP:文件描述符被挂起。例如,在 TCP 连接中,当对方关闭连接时,就会触发 POLLHUP 事件。
    • POLLNVAL:文件描述符无效。可能是因为文件描述符没有被正确打开或者已经被关闭。
    • POLLPRI:文件描述符有紧急数据可读。在网络编程中,紧急数据通常用于带外数据传输,例如 TCP 的紧急指针机制。当有紧急数据到达时,会触发 POLLPRI 事件。
  • revents:由内核填充的实际发生的事件。程序在 poll 函数返回后,可以检查这个字段来确定哪些事件已经发生。
1.3 核心函数:poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  • 参数解释
    • fds:指向 struct pollfd 数组的指针,该数组包含了要监听的文件描述符及其相关事件。
    • nfds:数组中有效元素的数量,即要监听的文件描述符的数量。与 select 不同,poll 不需要计算最大的文件描述符加 1。
    • timeout:超时时间,以毫秒为单位。其取值有以下几种情况:
      • -1:表示永久阻塞,直到有文件描述符上的事件发生。
      • 0:表示立即返回,无论是否有事件发生。
      • 大于 0 的值:表示等待指定的毫秒数,如果在这段时间内没有事件发生,则 poll 函数返回 0。
  • 返回值
    • 大于 0:表示有 n 个文件描述符上的事件发生。
    • 0:表示超时,在指定的时间内没有文件描述符上的事件发生。
    • -1:表示发生错误,错误信息存储在 errno 中。

2. 使用步骤(监听多个客户端)

2.1 初始化 pollfd 数组
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define MAX_FDS 1024int main() {// 创建服务器套接字int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");return 1;}// 绑定地址和端口struct sockaddr_in server_addr = {0};server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(8080);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(server_fd);return 1;}// 监听连接if (listen(server_fd, 5) == -1) {perror("listen");close(server_fd);return 1;}// 初始化 pollfd 数组struct pollfd fds[MAX_FDS];fds[0].fd = server_fd;fds[0].events = POLLIN;int nfds = 1;// 后续代码...
}

  • 解释
    • 首先创建了一个服务器套接字,并将其绑定到指定的地址和端口,然后开始监听连接。
    • 接着定义了一个 struct pollfd 数组 fds,大小为 MAX_FDS
    • 将服务器套接字的文件描述符赋值给 fds[0].fd,并设置要监听的事件为 POLLIN(即有新的连接请求可读)。
    • nfds 表示当前 fds 数组中有效元素的数量,初始值为 1,因为只有服务器套接字被添加到了数组中。
2.2 等待事件就绪
    // ... 前面的代码 ...while (1) {int ready = poll(fds, nfds, 2000);  // 2000 毫秒超时if (ready == -1) {perror("poll");break;} else if (ready == 0) {printf("Timeout, no events occurred.\n");continue;}// 后续代码...}// ... 后面的代码 ...

  • 解释
    • 使用 poll 函数等待事件发生,设置超时时间为 2000 毫秒。
    • 如果 poll 函数返回 -1,表示发生错误,使用 perror 输出错误信息并跳出循环。
    • 如果返回 0,表示超时,在 2000 毫秒内没有文件描述符上的事件发生,打印提示信息并继续下一次循环。
    • 如果返回值大于 0,表示有文件描述符上的事件发生,继续后续的处理。
2.3 遍历检查就绪事件
    // ... 前面的代码 ...for (int i = 0; i < nfds; i++) {if (fds[i].fd == -1 || !(fds[i].revents & POLLIN)) continue;if (fds[i].fd == server_fd) {// 处理新的连接请求struct sockaddr_in client_addr = {0};socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");continue;}// 将新的客户端套接字添加到 pollfd 数组中if (nfds < MAX_FDS) {fds[nfds].fd = client_fd;fds[nfds].events = POLLIN | POLLPRI;nfds++;} else {printf("Too many clients, rejecting new connection.\n");close(client_fd);}} else {// 处理客户端数据char buffer[1024];ssize_t bytes_read;if (fds[i].revents & POLLPRI) {// 处理紧急数据bytes_read = recv(fds[i].fd, buffer, sizeof(buffer), MSG_OOB);if (bytes_read == -1) {perror("recv urgent data");} else {buffer[bytes_read] = '\0';printf("Received urgent data from client: %s\n", buffer);}}if (fds[i].revents & POLLIN) {bytes_read = recv(fds[i].fd, buffer, sizeof(buffer), 0);if (bytes_read == -1) {perror("recv");close(fds[i].fd);fds[i].fd = -1;  // 标记为忽略} else if (bytes_read == 0) {// 客户端关闭连接close(fds[i].fd);fds[i].fd = -1;  // 标记为忽略} else {// 处理接收到的数据buffer[bytes_read] = '\0';printf("Received from client: %s\n", buffer);// 回显数据给客户端send(fds[i].fd, buffer, bytes_read, 0);}}}}// ... 后面的代码 ...

  • 解释
    • 遍历 fds 数组,对于每个元素,首先检查 fds[i].fd 是否为 -1,如果是,则表示该条目被忽略,跳过本次循环。
    • 然后检查 fds[i].revents 是否包含 POLLIN 事件,如果不包含,则表示该文件描述符上没有可读事件发生,跳过本次循环。
    • 如果 fds[i].fd 等于服务器套接字的文件描述符,表示有新的连接请求,使用 accept 函数接受连接,并将新的客户端套接字添加到 fds 数组中,同时更新 nfds 的值。这里监听客户端套接字的 POLLIN 和 POLLPRI 事件。
    • 如果 fds[i].fd 不等于服务器套接字的文件描述符,表示有客户端数据可读或有紧急数据到达。
      • 当 fds[i].revents 包含 POLLPRI 事件时,使用 recv 函数并设置 MSG_OOB 标志来接收紧急数据。
      • 当 fds[i].revents 包含 POLLIN 事件时,使用 recv 函数接收普通数据。
        • 如果 recv 函数返回 -1,表示发生错误,关闭该客户端套接字,并将 fds[i].fd 设置为 -1 以标记为忽略。
        • 如果 recv 函数返回 0,表示客户端关闭了连接,同样关闭该客户端套接字,并将 fds[i].fd 设置为 -1。
        • 如果 recv 函数返回值大于 0,表示成功接收到数据,将数据打印出来,并使用 send 函数将数据回显给客户端。

3. 优缺点

3.1 优点
  • 无 FD 数量硬编码限制:与 select 不同,poll 没有 FD_SETSIZE 这样的硬编码限制,仅受系统资源的约束。这意味着可以处理更多的文件描述符,适用于中规模并发的场景。
  • 事件类型更清晰poll 使用 POLLINPOLLOUTPOLLERRPOLLHUPPOLLNVALPOLLPRI 等明确的事件类型,比 select 使用的 fd_set 位掩码更易于理解和使用。开发者可以更方便地指定要监听的事件类型,并且在处理事件时也更加直观。
3.2 缺点
  • 仍需线性扫描就绪 FDpoll 函数返回后,程序需要遍历 struct pollfd 数组来检查哪些文件描述符上的事件已经发生。这个过程的时间复杂度为 O (n),其中 n 是要监听的文件描述符的数量。当文件描述符数量较多时,线性扫描会消耗较多的 CPU 时间,影响性能。
  • 每次调用需复制 pollfd 数组到内核:与 select 类似,poll 函数在每次调用时都需要将 struct pollfd 数组从用户空间复制到内核空间。虽然 poll 在性能上优于 select,但这种复制操作仍然会带来一定的开销,尤其是在处理大量文件描述符时。

4. 拓展:错误处理和性能优化建议

4.1 错误处理

在使用 poll 函数时,需要对可能出现的错误进行处理。常见的错误情况包括:

  • EBADFfds 数组中包含无效的文件描述符。在使用 poll 之前,应该确保所有的文件描述符都是有效的。
  • EFAULTfds 数组的指针无效,可能是因为指针指向了无效的内存地址。
  • EINTRpoll 函数被信号中断。在这种情况下,可以重新调用 poll 函数继续等待事件。

以下是一个简单的错误处理示例:

    int ready = poll(fds, nfds, 2000);if (ready == -1) {switch (errno) {case EBADF:printf("Invalid file descriptor in fds array.\n");break;case EFAULT:printf("Invalid pointer to fds array.\n");break;case EINTR:printf("Poll was interrupted by a signal, retrying...\n");continue;default:perror("poll");break;}}
4.2 性能优化建议
  • 合理设置超时时间:根据具体的应用场景,合理设置 poll 函数的超时时间。如果设置的超时时间过长,可能会导致程序在没有事件发生时长时间阻塞;如果设置的超时时间过短,可能会导致 poll 函数频繁返回,增加系统开销。
  • 动态管理 pollfd 数组:在实际应用中,可能会有新的文件描述符需要添加到 poll 监听列表中,或者有一些文件描述符不再需要监听。可以动态地管理 struct pollfd 数组,避免不必要的文件描述符被监听,从而减少线性扫描的时间。
  • 结合多线程或多进程:对于高并发的场景,可以结合多线程或多进程来处理 poll 函数返回的就绪事件。每个线程或进程可以负责处理一部分文件描述符,从而提高程序的并发处理能力。

通过以上的学习,你应该对 poll 函数有了更深入的理解,并且能够使用它来实现一个简单的多客户端服务器。在实际应用中,可以根据具体的需求和场景,选择合适的 I/O 多路复用机制。

四、epoll:Linux 高并发终极方案(适用于万级以上连接)

1. 核心原理与数据结构

1.1 核心原理

epoll 是 Linux 内核为处理大批量文件描述符而作了改进的 I/O 多路复用技术。其核心原理是使用一个事件表来记录所有关注的文件描述符及其事件,当有事件发生时,内核会将这些就绪的事件通知给用户空间。epoll 通过红黑树和链表这两种数据结构来高效地管理文件描述符和就绪事件,避免了像 select 和 poll 那样的线性扫描和大量的数据拷贝,从而在高并发场景下表现出卓越的性能。

1.2 数据结构
红黑树

红黑树是一种自平衡的二叉搜索树,epoll 使用红黑树来管理所有监听的文件描述符(FD)。红黑树的特点是插入、删除和查找操作的时间复杂度都是 O(logN),其中 N 是树中节点的数量。在 epoll 中,红黑树的每个节点代表一个被监听的文件描述符,通过红黑树可以快速地添加、删除和修改监听的文件描述符。

链表

链表用于存储就绪事件。当有文件描述符上的事件就绪时,内核会将这些事件添加到链表中。epoll_wait 函数直接从这个链表中获取就绪事件,而不需要像 select 和 poll 那样扫描所有的文件描述符,从而大大提高了效率。

1.3 核心函数
#include <sys/epoll.h>
int epoll_create(int size);           // 创建 epoll 实例(size 为预估 FD 数)
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);

  • epoll_create 函数

    • 功能:创建一个 epoll 实例,并返回一个文件描述符(epfd),用于后续的 epoll 操作。
    • 参数
      • size:该参数在 Linux 2.6.8 之后被忽略,但必须为正数。它最初用于告诉内核需要监听的文件描述符的大致数量。
    • 返回值:成功时返回一个非负的文件描述符,失败时返回 -1,并设置 errno
  • epoll_ctl 函数

    • 功能:用于控制 epoll 实例中的文件描述符,包括添加、修改和删除监听的文件描述符及其事件。
    • 参数
      • epfdepoll 实例的文件描述符,由 epoll_create 函数返回。
      • op:操作类型,有以下三种取值:
        • EPOLL_CTL_ADD:将指定的文件描述符添加到 epoll 实例中,并监听指定的事件。
        • EPOLL_CTL_MOD:修改已经添加到 epoll 实例中的文件描述符的监听事件。
        • EPOLL_CTL_DEL:从 epoll 实例中删除指定的文件描述符。
      • fd:要操作的文件描述符。
      • event:指向 struct epoll_event 结构体的指针,用于指定要监听的事件类型和关联的数据。
    • 返回值:成功时返回 0,失败时返回 -1,并设置 errno
  • epoll_wait 函数

    • 功能:等待 epoll 实例中监听的文件描述符上的事件发生。当有事件发生时,将就绪的事件信息复制到 events 数组中。
    • 参数
      • epfdepoll 实例的文件描述符。
      • events:指向 struct epoll_event 数组的指针,用于存储就绪的事件信息。
      • maxeventsevents 数组的最大元素个数,即最多可以返回的就绪事件数量。
      • timeout:超时时间,以毫秒为单位。取值如下:
        • -1:表示永久阻塞,直到有事件发生。
        • 0:表示立即返回,无论是否有事件发生。
        • 大于 0 的值:表示等待指定的毫秒数,如果在这段时间内没有事件发生,则返回 0。
    • 返回值:返回就绪的事件数量,即 events 数组中有效的元素个数。如果发生错误,返回 -1,并设置 errno
1.4 struct epoll_event 结构体
struct epoll_event {uint32_t events;       // 事件类型(如 EPOLLIN/EPOLLET)epoll_data_t data;     // 存储 FD 或自定义数据
};

  • events:表示要监听的事件类型或已经发生的事件类型。常见的事件类型有:

    • EPOLLIN:文件描述符有数据可读。
    • EPOLLOUT:文件描述符可写。
    • EPOLLERR:文件描述符发生错误。
    • EPOLLHUP:文件描述符被挂起。
    • EPOLLET:设置为边缘触发模式(默认是水平触发模式)。
  • dataepoll_data_t 是一个联合体,用于存储与文件描述符关联的数据。常见的用法是存储文件描述符本身,也可以存储自定义的指针或整数值。

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

2. 触发模式(关键特性)

2.1 水平触发(LT,默认模式)
特点

水平触发(Level Triggered,LT)是 epoll 的默认触发模式。在这种模式下,只要文件描述符上的事件条件满足(例如,有数据可读),就会持续触发相应的事件。也就是说,如果一次没有将数据完全读取完,epoll 会再次触发 EPOLLIN 事件,直到数据被完全读取。这种模式适合处理低速 I/O 操作,因为它允许程序有足够的时间来处理数据。

代码示例
#include <stdio.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>#define BUFFER_SIZE 1024int main() {struct epoll_event event;int fd = STDIN_FILENO;  // 标准输入文件描述符// 创建 epoll 实例int epfd = epoll_create(1);if (epfd == -1) {perror("epoll_create");return 1;}// 设置监听事件为水平触发,数据可读时触发event.events = EPOLLIN;event.data.fd = fd;// 将文件描述符添加到 epoll 实例中if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1) {perror("epoll_ctl");close(epfd);return 1;}struct epoll_event events[1];char buf[BUFFER_SIZE];while (1) {// 等待事件发生int ready = epoll_wait(epfd, events, 1, -1);if (ready == -1) {perror("epoll_wait");break;}if (events[0].events & EPOLLIN) {// 处理数据while (recv(fd, buf, sizeof(buf), 0) > 0) {// 这里可以添加具体的数据处理逻辑printf("Received data: %s\n", buf);memset(buf, 0, sizeof(buf));}}}close(epfd);return 0;
}
2.2 边缘触发(ET,高性能模式)
特点

边缘触发(Edge Triggered,ET)是一种高性能的触发模式。在这种模式下,只有当文件描述符上的事件状态发生变化(例如,有新的数据到达)时,才会触发一次事件。也就是说,一旦事件被触发,程序必须一次性将所有的数据读取完,否则后续即使还有数据,也不会再次触发事件。因此,在边缘触发模式下,文件描述符必须设置为非阻塞模式,以确保能够一次性读取完所有数据。

使用条件

使用边缘触发模式时,文件描述符必须设置为非阻塞模式。可以使用 fcntl 函数来设置文件描述符的属性。

#include <fcntl.h>// 设置文件描述符为非阻塞模式
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {perror("fcntl");return 1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl");return 1;
}
代码示例
#include <stdio.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>#define BUFFER_SIZE 1024int main() {struct epoll_event event;int fd = STDIN_FILENO;  // 标准输入文件描述符// 设置文件描述符为非阻塞模式int flags = fcntl(fd, F_GETFL, 0);if (flags == -1) {perror("fcntl");return 1;}if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl");return 1;}// 创建 epoll 实例int epfd = epoll_create(1);if (epfd == -1) {perror("epoll_create");return 1;}// 设置监听事件为边缘触发,数据可读时触发event.events = EPOLLIN | EPOLLET;event.data.fd = fd;// 将文件描述符添加到 epoll 实例中if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1) {perror("epoll_ctl");close(epfd);return 1;}struct epoll_event events[1];char buf[BUFFER_SIZE];while (1) {// 等待事件发生int ready = epoll_wait(epfd, events, 1, -1);if (ready == -1) {perror("epoll_wait");break;}if (events[0].events & EPOLLIN) {// 处理数据while (1) {ssize_t len = recv(fd, buf, sizeof(buf), 0);if (len == -1 && errno != EAGAIN) {perror("recv");break;} else if (len == -1 && errno == EAGAIN) {// 无数据时退出break;} else if (len == 0) {// 对方关闭连接break;} else {// 处理接收到的数据printf("Received data: %s\n", buf);memset(buf, 0, sizeof(buf));}}}}close(epfd);return 0;
}

3. 使用步骤(高并发服务器)

3.1 创建 epoll 实例
#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define MAX_EVENTS 1024int main() {// 创建服务器套接字int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");return 1;}// 绑定地址和端口struct sockaddr_in server_addr = {0};server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(8080);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(server_fd);return 1;}// 监听连接if (listen(server_fd, 5) == -1) {perror("listen");close(server_fd);return 1;}// 创建 epoll 实例int epfd = epoll_create(1024);if (epfd == -1) {perror("epoll_create");close(server_fd);return 1;}// 后续代码...
}

  • 解释:首先创建一个服务器套接字,并将其绑定到指定的地址和端口,然后开始监听连接。接着使用 epoll_create 函数创建一个 epoll 实例,返回的文件描述符 epfd 用于后续的 epoll 操作。
3.2 添加监听事件
    struct epoll_event event;event.events = EPOLLIN;event.data.fd = server_fd;// 将服务器套接字添加到 epoll 实例中if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &event) == -1) {perror("epoll_ctl");close(epfd);close(server_fd);return 1;}// 后续代码...

  • 解释:创建一个 struct epoll_event 结构体变量 event,设置要监听的事件为 EPOLLIN(即有新的连接请求可读),并将服务器套接字的文件描述符存储在 event.data.fd 中。然后使用 epoll_ctl 函数将服务器套接字添加到 epoll 实例中进行监听。
3.3 等待就绪事件
    struct epoll_event events[MAX_EVENTS];while (1) {// 等待事件发生,永久阻塞int ready = epoll_wait(epfd, events, MAX_EVENTS, -1);if (ready == -1) {perror("epoll_wait");break;}// 后续代码...}close(epfd);close(server_fd);return 0;
}

  • 解释:定义一个 struct epoll_event 数组 events,用于存储就绪的事件信息。使用 epoll_wait 函数等待事件发生,设置超时时间为 -1,表示永久阻塞,直到有事件发生。当有事件发生时,epoll_wait 函数返回就绪的事件数量。
3.4 处理就绪事件
        for (int i = 0; i < ready; i++) {int fd = events[i].data.fd;if (fd == server_fd) {// 处理新连接struct sockaddr_in client_addr = {0};socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");continue;}// 设置客户端套接字为非阻塞模式int flags = fcntl(client_fd, F_GETFL, 0);if (flags == -1) {perror("fcntl");close(client_fd);continue;}if (fcntl(client_fd, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl");close(client_fd);continue;}// 将客户端套接字添加到 epoll 实例中struct epoll_event client_event;client_event.events = EPOLLIN | EPOLLET;  // 边缘触发模式client_event.data.fd = client_fd;if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &client_event) == -1) {perror("epoll_ctl");close(client_fd);}} else {// 处理客户端数据char buffer[1024];while (1) {ssize_t len = recv(fd, buffer, sizeof(buffer), 0);if (len == -1 && errno != EAGAIN) {perror("recv");close(fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);break;} else if (len == -1 && errno == EAGAIN) {// 无数据时退出break;} else if (len == 0) {// 客户端关闭连接close(fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);break;} else {// 处理接收到的数据buffer[len] = '\0';printf("Received from client: %s\n", buffer);// 回显数据给客户端send(fd, buffer, len, 0);}}}}

  • 解释:遍历 events 数组,对于每个就绪的事件,首先获取其关联的文件描述符 fd。如果 fd 等于服务器套接字的文件描述符,表示有新的连接请求,使用 accept 函数接受连接,并将新的客户端套接字设置为非阻塞模式,然后将其添加到 epoll 实例中进行监听。如果 fd 不等于服务器套接字的文件描述符,表示有客户端数据可读,使用 recv 函数接收数据,并根据接收结果进行相应的处理。

4. 优缺点

4.1 优点
  • 高性能epoll 使用红黑树和链表来管理文件描述符和就绪事件,就绪事件通过链表直接返回,时间复杂度接近 O(1)。相比 select 和 poll 的线性扫描,epoll 在处理大量文件描述符时具有明显的性能优势。
  • 无 FD 限制epoll 仅受系统最大打开文件数的限制,默认情况下,系统最大打开文件数为 1024,但可以通过 ulimit -n 命令进行调整。因此,epoll 可以处理万级以上的连接。
  • 灵活触发模式epoll 支持水平触发(LT)和边缘触发(ET)两种模式,开发者可以根据不同的应用场景选择合适的触发模式。边缘触发模式适合处理高速网络 I/O,可以减少事件触发的次数,提高效率。
4.2 缺点
  • 仅 Linux 支持epoll 是 Linux 特有的接口,在 Windows 等其他操作系统上没有对应的实现。如果需要开发跨平台的网络应用程序,需要使用其他的 I/O 多路复用技术。
  • ET 模式需手动处理非阻塞 IO:边缘触发模式要求文件描述符必须设置为非阻塞模式,并且需要手动处理非阻塞 I/O 操作,这增加了代码的复杂度。如果处理不当,可能会导致数据丢失或程序出现异常。

5. 拓展:错误处理和性能优化

5.1 错误处理

在使用 epoll 函数时,需要对可能出现的错误进行处理。常见的错误情况包括:

  • EFAULTevents 数组指针无效,可能是因为指针指向了无效的内存地址。
  • EINTRepoll_wait 函数被信号中断。在这种情况下,可以重新调用 epoll_wait 函数继续等待事件。
  • EBADFepfd 不是一个有效的 epoll 实例文件描述符。
  • EINVALepfd 不是一个 epoll 实例文件描述符,或者 maxevents 小于等于 0。

以下是一个简单的错误处理示例:

    int ready = epoll_wait(epfd, events, MAX_EVENTS, -1);if (ready == -1) {switch (errno) {case EFAULT:printf("Invalid events array pointer.\n");break;case EINTR:printf("epoll_wait was interrupted by a signal, retrying...\n");continue;case EBADF:printf("Invalid epoll instance file descriptor.\n");break;case EINVAL:printf("Invalid epoll instance or maxevents value.\n");break;default:perror("epoll_wait");break;}}
5.2 性能优化
  • 合理设置 maxeventsmaxevents 参数表示 events 数组的最大元素个数,即最多可以返回的就绪事件数量。应该根据实际情况合理设置这个参数,避免设置过大或过小。如果设置过小,可能需要多次调用 epoll_wait 函数才能处理完所有的就绪事件;如果设置过大,会浪费内存空间。
  • 批量处理事件:在处理就绪事件时,可以采用批量处理的方式,减少系统调用的次数。例如,可以将多个客户端的请求合并处理,提高处理效率。
  • 使用线程池:对于高并发的场景,可以使用线程池来处理就绪事件。每个线程负责处理一部分客户端的请求,避免单个线程处理过多的请求导致性能下降。

通过以上的学习,你应该对 epoll 有了更深入的理解,并且能够使用它来实现一个高并发的服务器。在实际应用中,可以根据具体的需求和场景,充分发挥 epoll 的优势,提高网络应用程序的性能。

五、三者核心对比表

特性selectpollepoll
数据结构fd_set(位掩码,固定大小)pollfd 数组(动态)红黑树(管理 FD)+ 链表(就绪事件)
FD 限制FD_SETSIZE(默认 1024)受限于系统 ulimit -n理论无限制(仅受内存影响)
内核操作每次复制 FD 集合到内核每次复制 pollfd 数组到内核仅首次添加 FD 到内核(增量更新)
就绪通知水平触发(LT),线性扫描所有 FD水平触发(LT),扫描就绪 FD支持 LT/ET,直接返回就绪列表
时间复杂度O(n)O(n)O (1)(仅处理就绪事件)
内存拷贝高(每次 select 全量拷贝)中(每次 poll 全量拷贝)低(仅 epoll_ctl 时增量拷贝)
跨平台支持(Windows/Linux)仅 Linux/UNIX 支持仅 Linux 支持
典型场景小规模并发(FD < 1024)中规模并发(FD 中等数量)高并发(FD 万级以上)

六、如何选择?场景化决策指南

1. 小规模并发(FD < 100,跨平台需求)

  • 选 select:接口简单,无需复杂配置,适合入门学习或轻量级应用(如简单代理工具)。

2. 中规模并发(100 ≤ FD ≤ 1000,Linux 平台)

  • 选 poll:突破 FD_SETSIZE 限制,事件类型更清晰,适合中等并发场景(如中小型服务器)。

3. 高并发(FD > 1000,Linux 平台)

  • 选 epoll
    • LT 模式:代码简单,适合低速 IO 或对实时性要求不高的场景(如日志服务器)。
    • ET 模式:搭配非阻塞 IO,适合高速网络 IO(如 Web 服务器、即时通讯系统),需注意一次性读取所有数据。

4. 性能优化建议

  • epoll 最佳实践
    • 对高频读写的 FD 使用 ET 模式,减少事件触发次数。
    • 设置 EPOLLONESHOT 避免重复处理同一事件(适合状态机模型)。
    • 调整系统参数:ulimit -n 65535 提高最大打开文件数,优化内核 TCP 缓冲区。

七、总结:从基础到高阶的技术演进

  1. select:入门级多路复用,适合小规模、跨平台场景。
  2. poll:Linux 平台中规模并发的过渡方案,解决 FD 数量限制。
  3. epoll:Linux 高并发的终极选择,通过红黑树和事件链表实现高效事件管理,是 Nginx、Redis 等高性能框架的底层核心。

掌握这三种机制的原理与适用场景,能帮助开发者在不同项目中选择最优方案,从基础网络编程逐步进阶到高并发系统设计。实际开发中,建议优先使用 epoll(Linux 平台),并结合非阻塞 IO 和线程池技术,打造高性能网络应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/80078.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

制作你的时间管理“局”#自制软件,5款AI编程对比测试

玩 AI 编程最有意思的地方&#xff0c;就是当你有想法的时候&#xff0c;可以随时测试、把想法具体化&#xff0c;甚至产品化。今天我们制作一个事件管理器&#xff0c;用来量化我们每天的时间安排&#xff0c;提高时间的利用率&#xff0c;提升生产力。 同样的一组 prompt &am…

大数据系列 | 详解基于Zookeeper或ClickHouse Keeper的ClickHouse集群部署--完结

大数据系列 | 详解基于Zookeeper或ClickHouse Keeper的ClickHouse集群部署 1. ClickHouse与MySQL的区别2. 在群集的所有机器上安装ClickHouse服务端2.1. 在线安装clickhouse2.2. 离线安装clickhouse 3. ClickHouse Keeper/Zookeeper集群安装4. 在配置文件中设置集群配置5. 在每…

宏碁笔记本电脑怎样开启/关闭触摸板

使用快捷键&#xff1a;大多数宏碁笔记本可以使用 “FnF7” 或 “FnF8” 组合键来开启或关闭触摸板&#xff0c;部分型号可能是 “FnF2”“FnF9” 等。如果不确定&#xff0c;可以查看键盘上的功能键图标&#xff0c;一般有触摸板图案的按键就是触摸板的快捷键。通过设备管理器…

使用Mybaitis-plus提供的各种的免写SQL的Wrapper的使用方式

文章目录 内连接JoinWrappers.lambda和 new MPJLambdaWrapper 生成的MPJLambdaWrapper对象有啥区别&#xff1f;LambdaQueryWrapper 和 QueryWrapper的区别&#xff1f;LambdaQueryWrapper和MPJLambdaQueryWrapper的区别&#xff1f;在作单表更新时建议使用&#xff1a;LambdaU…

基于微信小程序的走失儿童帮助系统-项目分享

基于微信小程序的走失儿童帮助系统-项目分享 项目介绍项目摘要管理员功能图用户功能图系统功能图项目预览首页走失儿童个人中心走失儿童管理 最后 项目介绍 使用者&#xff1a;管理员、用户 开发技术&#xff1a;MySQLJavaSpringBootVue 项目摘要 本系统采用微信小程序进行开…

P3916 图的遍历

P3916 图的遍历 题目来源-洛谷 题意 有向图中&#xff0c;找出每个节点能访问到的最大的节点 思路 每个节点的最大节点&#xff0c;不是最长距离&#xff0c;如果是每个节点都用dfs去找最大值&#xff0c;显然1e6*1e6 超时了&#xff0c;只能60分从第一个节点开始遍历&…

掌握常见 HTTP 方法:GET、POST、PUT 到 CONNECT 全面梳理

今天面试还问了除了 get 和 post 方法还有其他请求方法吗&#xff0c;一个都不知道&#xff0c;这里记录下。 &#x1f310; 常见 HTTP 请求方法一览 方法作用描述是否幂等是否常用GET获取资源&#xff0c;参数一般拼接在 URL 中✅ 是✅ 常用POST创建资源 / 提交数据&#xff…

裸金属服务器的应用场景有哪些?

随着云计算技术不断发展&#xff0c;裸金属服务器作为一台既具有传统物理服务器特点的硬件设备&#xff0c;还具备云计算技术的服务器化服务功能&#xff0c;是硬件和软件相结合的网络设备&#xff0c;逐渐被越来越多的企业所关注&#xff0c;那么&#xff0c;裸金属服务器的应…

【得物】20250419笔试算法题

文章目录 前言第一题1. 题目描述2. 思路解析3. AC代码 第二题1. 题目描述2. 思路解析3. AC代码 第三题1. 题目描述2. 思路解析3. AC代码 前言 三道题目都比较简单&#xff0c;大家都可以试着做一下。 第一题 1. 题目描述 题目链接&#xff1a;矩阵变换 2. 思路解析 按题…

明远智睿2351开发板四核1.4G Linux处理器:驱动创新的引擎

在科技日新月异的今天&#xff0c;创新成为了推动社会进步的核心动力。而在这场创新的浪潮中&#xff0c;一款性能卓越、功能全面的处理器无疑是不可或缺的引擎。今天&#xff0c;我们介绍的这款四核1.4G处理器搭配Linux系统的组合&#xff0c;正是这样一款能够驱动未来创新的强…

Oracle Database Resident Connection Pooling (DRCP) 白皮书阅读笔记

本文为“Extreme Oracle Database Connection Scalability with Database Resident Connection Pooling (DRCP)”的中文翻译加阅读笔记。觉得是重点的就用粗体表示了。 白皮书版本为March 2025, Version 3.3&#xff0c;副标题为&#xff1a;Optimizing Oracle Database resou…

VS Code + GitHub:高效开发工作流指南

目录 一、安装 & 基本配置 1.下载 VS Code 2.安装推荐插件(打开侧边栏 Extensions) 3.设置中文界面(可选) 二、使用 VS Code 操作 Git/GitHub 1.基本 Git 操作(不输命令行!) 2.连接 GitHub(第一次使用) 三、克隆远程仓库到 VS Code 方法一(推荐): 方…

【LLM】llama.cpp:合并 GGUF 模型分片

GGUF&#xff08;GPT-Generated Unified Format&#xff09;是一种专为大规模语言模型设计的二进制文件格式&#xff0c;支持将模型分割成多个分片&#xff08;*-of-*.gguf&#xff09;。当从开源社区&#xff08;如 HuggingFace 或 ModelScope&#xff09;下载量化模型时&…

Ubuntu 系统下安装和使用性能分析工具 perf

在 Ubuntu 系统下安装和使用性能分析工具 perf 的步骤如下&#xff1a; 1. 安装 perf perf 是 Linux 内核的一部分&#xff0c;通常通过安装 linux-tools 包获取&#xff1a; # 更新软件包列表 sudo apt update# 安装 perf&#xff08;根据当前内核版本自动匹配&#xff09; …

Buffer of Thoughts: Thought-Augmented Reasoningwith Large Language Models

CODE: NeurIPS 2024 https://github.com/YangLing0818/buffer-of-thought-llm Abstract 我们介绍了思想缓冲(BoT)&#xff0c;一种新颖而通用的思想增强推理方法&#xff0c;用于提高大型语言模型(大型语言模型)的准确性、效率和鲁棒性。具体来说&#xff0c;我们提出了元缓冲…

Java面试中问单例模式如何回答

1. 什么是单例模式? 单例模式(Singleton Pattern)是一种设计模式,确保某个类在整个应用中只有一个实例,并且提供全局访问点。它有以下特点: 确保只有一个实例。提供全局访问点。防止多次实例化,节约资源。2. 如何实现单例模式? 单例模式有多种实现方式,以下是最常见…

实战华为1:1方式1 to 1 VLAN映射

本文摘自笔者于2024年出版&#xff0c;并得到广泛读者认可&#xff0c;已多次重印的《华为HCIP-Datacom路由交换学习指南》。 华为设备的1 to 1 VLAN映射有1:1和N :1两种方式。1:1方式是将指定的一个用户私网VLAN标签映射为一个公网VLAN标签&#xff0c;是一种一对一的映射关系…

认识Vue

认识Vue 文章目录 认识Vue一、vue是什么二、Vue核心特性数据驱动&#xff08;MVVM)组件化指令系统 三、Vue跟传统开发的区别1. **开发模式&#xff1a;MVVM vs 模板驱动**2. **组件化开发**3. **状态管理**4. **路由管理**5. **构建与工程化**6. **性能优化**7. **学习曲线**8.…

iOS中使用AWS上传zip文件到Minio上的oss平台上

1. 集成AWS相关库&#xff08;千万不要用最新的版本&#xff0c;否则会出现风格化虚拟路径&#xff0c;找不到主机名&#xff09; pod AWSS3, ~> 2.10.0 pod AWSCore, ~> 2.10.0 2. 编写集成的相关代码 - (void)uploadFileToMinIO {NSString *endPoint "http://…

usb2.0的硬件知识(一)

一、USB2.0的硬件知识 1.1 USB2.0速率 USB 2.0协议支持3种速率&#xff1a;低速(Low Speed&#xff0c;1.5Mbps)、全速(Full Speed, 12Mbps)、高速(High Speed, 480Mbps)&#xff1b;USB Hub、USB设备&#xff0c;也分为低速、全速、高速三种类型。 1.2 USB2.0硬件线序组成 U…