首页 云计算

Linux 网络 I/O 多路复用:Poll 与 Epoll 性能对比与实战优化

分类:云计算
字数: (4382)
阅读: (7520)
内容摘要:Linux 网络 I/O 多路复用:Poll 与 Epoll 性能对比与实战优化,

在高并发的 Linux 网络编程中,如何高效地处理大量并发连接是一个核心问题。pollepoll 都是 Linux 提供的 I/O 多路复用机制,它们允许一个进程同时监听多个文件描述符,并在其中任何一个文件描述符准备好进行 I/O 操作时通知进程。选择合适的 I/O 模型对于应用程序的性能至关重要,尤其是在处理高并发场景下的网络请求,例如使用 Nginx 反向代理服务器处理大量客户端请求,需要根据具体的并发连接数和请求模式来优化性能。

阻塞 I/O 的局限性

在传统的阻塞 I/O 模型中,每个客户端连接都需要创建一个线程或进程来处理。当客户端数量增加时,系统资源消耗迅速增长,导致性能瓶颈。此外,线程/进程切换也会带来额外的开销。

I/O 多路复用的优势

I/O 多路复用允许单个线程同时监听多个文件描述符(例如 socket),避免了为每个连接创建线程/进程的开销。当某个文件描述符就绪时,pollepoll 会通知应用程序,应用程序可以立即处理该文件描述符上的 I/O 操作。

Linux 网络 I/O 多路复用:Poll 与 Epoll 性能对比与实战优化

Poll 的原理与使用

poll 系统调用通过轮询(polling)所有注册的文件描述符来检查其状态。它维护一个 pollfd 结构体数组,每个 pollfd 结构体包含一个文件描述符和一个事件掩码。

struct pollfd {
 int fd; /* 文件描述符 */
 short events; /* 关注的事件 */
 short revents; /* 实际发生的事件 */
};

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

events 字段指定了应用程序关心的事件,例如 POLLIN(可读)、POLLOUT(可写)等。revents 字段由内核填充,表示实际发生的事件。

Linux 网络 I/O 多路复用:Poll 与 Epoll 性能对比与实战优化

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <poll.h>
#include <unistd.h>
#include <string.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
 int server_fd, new_socket, i;
 struct sockaddr_in address;
 int addrlen = sizeof(address);
 struct pollfd fds[MAX_CLIENTS + 1]; // +1 for server socket
 char buffer[1024] = {0};

 // 创建 socket
 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
 perror("socket failed");
 exit(EXIT_FAILURE);
 }

 address.sin_family = AF_INET;
 address.sin_addr.s_addr = INADDR_ANY;
 address.sin_port = htons(PORT);

 // 绑定 socket
 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
 perror("bind failed");
 exit(EXIT_FAILURE);
 }

 // 监听 socket
 if (listen(server_fd, 3) < 0) {
 perror("listen failed");
 exit(EXIT_FAILURE);
 }

 // 初始化 pollfd 数组
 fds[0].fd = server_fd; // 服务器socket
 fds[0].events = POLLIN; // 监听可读事件
 for (i = 1; i <= MAX_CLIENTS; i++) {
 fds[i].fd = -1; // 初始化为-1,表示空闲
 }

 printf("Server listening on port %d\n", PORT);

 while (1) {
 int ready = poll(fds, MAX_CLIENTS + 1, -1); // -1 表示无限期阻塞

 if (ready < 0) {
 perror("poll failed");
 exit(EXIT_FAILURE);
 }

 if (fds[0].revents & POLLIN) { // 新的连接请求
 if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
 perror("accept failed");
 exit(EXIT_FAILURE);
 }

 printf("New connection, socket fd is %d, address: %s:%d \n", new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));

 // 将新的 socket 添加到 pollfd 数组中
 for (i = 1; i <= MAX_CLIENTS; i++) {
 if (fds[i].fd == -1) {
 fds[i].fd = new_socket;
 fds[i].events = POLLIN;
 break;
 }
 }
 if (i > MAX_CLIENTS) {
 printf("Too many clients!\n");
 close(new_socket); // 关闭连接
 }
 }

 // 处理已连接的 socket
 for (i = 1; i <= MAX_CLIENTS; i++) {
 if (fds[i].fd > 0 && (fds[i].revents & POLLIN)) {
 memset(buffer, 0, sizeof(buffer));
 int valread = read(fds[i].fd, buffer, 1024);
 if (valread == 0) { // 客户端关闭连接
 printf("Client disconnected, socket fd %d \n", fds[i].fd);
 close(fds[i].fd);
 fds[i].fd = -1; // 重置 pollfd
 } else if (valread < 0) {
 perror("read failed");
 close(fds[i].fd);
 fds[i].fd = -1; // 重置 pollfd
 } else {
 printf("Received: %s from socket %d\n", buffer, fds[i].fd);
 send(fds[i].fd, buffer, strlen(buffer), 0);
 }
 }
 }
 }

 return 0;
}

Epoll 的原理与使用

epoll 是一种更高效的 I/O 多路复用机制。与 poll 不同,epoll 使用事件驱动的方式,只有在文件描述符状态发生变化时才会通知应用程序。epoll 主要涉及三个系统调用:

Linux 网络 I/O 多路复用:Poll 与 Epoll 性能对比与实战优化
  1. epoll_create():创建一个 epoll 实例,返回一个文件描述符。
  2. epoll_ctl():向 epoll 实例中添加、修改或删除文件描述符及其关联的事件。
  3. epoll_wait():等待 epoll 实例上的事件发生。
int epoll_create(int size);
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);

struct epoll_event {
 uint32_t events; /* Epoll 事件 */
 epoll_data_t data; /* 用户数据 */
};

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

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

#define PORT 8080
#define MAX_EVENTS 10

int main() {
 int server_fd, new_socket, epoll_fd, i;
 struct sockaddr_in address;
 int addrlen = sizeof(address);
 struct epoll_event event, events[MAX_EVENTS];
 char buffer[1024] = {0};

 // 创建 socket
 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
 perror("socket failed");
 exit(EXIT_FAILURE);
 }

 // 设置 socket 为非阻塞
 int flags = fcntl(server_fd, F_GETFL, 0);
 if (fcntl(server_fd, F_SETFL, flags | O_NONBLOCK) < 0) {
 perror("fcntl failed");
 exit(EXIT_FAILURE);
 }

 address.sin_family = AF_INET;
 address.sin_addr.s_addr = INADDR_ANY;
 address.sin_port = htons(PORT);

 // 绑定 socket
 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
 perror("bind failed");
 exit(EXIT_FAILURE);
 }

 // 监听 socket
 if (listen(server_fd, 3) < 0) {
 perror("listen failed");
 exit(EXIT_FAILURE);
 }

 // 创建 epoll 实例
 if ((epoll_fd = epoll_create1(0)) < 0) {
 perror("epoll_create1 failed");
 exit(EXIT_FAILURE);
 }

 event.events = EPOLLIN; // 监听可读事件
 event.data.fd = server_fd;

 // 将服务器 socket 添加到 epoll 实例中
 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) {
 perror("epoll_ctl failed");
 exit(EXIT_FAILURE);
 }

 printf("Server listening on port %d\n", PORT);

 while (1) {
 int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); // -1 表示无限期阻塞

 if (nfds < 0) {
 perror("epoll_wait failed");
 exit(EXIT_FAILURE);
 }

 for (i = 0; i < nfds; i++) {
 if (events[i].data.fd == server_fd) { // 新的连接请求
 while ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) > 0) {
 printf("New connection, socket fd is %d, address: %s:%d \n", new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));

 // 设置新的 socket 为非阻塞
 flags = fcntl(new_socket, F_GETFL, 0);
 if (fcntl(new_socket, F_SETFL, flags | O_NONBLOCK) < 0) {
 perror("fcntl failed");
 close(new_socket);
 continue;
 }

 event.events = EPOLLIN | EPOLLET; // 边缘触发模式
 event.data.fd = new_socket;

 // 将新的 socket 添加到 epoll 实例中
 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event) < 0) {
 perror("epoll_ctl failed");
 close(new_socket);
 }
 }
 if(errno != EAGAIN && errno != EWOULDBLOCK){
 perror("accept failed");
 }
 } else { // 已连接的 socket 有数据可读
 int fd = events[i].data.fd;
 memset(buffer, 0, sizeof(buffer));
 int valread = read(fd, buffer, 1024);
 if (valread == 0) { // 客户端关闭连接
 printf("Client disconnected, socket fd %d \n", fd);
 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); // 从 epoll 实例中移除
 close(fd);
 } else if (valread < 0) {
 perror("read failed");
 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); // 从 epoll 实例中移除
 close(fd);
 } else {
 printf("Received: %s from socket %d\n", buffer, fd);
 send(fd, buffer, strlen(buffer), 0);
 }
 }
 }
 }

 close(server_fd);
 close(epoll_fd);
 return 0;
}

Poll 与 Epoll 的性能对比

  • Poll: 每次调用 poll 都会轮询所有注册的文件描述符,时间复杂度为 O(n),其中 n 是文件描述符的数量。因此,当文件描述符数量很大时,poll 的性能会显著下降。
  • Epoll: 基于事件驱动,只有在文件描述符状态发生变化时才会通知应用程序。epoll 使用红黑树等数据结构来维护文件描述符,插入、删除和查找的时间复杂度为 O(log n)。在大量并发连接的情况下,epoll 的性能优于 poll

适用场景:

Linux 网络 I/O 多路复用:Poll 与 Epoll 性能对比与实战优化
  • Poll: 适用于连接数较少,且活动连接占比较高的情况。
  • Epoll: 适用于连接数较多,但活动连接占比较低的情况。例如,高并发的 Web 服务器,如 Nginx,通常使用 epoll 来处理客户端连接。在使用宝塔面板等工具简化服务器管理时,仍然需要理解底层 I/O 模型的选择对服务器性能的影响。

实战避坑经验总结

  1. 选择合适的触发模式: epoll 支持边缘触发(ET)和水平触发(LT)两种模式。ET 模式只在文件描述符状态发生变化时通知应用程序,因此需要一次性读取所有数据,否则可能会丢失事件。LT 模式只要文件描述符可读/可写,就会一直通知应用程序,相对简单,但效率较低。选择合适的触发模式需要根据应用程序的需求进行权衡。
  2. 避免惊群效应: 在多线程/多进程环境下,多个线程/进程可能同时被唤醒处理同一个事件,导致资源竞争。可以使用 EPOLLONESHOT 事件来避免惊群效应。
  3. 合理设置 timeout:pollepoll_wait 中设置合适的 timeout 值,可以避免程序长时间阻塞。
  4. 文件描述符泄露: 确保在使用完文件描述符后及时关闭,避免文件描述符泄露。

理解 pollepoll 的原理和使用方法,可以帮助我们编写出更高效、更稳定的网络应用程序,更好地应对高并发场景下的挑战。在实际应用中,要根据具体的业务场景和性能需求,选择合适的 I/O 多路复用机制,并进行相应的优化。

Linux 网络 I/O 多路复用:Poll 与 Epoll 性能对比与实战优化

转载请注明出处: GC触发器

本文的链接地址: http://m.acea1.store/blog/028986.SHTML

本文最后 发布于2026-04-07 02:45:17,已经过了20天没有更新,若内容或图片 失效,请留言反馈

()
您可能对以下文章感兴趣
评论
  • 绿茶观察员 4 天前
    写得真不错,把 poll 和 epoll 的原理讲得很清楚,代码示例也很完整,点赞!
  • 沙县小吃 5 天前
    感谢分享,最近正好在研究这块,mark一下慢慢看,学习了!