# IO多路复用 ## 1. 为什么需要IO多路复用 ### 1.1 传统IO模型的问题 - **阻塞IO**:进程在读写操作时会阻塞,无法处理其他fd - **多进程/多线程**:每个连接一个进程/线程,资源消耗大 - **非阻塞IO**:需要轮询所有fd,CPU占用高 ### 1.2 IO多路复用的优势 - **单线程处理多个fd**:减少线程/进程创建开销 - **事件驱动**:只处理有事件的fd,效率高 - **资源节省**:避免频繁的上下文切换 ### 1.3 典型应用场景 - **Web服务器**:处理大量并发连接 - **数据库服务器**:管理多个客户端连接 - **聊天服务器**:实时消息转发 - **嵌入式系统**:多传感器数据采集 ## 2. select详解 ### 2.1 函数原型 ```c #include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` ### 2.2 参数说明 #### nfds - 需要监视的文件描述符数量 - 值为`nfds + 1`(因为fd从0开始) - 例如:最大fd为5,则nfds=6 #### fd_set类型 - 文件描述符集合,本质上是位图 - 最大支持`FD_SETSIZE`个fd(通常为1024) #### fd_set操作宏 ```c FD_ZERO(fd_set *set); // 清空集合 FD_SET(int fd, fd_set *set); // 添加fd到集合 FD_CLR(int fd, fd_set *set); // 从集合中移除fd FD_ISSET(int fd, fd_set *set); // 检查fd是否在集合中 ``` #### timeout参数 ```c struct timeval { long tv_sec; // 秒 long tv_usec; // 微秒 }; ``` - `NULL`:永久阻塞,直到有事件发生 - `0, 0`:非阻塞,立即返回 - `>0`:等待指定时间后返回 ### 2.3 返回值 - **>0**:有事件发生的fd数量 - **0**:超时 - **-1**:出错(errno设置) ### 2.4 select使用示例 ```c #include #include #include #include #include #include #include #define PORT 8888 #define BUF_SIZE 1024 #define MAX_CLIENTS 10 int main() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len; fd_set read_fds, temp_fds; int max_fd, i, activity; char buffer[BUF_SIZE]; int client_fds[MAX_CLIENTS]; int client_count = 0; // 初始化客户端数组 for (i = 0; i < MAX_CLIENTS; i++) { client_fds[i] = 0; } // 1. 创建服务器套接字 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 2. 绑定地址和端口 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 3. 监听 if (listen(server_fd, 5) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server listening on port %d\n", PORT); // 4. 初始化fd_set FD_ZERO(&read_fds); FD_SET(server_fd, &read_fds); max_fd = server_fd; // 5. 主循环 while (1) { temp_fds = read_fds; // select等待事件 activity = select(max_fd + 1, &temp_fds, NULL, NULL, NULL); if (activity < 0) { perror("select error"); continue; } // 检查服务器套接字是否有新连接 if (FD_ISSET(server_fd, &temp_fds)) { client_len = sizeof(client_addr); client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { perror("accept failed"); continue; } printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 添加到客户端数组 for (i = 0; i < MAX_CLIENTS; i++) { if (client_fds[i] == 0) { client_fds[i] = client_fd; FD_SET(client_fd, &read_fds); if (client_fd > max_fd) { max_fd = client_fd; } break; } } } // 检查客户端数据 for (i = 0; i < MAX_CLIENTS; i++) { if (client_fds[i] > 0 && FD_ISSET(client_fds[i], &temp_fds)) { int valread = read(client_fds[i], buffer, BUF_SIZE); if (valread == 0) { // 客户端断开连接 printf("Client disconnected\n"); close(client_fds[i]); FD_CLR(client_fds[i], &read_fds); client_fds[i] = 0; } else { // 回显数据 buffer[valread] = '\0'; printf("Received: %s", buffer); send(client_fds[i], buffer, valread, 0); } } } } close(server_fd); return 0; } ``` ### 2.5 select的局限性 1. **fd数量限制**:`FD_SETSIZE`通常为1024 2. **性能问题**:每次调用需要拷贝fd_set到内核 3. **线性扫描**:需要遍历所有fd检查事件 4. **fd_set重建**:每次调用都需要重新设置fd_set ## 3. poll详解 ### 3.1 函数原型 ```c #include int poll(struct pollfd *fds, nfds_t nfds, int timeout); ``` ### 3.2 pollfd结构体 ```c struct pollfd { int fd; // 文件描述符 short events; // 请求的事件 short revents; // 返回的事件 }; ``` ### 3.3 事件类型 #### events(请求事件) - `POLLIN`:有数据可读 - `POLLOUT`:可以写入数据 - `POLLERR`:发生错误 - `POLLHUP`:挂起 - `POLLPRI`:有紧急数据 #### revents(返回事件) - 包含实际发生的事件 - 可以检查`POLLIN`、`POLLOUT`等 ### 3.4 poll使用示例 ```c #include #include #include #include #include #include #include #define PORT 8888 #define BUF_SIZE 1024 #define MAX_CLIENTS 10 int main() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len; struct pollfd fds[MAX_CLIENTS + 1]; int nfds = 1; int timeout = 5000; // 5秒超时 char buffer[BUF_SIZE]; int i, ret; // 1. 创建服务器套接字 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 2. 绑定地址和端口 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 3. 监听 if (listen(server_fd, 5) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server listening on port %d\n", PORT); // 4. 初始化pollfd数组 fds[0].fd = server_fd; fds[0].events = POLLIN; // 5. 主循环 while (1) { ret = poll(fds, nfds, timeout); if (ret < 0) { perror("poll error"); continue; } if (ret == 0) { printf("Poll timeout\n"); continue; } // 检查服务器套接字 if (fds[0].revents & POLLIN) { client_len = sizeof(client_addr); client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { perror("accept failed"); continue; } printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 添加到pollfd数组 if (nfds < MAX_CLIENTS + 1) { fds[nfds].fd = client_fd; fds[nfds].events = POLLIN; nfds++; } else { printf("Max clients reached\n"); close(client_fd); } } // 检查客户端数据 for (i = 1; i < nfds; i++) { if (fds[i].revents & POLLIN) { int valread = read(fds[i].fd, buffer, BUF_SIZE); if (valread == 0) { // 客户端断开连接 printf("Client disconnected\n"); close(fds[i].fd); // 移除客户端 fds[i] = fds[nfds - 1]; nfds--; i--; } else { // 回显数据 buffer[valread] = '\0'; printf("Received: %s", buffer); send(fds[i].fd, buffer, valread, 0); } } } } close(server_fd); return 0; } ``` ### 3.5 poll的优势 1. **无fd数量限制**:使用动态数组 2. **结构更清晰**:events和revents分离 3. **不需要每次重建**:直接修改events字段 ## 4. epoll详解 ### 4.1 核心API #### epoll_create ```c #include int epoll_create(int size); ``` - 创建epoll实例 - `size`:建议的大小(内核会动态调整) - 返回epoll文件描述符 #### epoll_ctl ```c int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); ``` - `epfd`:epoll实例文件描述符 - `op`:操作类型 - `EPOLL_CTL_ADD`:添加fd - `EPOLL_CTL_MOD`:修改fd - `EPOLL_CTL_DEL`:删除fd - `fd`:目标文件描述符 - `event`:事件结构体 #### epoll_wait ```c int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); ``` - `events`:输出参数,存储发生的事件 - `maxevents`:最大事件数 - `timeout`:超时时间(毫秒) ### 4.2 epoll_event结构体 ```c struct epoll_event { uint32_t events; // 事件类型 epoll_data_t data; // 用户数据 }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; ``` ### 4.3 事件类型 - `EPOLLIN`:有数据可读 - `EPOLLOUT`:可以写入数据 - `EPOLLERR`:发生错误 - `EPOLLHUP`:挂起 - `EPOLLRDHUP`:对端关闭连接 - `EPOLLET`:边缘触发模式 ### 4.4 epoll使用示例 ```c #include #include #include #include #include #include #include #define PORT 8888 #define BUF_SIZE 1024 #define MAX_EVENTS 10 int main() { int server_fd, client_fd, epoll_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len; struct epoll_event event, events[MAX_EVENTS]; char buffer[BUF_SIZE]; int i, nfds; // 1. 创建服务器套接字 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 2. 绑定地址和端口 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 3. 监听 if (listen(server_fd, 5) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server listening on port %d\n", PORT); // 4. 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd < 0) { perror("epoll_create1 failed"); close(server_fd); exit(EXIT_FAILURE); } // 5. 添加服务器套接字到epoll event.events = EPOLLIN; event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) { perror("epoll_ctl failed"); close(epoll_fd); close(server_fd); exit(EXIT_FAILURE); } // 6. 主循环 while (1) { nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds < 0) { perror("epoll_wait error"); continue; } for (i = 0; i < nfds; i++) { if (events[i].data.fd == server_fd) { // 新连接 client_len = sizeof(client_addr); client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { perror("accept failed"); continue; } printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 添加客户端到epoll event.events = EPOLLIN; event.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) { perror("epoll_ctl failed"); close(client_fd); } } else { // 客户端数据 int valread = read(events[i].data.fd, buffer, BUF_SIZE); if (valread == 0) { // 客户端断开连接 printf("Client disconnected\n"); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL); close(events[i].data.fd); } else { // 回显数据 buffer[valread] = '\0'; printf("Received: %s", buffer); send(events[i].data.fd, buffer, valread, 0); } } } } close(epoll_fd); close(server_fd); return 0; } ``` ### 4.5 LT(水平触发)vs ET(边缘触发) #### LT模式(默认) - 只要fd可读/可写,就会持续通知 - 编程简单,不容易丢失事件 - 可能导致重复通知 #### ET模式 - 只在状态变化时通知一次 - 性能更高,但编程复杂 - 必须一次性读完所有数据 #### ET模式要求 1. **必须使用非阻塞fd** 2. **必须一次性读完所有数据** 3. **使用`EAGAIN`判断是否读完** ### 4.6 ET模式示例 ```c #include #include #include #include #include #include #include #include #include #define PORT 8888 #define BUF_SIZE 1024 // 设置非阻塞模式 int set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) return -1; return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int main() { int server_fd, client_fd, epoll_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len; struct epoll_event event, events[10]; char buffer[BUF_SIZE]; int i, nfds, n; // 1. 创建服务器套接字 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置非阻塞 set_nonblocking(server_fd); // 2. 绑定地址和端口 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 3. 监听 if (listen(server_fd, 5) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server listening on port %d\n", PORT); // 4. 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd < 0) { perror("epoll_create1 failed"); close(server_fd); exit(EXIT_FAILURE); } // 5. 添加服务器套接字到epoll(边缘触发) event.events = EPOLLIN | EPOLLET; event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) { perror("epoll_ctl failed"); close(epoll_fd); close(server_fd); exit(EXIT_FAILURE); } // 6. 主循环 while (1) { nfds = epoll_wait(epoll_fd, events, 10, -1); if (nfds < 0) { perror("epoll_wait error"); continue; } for (i = 0; i < nfds; i++) { if (events[i].data.fd == server_fd) { // 新连接 while (1) { client_len = sizeof(client_addr); client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; // 没有更多连接 } perror("accept failed"); break; } printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 设置非阻塞 set_nonblocking(client_fd); // 添加客户端到epoll(边缘触发) event.events = EPOLLIN | EPOLLET; event.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) { perror("epoll_ctl failed"); close(client_fd); } } } else { // 客户端数据(边缘触发,必须一次性读完) while (1) { n = read(events[i].data.fd, buffer, BUF_SIZE); if (n == 0) { // 客户端断开连接 printf("Client disconnected\n"); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL); close(events[i].data.fd); break; } else if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; // 没有更多数据 } perror("read error"); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL); close(events[i].data.fd); break; } else { // 回显数据 buffer[n] = '\0'; printf("Received: %s", buffer); send(events[i].data.fd, buffer, n, 0); } } } } } close(epoll_fd); close(server_fd); return 0; } ``` ## 5. select vs poll vs epoll对比表 | 特性 | select | poll | epoll | | ---------- | ------------------ | -------------- | --------------- | | fd数量限制 | 1024(FD_SETSIZE) | 无限制 | 无限制 | | 数据结构 | fd_set(位图) | pollfd数组 | 红黑树+就绪链表 | | 触发方式 | 水平触发 | 水平触发 | 支持边缘触发 | | 性能 | O(n) | O(n) | O(1) | | 内核实现 | 遍历所有fd | 遍历所有fd | 回调机制 | | 内存拷贝 | 每次拷贝fd_set | 每次拷贝pollfd | 仅首次添加 | | 适用场景 | 少量fd | 中等数量fd | 大量fd | | 跨平台 | 支持 | 支持 | 仅Linux | ## 6. 嵌入式场景 ### 6.1 多路传感器数据采集 ```c #include #include #include #include #include #define SENSOR_COUNT 4 int main() { int fds[SENSOR_COUNT]; fd_set read_fds; int max_fd = 0; int i, activity; char buffer[1024]; // 打开传感器设备 for (i = 0; i < SENSOR_COUNT; i++) { char device[64]; snprintf(device, sizeof(device), "/dev/sensor%d", i); fds[i] = open(device, O_RDONLY | O_NONBLOCK); if (fds[i] < 0) { perror("open sensor failed"); continue; } if (fds[i] > max_fd) { max_fd = fds[i]; } } // 主循环 while (1) { FD_ZERO(&read_fds); for (i = 0; i < SENSOR_COUNT; i++) { if (fds[i] > 0) { FD_SET(fds[i], &read_fds); } } activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL); if (activity < 0) { perror("select error"); continue; } for (i = 0; i < SENSOR_COUNT; i++) { if (fds[i] > 0 && FD_ISSET(fds[i], &read_fds)) { int n = read(fds[i], buffer, sizeof(buffer)); if (n > 0) { printf("Sensor %d data: %.*s\n", i, n, buffer); } } } } // 关闭传感器 for (i = 0; i < SENSOR_COUNT; i++) { if (fds[i] > 0) { close(fds[i]); } } return 0; } ``` ### 6.2 多客户端通信 ```c #include #include #include #include #include #include #include #define PORT 8888 #define MAX_EVENTS 10 #define BUF_SIZE 1024 int main() { int server_fd, client_fd, epoll_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len; struct epoll_event event, events[MAX_EVENTS]; char buffer[BUF_SIZE]; int i, nfds; // 创建服务器套接字 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 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 = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 监听 if (listen(server_fd, 5) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server listening on port %d\n", PORT); // 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd < 0) { perror("epoll_create1 failed"); close(server_fd); exit(EXIT_FAILURE); } // 添加服务器套接字到epoll event.events = EPOLLIN; event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) { perror("epoll_ctl failed"); close(epoll_fd); close(server_fd); exit(EXIT_FAILURE); } // 主循环 while (1) { nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds < 0) { perror("epoll_wait error"); continue; } for (i = 0; i < nfds; i++) { if (events[i].data.fd == server_fd) { // 新连接 client_len = sizeof(client_addr); client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { perror("accept failed"); continue; } printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 添加客户端到epoll event.events = EPOLLIN; event.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) { perror("epoll_ctl failed"); close(client_fd); } } else { // 客户端数据 int valread = read(events[i].data.fd, buffer, BUF_SIZE); if (valread == 0) { // 客户端断开连接 printf("Client disconnected\n"); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL); close(events[i].data.fd); } else { // 处理数据 buffer[valread] = '\0'; printf("Received from client %d: %s", events[i].data.fd, buffer); // 广播给其他客户端 for (int j = 0; j < nfds; j++) { if (events[j].data.fd != server_fd && events[j].data.fd != events[i].data.fd) { send(events[j].data.fd, buffer, valread, 0); } } } } } } close(epoll_fd); close(server_fd); return 0; } ``` ## 7. 注意事项 ### 7.1 性能考虑 - **epoll性能最好**:适合大量fd的场景 - **select/poll适合少量fd**:简单场景使用 - **边缘触发需要非阻塞fd**:避免阻塞导致事件丢失 - **合理设置超时**:避免无意义的等待 ### 7.2 编程要点 - **select需要每次重建fd_set**:注意FD_ZERO和FD_SET - **poll需要管理pollfd数组**:动态添加/删除fd - **epoll需要管理epoll_event**:正确设置events和data - **处理EAGAIN**:非阻塞模式下必须处理 ### 7.3 常见错误 - **fd数量超过限制**:select的FD_SETSIZE限制 - **忘记字节序转换**:端口和IP地址需要转换 - **未处理部分读写**:需要循环读写直到完成 - **内存泄漏**:忘记关闭fd ## 8. 面试要点 ### 8.1 常见问题 1. **select、poll、epoll的区别是什么?** - fd数量限制、性能、实现机制、适用场景 2. **epoll的LT和ET模式有什么区别?** - 通知机制、性能、编程复杂度、使用要求 3. **为什么epoll性能更好?** - 红黑树+就绪链表、回调机制、避免遍历 4. **如何处理部分读写?** - 循环读写、处理EAGAIN、使用非阻塞fd 5. **IO多路复用的适用场景有哪些?** - Web服务器、数据库、聊天服务器、嵌入式系统 ### 8.2 编程要点 - 记住三种IO多路复用的函数原型和参数 - 理解fd_set、pollfd、epoll_event的结构 - 掌握select/poll/epoll的使用流程 - 理解LT和ET模式的区别和要求 - 能够处理常见的错误情况 ### 8.3 性能优化 - 根据场景选择合适的IO多路复用方式 - 合理设置超时时间,避免无意义等待 - 使用边缘触发提高性能(需要非阻塞fd) - 批量处理事件,减少系统调用次数