IO多路复用:select、poll、epoll的底层区别
1. select
工作原理:
select
使用一个固定大小的数组来存储需要监控的文件描述符。这个数组的大小通常是限制在FD_SETSIZE
(通常为 1024)内。- 当调用
select
时,操作系统会检查每个文件描述符的状态,确定哪些描述符准备好进行 I/O 操作。 select
是阻塞的,即如果没有文件描述符准备好,它会阻塞调用线程直到有文件描述符变为可用状态,或达到超时。
优缺点:
- 优点:
- 简单易用,适合于小规模的文件描述符监控。
- 缺点:
- 不支持超过
FD_SETSIZE
的文件描述符。 - 每次调用都需要将文件描述符数组复制到内核空间,性能开销大。
- 每次调用时都要遍历整个文件描述符集合,效率低下,尤其是在大量文件描述符时。
- 不支持超过
2. poll
工作原理:
poll
使用一个可变大小的数组(pollfd
结构体数组)来存储需要监控的文件描述符。与select
不同,poll
不再受到FD_SETSIZE
的限制。- 调用
poll
时,它会检查pollfd
数组中每个描述符的状态,并返回准备好 I/O 操作的文件描述符数量。 poll
也是阻塞的。
优缺点:
- 优点:
- 不受
FD_SETSIZE
的限制,可以处理任意数量的文件描述符。 - 结构更灵活,能够更容易地扩展。
- 不受
- 缺点:
- 仍然需要在每次调用时遍历整个数组,性能问题仍然存在。
- 每次调用时,文件描述符状态的更新需要复制数据到内核,性能开销依旧。
3. epoll
工作原理:
epoll
是为了解决select
和poll
的一些性能瓶颈而设计的。它使用一个文件描述符来表示一个epoll
实例。epoll
通过epoll_ctl
添加和删除文件描述符,而不是在每次调用中传递整个文件描述符数组。它维护了一个内核中的事件表。epoll_wait
只返回准备好 I/O 操作的文件描述符,而不是遍历所有描述符。- 可以选择边缘触发(Edge Triggered)和水平触发(Level Triggered)模式。
- 在水平触发模式下,当某个文件描述符(FD)变为可读、可写或发生异常时,
epoll_wait
会返回该文件描述符的事件。只要文件描述符的状态满足条件(如可读),即使后续调用epoll_wait
也会继续返回该文件描述符,直到应用程序将所有数据读出或写入完成。应用程序可以多次读取数据,确保数据不会丢失。适合需要确保数据完整处理的场景。 - 在边缘触发模式下,当文件描述符的状态发生变化(例如从不可读变为可读)时,
epoll_wait
会返回该文件描述符。一旦状态变化通知后,只有在后续状态再次变化时(如读入数据后再次变为可读),才会再次触发通知。如果没有读取所有可用数据,可能会错过后续的事件通知。适合高性能和高并发的场景。
优缺点:
- 优点:
- 高效:适用于大量并发连接,尤其是在高并发的场景下,性能显著提高。
- 不需要在每次调用时复制文件描述符的状态,减少了开销。
- 只返回准备好的描述符,避免了遍历所有描述符的开销。
- 缺点:
- API 比较复杂,使用上相对
select
和poll
难度较高。 - 只在 Linux 系统中可用,移植性较差。
- API 比较复杂,使用上相对
总结
特性 | select | poll | epoll |
---|---|---|---|
文件描述符限制 | 有 (FD_SETSIZE ) | 没有 | 没有 |
数据结构 | 固定大小的位图 | 可变大小的结构体数组 | 内核维护的事件表 |
调用性能 | O(n),每次遍历 | O(n),每次遍历 | O(1),只返回活跃的文件描述符 |
内存复制 | 每次调用都需要 | 每次调用都需要 | 仅在添加/删除时需要 |
触发模式 | 水平触发 | 水平触发 | 支持水平触发和边缘触发 |
适用场景 | 小型应用 | 中型应用 | 大型高并发应用 |
select
:简单但有文件描述符数量限制,性能在高并发情况下不佳。poll
:解决了select
的限制,但性能依然受到遍历数组的影响。epoll
:最适合高并发场景,提供更高的性能和更灵活的接口,适合处理大量文件描述符。
代码示例
每个示例都创建了一个简单的 TCP 服务器,接收客户端的连接请求并处理数据。
1. 使用 select
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建 socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置 socket 选项
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
// 监听
listen(server_fd, MAX_CLIENTS);
fd_set read_fds;
int max_sd;
while (true) {
// 清空 fd_set
FD_ZERO(&read_fds);
FD_SET(server_fd, &read_fds);
max_sd = server_fd;
// 添加客户端 sockets
for (int i = 0; i < MAX_CLIENTS; i++) {
if (FD_ISSET(i, &read_fds) && i > 0) {
FD_SET(i, &read_fds);
if (i > max_sd) max_sd = i;
}
}
// 调用 select
int activity = select(max_sd + 1, &read_fds, nullptr, nullptr, nullptr);
if ((activity < 0) && (errno != EINTR)) {
std::cerr << "select error" << std::endl;
}
// 处理新连接
if (FD_ISSET(server_fd, &read_fds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
std::cout << "New connection: " << new_socket << std::endl;
FD_SET(new_socket, &read_fds);
}
// 处理客户端请求
for (int i = 0; i < MAX_CLIENTS; i++) {
if (FD_ISSET(i, &read_fds)) {
int valread = read(i, buffer, 1024);
if (valread == 0) {
// 客户端断开连接
std::cout << "Client disconnected: " << i << std::endl;
close(i);
FD_CLR(i, &read_fds);
} else {
buffer[valread] = '\0';
std::cout << "Received from client " << i << ": " << buffer << std::endl;
send(i, buffer, valread, 0); // 回送数据
}
}
}
}
return 0;
}
2. 使用 poll
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <poll.h>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建 socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置 socket 选项
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
// 监听
listen(server_fd, MAX_CLIENTS);
struct pollfd fds[MAX_CLIENTS + 1];
fds[0].fd = server_fd;
fds[0].events = POLLIN;
for (int i = 1; i <= MAX_CLIENTS; i++) {
fds[i].fd = -1; // 初始化客户端 sockets
}
while (true) {
int activity = poll(fds, MAX_CLIENTS + 1, -1); // 等待无限期
if ((activity < 0) && (errno != EINTR)) {
std::cerr << "poll error" << std::endl;
}
// 处理新连接
if (fds[0].revents & POLLIN) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
std::cout << "New connection: " << new_socket << std::endl;
for (int i = 1; i <= MAX_CLIENTS; i++) {
if (fds[i].fd == -1) {
fds[i].fd = new_socket;
fds[i].events = POLLIN;
break;
}
}
}
// 处理客户端请求
for (int i = 1; i <= MAX_CLIENTS; i++) {
if (fds[i].fd > 0 && (fds[i].revents & POLLIN)) {
int valread = read(fds[i].fd, buffer, 1024);
if (valread == 0) {
// 客户端断开连接
std::cout << "Client disconnected: " << fds[i].fd << std::endl;
close(fds[i].fd);
fds[i].fd = -1; // 标记为无效
} else {
buffer[valread] = '\0';
std::cout << "Received from client " << fds[i].fd << ": " << buffer << std::endl;
send(fds[i].fd, buffer, valread, 0); // 回送数据
}
}
}
}
return 0;
}
3. 使用 epoll
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建 socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置 socket 选项
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
// 监听
listen(server_fd, MAX_CLIENTS);
// 创建 epoll 实例
int epoll_fd = epoll_create1(0);
struct epoll_event event, events[MAX_CLIENTS];
// 添加 server socket 到 epoll
event.events = EPOLLIN;
event.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
while (true) {
int num_events = epoll_wait(epoll_fd, events, MAX_CLIENTS, -1);
for (int i = 0; i < num_events; i++) {
if (events[i].data.fd == server_fd) {
// 处理新连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
std::cout << "New connection: " << new_socket << std::endl;
// 将新连接的 socket 添加到 epoll
event.events = EPOLLIN;
event.data.fd = new_socket;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event);
} else {
// 处理客户端请求
int valread = read(events[i].data.fd, buffer, 1024);
if (valread == 0) {
// 客户端断开连接
std::cout << "Client disconnected: " << events[i].data.fd << std::endl;
close(events[i].data.fd);
} else {
buffer[valread] = '\0';
std::cout << "Received from client " << events[i].data.fd << ": " << buffer << std::endl;
send(events[i].data.fd, buffer, valread, 0); // 回送数据
}
}
}
}
return 0;