io多路复用(select,poll,epoll)
select 示例
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <string.h>
constexpr int PORT = 8080;
constexpr int BUFFER_SIZE = 1024;
void setup(int &sockfd) {
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Failed to create socket");
exit(EXIT_FAILURE);
}
sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
if (bind(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("Failed to bind socket");
close(sockfd);
exit(EXIT_FAILURE);
}
if (listen(sockfd, 5) < 0) {
perror("Failed to listen on socket");
close(sockfd);
exit(EXIT_FAILURE);
}
// Set non-blocking mode
fcntl(sockfd, F_SETFL, O_NONBLOCK);
std::cout << "Server listening on port " << PORT << std::endl;
}
int main() {
int serverSock, maxfd;
setup(serverSock);
fd_set readfds;
FD_ZERO(&readfds);
while (true) {
// Add server socket and standard input to the set
FD_SET(serverSock, &readfds);
FD_SET(STDIN_FILENO, &readfds);
maxfd = std::max(serverSock, STDIN_FILENO);
// Set timeout for select (2 seconds)
timeval timeout{};
timeout.tv_sec = 2;
timeout.tv_usec = 0;
int activity = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
if (activity < 0 && errno != EINTR) {
perror("select error");
} else if (activity == 0) {
std::cout << "Timeout occurred! No activity detected.\n";
continue;
}
// Check if there's activity on the server socket
if (FD_ISSET(serverSock, &readfds)) {
sockaddr_in clientAddr{};
socklen_t addrLen = sizeof(clientAddr);
int clientSock = accept(serverSock, (struct sockaddr *)&clientAddr, &addrLen);
if (clientSock >= 0) {
std::cout << "New connection from " << inet_ntoa(clientAddr.sin_addr) << std::endl;
close(clientSock);
}
}
// Check if there's input on stdin
if (FD_ISSET(STDIN_FILENO, &readfds)) {
char buffer[BUFFER_SIZE];
ssize_t bytesRead = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (bytesRead > 0) {
buffer[bytesRead] = '\0';
std::cout << "Received input: " << buffer << std::endl;
}
}
}
close(serverSock);
return 0;
}
- 优点: 跨平台
- 缺点:
- 监听文件描述符多了之后,慢的一匹,每次都要遍历所有fd
- 文件描述符上限1024
- 每次调用select,都需要把文件描述符集合从用户态拷贝到内核态
poll 示例
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <vector>
using namespace std;
#define PORT 8888
#define MAX_EVENTS 1024
int main() {
int server_fd;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[1024] = {0};
vector<pollfd> fds;
// 创建套接字
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);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 初始化pollfd数组
pollfd fd;
fd.fd = server_fd;
fd.events = POLLIN;
fds.push_back(fd);
while (true) {
int num_events = poll(fds.data(), fds.size(), -1); // -1 为无线阻塞,0为不阻塞,其他值表示阻塞的时间
if (num_events < 0) {
perror("poll");
exit(EXIT_FAILURE);
}
for (int i = 0; i < num_events; ++i) {
if (fds[i].revents & POLLIN) {
if (fds[i].fd == server_fd) {
// 有新的连接
int new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 将新连接添加到pollfd数组
pollfd new_fd;
new_fd.fd = new_socket;
new_fd.events = POLLIN;
fds.push_back(new_fd);
} else {
// 有数据可读
int bytes_read = read(fds[i].fd, buffer, 1024);
if (bytes_read <= 0) {
// 连接关闭或出错
close(fds[i].fd);
fds.erase(fds.begin() + i);
--i; // 调整索引
} else {
// 处理接收到的数据
cout << "Client: " << buffer << endl;
// ...
}
}
}
}
}
return 0;
}
相比于select,poll扩展了文件描述符的个数,但其还是使用遍历所有描述符的方式检查事件触发
- 优点
- 没有文件描述符个数限制(但是性能瓶颈很明显)
- 描述符个数小的场景下,推荐用poll,不用select
- 缺点
- 就是慢,还是遍历的方式,文件描述符多了比select更差
- 相比于epoll,没有事件信息
epoll 示例
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <cstring>
#include <vector>
#define PORT 8080
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
// 创建非阻塞 socket
int create_server_socket() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Socket creation failed!" << std::endl;
return -1;
}
// 设置 socket 为非阻塞模式
int flags = fcntl(server_fd, F_GETFL, 0);
fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);
sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "Binding failed!" << std::endl;
close(server_fd);
return -1;
}
if (listen(server_fd, 3) < 0) {
std::cerr << "Listen failed!" << std::endl;
close(server_fd);
return -1;
}
return server_fd;
}
int main() {
int server_fd = create_server_socket();
if (server_fd == -1) {
return 1;
}
// 创建 epoll 实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
std::cerr << "Epoll create failed!" << std::endl;
close(server_fd);
return 1;
}
// 将 server_fd 加入 epoll 监听
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
std::cerr << "Epoll_ctl failed!" << std::endl;
close(server_fd);
close(epoll_fd);
return 1;
}
std::vector<struct epoll_event> events(MAX_EVENTS);
while (true) {
// 等待事件发生
int num_events = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1); // 阻塞直到有事件
if (num_events == -1) {
std::cerr << "Epoll wait failed!" << std::endl;
break;
}
// 处理所有发生的事件
for (int i = 0; i < num_events; ++i) {
if (events[i].data.fd == server_fd) {
// 新的客户端连接
sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
std::cerr << "Accept failed!" << std::endl;
} else {
std::cout << "New client connected!" << std::endl;
// 设置新连接为非阻塞
int flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
// 将 client_fd 添加到 epoll 中
event.events = EPOLLIN | EPOLLET; // 使用边缘触发
event.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
std::cerr << "Epoll_ctl failed!" << std::endl;
close(client_fd);
}
}
} else if (events[i].events & EPOLLIN) {
// 客户端发送数据
int client_fd = events[i].data.fd;
char buffer[BUFFER_SIZE];
ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer));
if (bytes_read > 0) {
std::cout << "Received data: " << std::string(buffer, bytes_read) << std::endl;
// 向客户端发送数据
write(client_fd, "Hello, client!", 14);
} else if (bytes_read == 0) {
// 客户端关闭连接
std::cout << "Client disconnected!" << std::endl;
// 从 epoll 中删除 client_fd
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, nullptr) == -1) {
std::cerr << "Failed to remove client_fd from epoll!" << std::endl;
}
close(client_fd);
} else {
std::cerr << "Read error!" << std::endl;
close(client_fd);
}
}
}
}
// 清理资源
close(server_fd);
close(epoll_fd);
return 0;
}
-
缺点: 个人认为, epoll在linux下, 没有缺点, 不在linux 那也没有epoll 了, 剩下的无非就是资源啥乱七八糟的
-
epoll 的性能优势来源:
-
事件的通知机制:
- epoll 则采用了更高效的事件通知机制。当一个事件发生时,内核只会通知应用程序,而无需每次遍历整个文件描述符集合。epoll 通过一个基于事件回调的机制,只有在有事件发生时,才将相关的文件描述符通知给应用程序。这样,应用程序无需每次都遍历整个文件描述符集合,而只需要关心那些真正有事件的文件描述符
- poll/select 通过传递一个文件描述符集合(fd_set)来通知哪些文件描述符发生了事件。在每次调用时,内核都必须遍历这个文件描述符集合,并检查每个描述符的状态,哪怕没有任何事件发生,这个遍历的过程是线性的,时间复杂度是 O(n),其中 n 是文件描述符的数量
-
文件描述符管理
- epoll 用了基于事件的通知机制,称为 水平触发(Level Triggered, LT 和 边缘触发(Edge Triggered, ET)。通过 epoll_ctl 将文件描述符添加到 epoll 的监听队列中,epoll 会记录文件描述符的状态,且只有在文件描述符的状态变化时,内核才会通知应用程序,避免了不必要的遍历和重复通知
- select/poll 如果你要监听多个文件描述符,文件描述符数量上限通常是固定的(如 FD_SETSIZE),并且在添加、删除文件描述符时,每次都需要重新传递整个文件描述符集合给内核,因此,在动态增加或删除文件描述符时,会有大量的用户态到内核态的拷贝
-
事件通知的优化
- epoll 提供水平触发(Level Triggered, LT) 和 边缘触发(Edge Triggered, ET)两种模式
- select/poll 仅支持 水平触发
Signal-Driven I/O Model(信号驱动)
信号驱动I/O是通过内核发送信号(如SIGIO)来通知进程某个I/O操作已准备好,而不需要进程主动去检查文件描述符的状态。该模型通常用于异步通知I/O事件的发生,避免了轮询操作,从而减少了CPU的浪费
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <errno.h>
#define PORT 8080
int sockfd;
// 信号处理函数
void sigio_handler(int signo) {
char buffer[1024];
int n;
// 读取套接字数据
n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n < 0) {
if (errno == EAGAIN) {
printf("No data available\n");
} else {
perror("Read error");
}
return;
}
buffer[n] = '\0'; // Null terminate the string
printf("Received data: %s\n", buffer);
}
int main() {
struct sockaddr_in server_addr;
struct sigaction sa;
// 创建一个套接字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket failed");
exit(EXIT_FAILURE);
}
// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 绑定套接字到端口
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 设置套接字为非阻塞模式
if (fcntl(sockfd, F_SETFL, O_NONBLOCK) < 0) {
perror("fcntl failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 设置套接字为异步I/O模式,信号SIGIO将在I/O事件发生时发送
if (fcntl(sockfd, F_SETFL, FASYNC) < 0) {
perror("fcntl failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 注册SIGIO信号处理函数
sa.sa_handler = sigio_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGIO, &sa, NULL) < 0) {
perror("sigaction failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 启动监听
if (listen(sockfd, 5) < 0) {
perror("Listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 服务器主循环,等待SIGIO信号
while (1) {
pause(); // 等待信号到达
}
close(sockfd);
return 0;
}
-
代码中, 定义了 sigio_handler 函数,用于处理 SIGIO 信号,当I/O操作准备好时,内核会发送此信号,并触发该函数,读取数据并进行处理
-
fcntl(sockfd, F_SETFL, O_NONBLOCK):设置套接字为非阻塞模式
fcntl(sockfd, F_SETFL, FASYNC):使套接字支持异步I/O,进程可以通过信号接收I/O事件通知 -
通过sigaction注册处理SIGIO信号的回调函数。每当I/O事件发生时,内核会发送SIGIO信号,进程就会执行该信号处理函数