2.9 Linux I/O多路复用机制:select、poll、epoll详解
一、select
基本原理:通过轮询方式检查文件描述符集合。
特点:
- 有最大文件描述符数量限制(FD_SETSIZE,通常1024)。
- 每次调用需要重新设置fd_set,select( )会破坏 fd_set,只保留就绪的 fd,因此必须每次重新设置。
- 线性扫描所有fd,即使只有一个 fd 需要就绪,效率随 fd 数量增加而下降。
- 每次调用都会全量拷贝 fd_set 两次,一次 用户态 -> 内核态,一次 内核态 -> 用户态。
- 如果第一遍内核轮询发现没有 fd 就绪会睡眠,当有 fd 就绪会再次唤醒进程,内核会再次轮询一遍,之后返回到用户态。
使用场景:跨平台兼容性要求高,fd 数量少的场景。
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds); // 监控标准输入
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
// -1 表示调用失败;0 表示超时,没有任何文件描述符就绪;>0 表示文件描述符就绪的数量
// 不填时间会无限等待
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <errno.h>
#define MAX_CLIENTS 30
#define BUFFER_SIZE 1024
#define PORT 8080
int main() {
int server_fd, new_socket, client_sockets[MAX_CLIENTS];
fd_set readfds;
int max_sd, activity, i, valread, sd;
struct sockaddr_in address;
char buffer[BUFFER_SIZE] = {0};
// 初始化客户端socket数组
for (i = 0; i < MAX_CLIENTS; i++) {
client_sockets[i] = 0;
}
// 创建服务器socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket选项(避免地址占用错误)
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 绑定socket到端口
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, 5) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
// 清空文件描述符集合
FD_ZERO(&readfds);
// 添加服务器socket到集合
FD_SET(server_fd, &readfds);
max_sd = server_fd;
// 添加客户端sockets到集合
for (i = 0; i < MAX_CLIENTS; i++) {
sd = client_sockets[i];
if (sd > 0) {
FD_SET(sd, &readfds);
}
if (sd > max_sd) {
max_sd = sd;
}
}
// 等待活动(无限等待)
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
perror("select error");
}
// 检查是否有新的连接
if (FD_ISSET(server_fd, &readfds)) {
if ((new_socket = accept(server_fd, NULL, NULL)) < 0) {
perror("accept");
continue;
}
printf("New connection, socket fd: %d\n", new_socket);
// 添加新socket到客户端数组
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_sockets[i] == 0) {
client_sockets[i] = new_socket;
printf("Adding to list of sockets as %d\n", i);
break;
}
}
if (i == MAX_CLIENTS) {
printf("Too many clients\n");
close(new_socket);
}
}
// 检查客户端socket的活动
for (i = 0; i < MAX_CLIENTS; i++) {
sd = client_sockets[i];
if (FD_ISSET(sd, &readfds)) {
// 读取客户端数据
if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) {
// 客户端断开连接
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&address);
printf("Client disconnected, ip %s, port %d\n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
close(sd);
client_sockets[i] = 0;
} else {
// 回显数据给客户端
buffer[valread] = '\0';
printf("Received: %s\n", buffer);
send(sd, buffer, strlen(buffer), 0);
}
}
}
}
return 0;
}
二、poll
基本原理:使用 pollfd 结构数组代替 fd_set 位图,没有最大fd数量限制
特点:
- 没有最大 fd 数量限制(受系统限制);
- 不需要每次调用重新设置监控集合;
- 仍然是线性扫描,效率问题依然存在。
使用场景:fd数量较多但不需要高性能的场景。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#define MAX_CLIENTS 100
#define BUFFER_SIZE 1024
#define PORT 8080
#define TIMEOUT 30000 // 30秒超时
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 创建pollfd数组
struct pollfd fds[MAX_CLIENTS + 1]; // +1 给服务器socket
int nfds = 1; // 初始只有服务器socket
int current_size;
int i, ret;
// 初始化pollfd数组
for (i = 0; i < MAX_CLIENTS + 1; i++) {
fds[i].fd = -1; // 初始化为无效fd
fds[i].events = 0;
fds[i].revents = 0;
}
// 创建服务器socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket选项(避免地址占用错误)
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 绑定socket到端口
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, 5) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 设置服务器socket
fds[0].fd = server_fd;
fds[0].events = POLLIN; // 监控可读事件
while (1) {
// 调用poll等待事件
ret = poll(fds, nfds, TIMEOUT);
if (ret < 0) {
perror("poll error");
break;
} else if (ret == 0) {
printf("Poll timeo
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
C++/嵌入式开发 秋招面经 文章被收录于专栏
一名985硕,在25年秋招中斩获多个C++/嵌入式开发Offer。本专栏将分享我的面经,涵盖C/C++、操作系统、计算机网络、ARM体系与架构、Linux应用/驱动开发、Qt、通信协议及开发工具链等核心内容。
查看16道真题和解析
