poll 和 epoll 是 Linux 系统中用于实现 I/O 多路复用 的系统调用,用于高效管理多个文件描述符(如网络套接字)的 I/O 事件(可读、可写、错误等),是高并发网络编程的核心技术。两者解决的问题相似,但在性能和设计上有显著差异,以下是详细对比:
一、I/O 多路复用的核心问题
在网络编程中,单个进程 / 线程需要同时处理多个网络连接(如服务器同时应对成百上千个客户端)。传统的 “一连接一线程” 模型会导致资源耗尽(线程栈、上下文切换开销),而 I/O 多路复用 允许单个进程 / 线程通过一个系统调用同时监控多个文件描述符(fd),仅当某个 fd 有 I/O 事件发生时才进行处理,大幅提升效率。
poll 和 epoll 都是实现这一机制的工具,替代了更早的 select,但 epoll 是 Linux 特有的优化版本。
二、poll 机制
poll 是 POSIX 标准定义的系统调用,在 Linux、Unix 等系统中通用,功能上是 select 的改进版。
1. 核心原理
-
数据结构:使用
struct pollfd数组描述需要监控的文件描述符及事件:struct pollfd { int fd; // 要监控的文件描述符 short events; // 关注的事件(如 POLLIN 表示可读,POLLOUT 表示可写) short revents; // 实际发生的事件(由内核填充) }; -
调用流程:
-
应用程序初始化
pollfd数组,设置需要监控的 fd 和事件。 -
调用
poll系统调用,阻塞等待事件发生:int poll(struct pollfd *fds, nfds_t nfds, int timeout);fds:监控的 fd 数组;nfds:数组长度;timeout:超时时间(毫秒,-1 表示永久阻塞)。
-
内核遍历所有 fd,检查是否有事件发生,将结果写入
revents并返回就绪的 fd 数量。 -
应用程序遍历
pollfd数组,根据revents处理就绪的 fd。
-
2. 优缺点
-
优点:
- 突破
select对 fd 数量的限制(select受FD_SETSIZE限制,通常为 1024),poll仅受系统文件描述符上限限制。 - 无需每次调用都重置监控事件(
select的fd_set会被内核修改,需重新初始化)。
- 突破
-
缺点:
- 效率低:每次调用
poll时,内核需遍历整个pollfd数组检查事件,当 fd 数量庞大(如上万)时,遍历开销显著。 - 无事件就绪通知机制:应用程序需遍历整个数组才能找到就绪的 fd,进一步增加开销。
- 水平触发(LT)模式:只要 fd 有未处理的数据,就会持续触发事件(可能导致不必要的调用)。
- 效率低:每次调用
三、epoll 机制
epoll 是 Linux 2.6 内核引入的 I/O 多路复用机制,专为高并发场景设计,性能远超 poll 和 select。
1. 核心原理
epoll 通过三个系统调用实现,引入了 “事件表” 和 “就绪队列” 的设计,避免了 poll 的遍历开销:
epoll_create:创建一个 epoll 实例(事件表),返回一个管理 fd。epoll_ctl:向事件表中添加、修改或删除需要监控的 fd 及事件。epoll_wait:等待事件发生,返回就绪的 fd 列表。
关键设计:
- 事件表:内核维护一个红黑树存储所有注册的 fd 和事件,支持高效的增删改操作(O (log n))。
- 就绪队列:内核维护一个双向链表,当 fd 有事件发生时,自动加入该队列,
epoll_wait直接返回就绪队列中的 fd,无需遍历所有注册的 fd。
2. 触发模式
epoll 支持两种事件触发模式,可根据场景选择:
- 水平触发(Level Trigger,LT) :
只要 fd 中还有未处理的数据(如可读缓冲区非空),就会持续触发事件。优点是编程简单(无需一次性处理完所有数据),缺点是可能有冗余通知。 - 边缘触发(Edge Trigger,ET) :
仅在 fd 状态发生变化时触发一次(如从不可读变为可读)。优点是通知次数少,效率高;缺点是必须一次性处理完所有数据(否则可能遗漏事件),编程复杂(需配合非阻塞 I/O)。
3. 调用流程
// 1. 创建 epoll 实例
int epfd = epoll_create1(0);
// 2. 注册需要监控的 fd 和事件(如监听可读事件)
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边缘触发模式的可读事件
ev.data.fd = sockfd; // 关联的文件描述符
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
// 3. 等待事件发生
struct epoll_event events[1024];
int n = epoll_wait(epfd, events, 1024, -1);
// 4. 处理就绪事件
for (int i = 0; i < n; i++) {
if (events[i].events & EPOLLIN) {
// 处理可读事件(如读取客户端数据)
}
}
4. 优缺点
-
优点:
- 高效性:内核通过红黑树管理注册的 fd,通过就绪队列直接返回就绪事件,避免遍历所有 fd,适合大量 fd 场景(万级以上)。
- 灵活的触发模式:支持 LT 和 ET,ET 模式可减少系统调用次数,提升性能。
- 无 fd 数量限制:仅受系统内存和文件描述符上限限制。
-
缺点:
- Linux 特有:不支持 Windows、BSD 等其他系统(移植性差)。
- ET 模式编程复杂:需确保一次性处理完所有数据,否则会丢失事件。
四、poll 与 epoll 的核心差异
| 对比维度 | poll | epoll |
|---|---|---|
| 适用场景 | 连接数较少(千级以下)的场景 | 高并发(万级以上连接)场景 |
| 事件查找方式 | 每次调用遍历所有注册的 fd | 直接返回就绪队列中的 fd,无需遍历 |
| 时间复杂度 | O (n)(n 为注册的 fd 总数) | O (1)(获取就绪事件)+ O (log n)(增删改) |
| 触发模式 | 仅支持水平触发(LT) | 支持 LT 和边缘触发(ET) |
| 系统调用次数 | 每次等待事件都需传入完整的 fd 列表 | 注册 / 修改 fd 时调用 epoll_ctl,等待时无需重复传入 |
| 移植性 | 跨平台(Linux、Unix、BSD 等) | 仅 Linux 支持 |
五、如何选择?
- 中小规模连接(<1000) :
poll足够用,且移植性更好(如需要跨平台)。 - 高并发场景(>10000) :优先用
epoll,尤其是 ET 模式,可显著降低系统开销(如 Nginx、Redis 等高性能服务器均采用epoll)。 - 跨平台需求:若需支持 Windows 或 macOS,可考虑
kqueue(BSD/macOS)或IOCP(Windows),或使用封装库(如 libevent、libuv)屏蔽底层差异。
总结
poll 是对 select 的改进,解决了 fd 数量限制,但仍存在遍历开销;epoll 是 Linux 为高并发设计的优化方案,通过事件表和就绪队列实现高效 I/O 多路复用,是高性能网络服务器的首选。理解两者的差异,有助于在实际开发中根据场景选择合适的技术,平衡性能和移植性
select和poll本质时轮询,将set_fd 数据从用户空间拷贝到内核空间,一旦有哪个fd活跃了,则将整个数组拷贝到用户空间,逐个检查时那个fd活跃。
epoll是基于事件驱动的,将新增加的连接,(epoll_ctr)注册到内核提供的红黑树节点(epoll_create创建红黑树),节点注册了回调函数,一旦i/o就绪,则触发回调函数,将fd节点加入就绪队列。直接处理就绪队列便可。epoll_wait就是从就绪链表中拿这些fd。
如果连接数不多,epoll没有优势。
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <algorithm>
// 线程池类
class ThreadPool {
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->queueMutex);
this->condition.wait(lock, [this] {
return this->stop || !this->tasks.empty();
});
if (this->stop && this->tasks.empty())
return;
task = std::move(this->tasks.front());
this->tasks.pop();
}
task();
}
});
}
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queueMutex);
stop = true;
}
condition.notify_all();
for (std::thread &worker : workers)
worker.join();
}
template<class F>
void enqueue(F &&f) {
{
std::unique_lock<std::mutex> lock(queueMutex);
tasks.emplace(std::forward<F>(f));
}
condition.notify_one();
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
bool stop;
};
// 设置文件描述符为非阻塞模式
void setNonBlocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL failed");
exit(EXIT_FAILURE);
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL failed");
exit(EXIT_FAILURE);
}
}
// 读取数据
ssize_t readn(int fd, char *buf, size_t n) {
size_t nleft = n;
ssize_t nread;
char *ptr = buf;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else if (errno == EAGAIN)
return n - nleft;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
// 发送数据
ssize_t writen(int fd, const char *buf, size_t n) {
size_t nleft = n;
ssize_t nwritten;
const char *ptr = buf;
while (nleft > 0) {
if ((nwritten = write(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nwritten = 0;
else if (errno == EAGAIN)
return n - nleft;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
// 处理客户端请求
void handleClient(int clientFd, ThreadPool &threadPool, int epollFd) {
char buf[1024] = {0};
ssize_t n = readn(clientFd, buf, sizeof(buf) - 1);
if (n <= 0) {
if (n < 0) perror("read error");
close(clientFd);
epoll_ctl(epollFd, EPOLL_CTL_DEL, clientFd, nullptr);
return;
}
std::cout << "Received from client " << clientFd << ": " << buf << std::endl;
// 构造响应
std::string response = "HTTP/1.1 200 OK\r\n";
response += "Content-Length: " + std::to_string(n) + "\r\n";
response += "Content-Type: text/plain\r\n\r\n";
response += std::string(buf, n);
// 发送响应
n = writen(clientFd, response.c_str(), response.size());
if (n < 0) {
perror("write error");
close(clientFd);
epoll_ctl(epollFd, EPOLL_CTL_DEL, clientFd, nullptr);
return;
}
// 对于HTTP长连接,可以保持连接,这里简单处理为关闭
close(clientFd);
epoll_ctl(epollFd, EPOLL_CTL_DEL, clientFd, nullptr);
}
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]);
// 创建监听socket
int listenFd = socket(AF_INET, SOCK_STREAM, 0);
if (listenFd < 0) {
perror("socket creation failed");
return 1;
}
// 设置SO_REUSEADDR选项
int opt = 1;
if (setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt failed");
return 1;
}
// 绑定地址
struct sockaddr_in serverAddr;
std::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(port);
if (bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("bind failed");
close(listenFd);
return 1;
}
// 开始监听
if (listen(listenFd, SOMAXCONN) < 0) {
perror("listen failed");
close(listenFd);
return 1;
}
// 设置监听socket为非阻塞
setNonBlocking(listenFd);
// 创建epoll实例
int epollFd = epoll_create1(0);
if (epollFd < 0) {
perror("epoll_create1 failed");
close(listenFd);
return 1;
}
// 注册监听socket到epoll
struct epoll_event event;
std::memset(&event, 0, sizeof(event));
event.data.fd = listenFd;
event.events = EPOLLIN | EPOLLET; // 边缘触发模式
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, listenFd, &event) < 0) {
perror("epoll_ctl add listenFd failed");
close(listenFd);
close(epollFd);
return 1;
}
// 创建线程池,线程数为CPU核心数
int numThreads = std::thread::hardware_concurrency();
if (numThreads == 0) numThreads = 4;
ThreadPool threadPool(numThreads);
std::cout << "Server started on port " << port << ", using " << numThreads << " threads" << std::endl;
// 事件循环
const int MAX_EVENTS = 1024;
struct epoll_event events[MAX_EVENTS];
while (true) {
int nfds = epoll_wait(epollFd, events, MAX_EVENTS, -1);
if (nfds < 0) {
perror("epoll_wait failed");
break;
}
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listenFd) {
// 处理新连接
while (true) {
struct sockaddr_in clientAddr;
socklen_t clientLen = sizeof(clientAddr);
int clientFd = accept(listenFd, (struct sockaddr *)&clientAddr, &clientLen);
if (clientFd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
break; // 没有更多连接
else {
perror("accept failed");
break;
}
}
std::cout << "New connection from " << inet_ntoa(clientAddr.sin_addr)
<< ":" << ntohs(clientAddr.sin_port) << std::endl;
// 设置客户端socket为非阻塞
setNonBlocking(clientFd);
// 注册客户端socket到epoll
struct epoll_event clientEvent;
std::memset(&clientEvent, 0, sizeof(clientEvent));
clientEvent.data.fd = clientFd;
clientEvent.events = EPOLLIN | EPOLLET | EPOLLONESHOT; // 一次性触发
if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientFd, &clientEvent) < 0) {
perror("epoll_ctl add clientFd failed");
close(clientFd);
}
}
} else if (events[i].events & EPOLLIN) {
// 处理可读事件
int clientFd = events[i].data.fd;
threadPool.enqueue([clientFd, &threadPool, epollFd]() {
handleClient(clientFd, threadPool, epollFd);
});
}
}
}
// 清理资源
close(listenFd);
close(epollFd);
return 0;
}