目录
前言
I/O多路复用(I/O Multiplexing)是一种在操作系统中处理多个I/O操作的技术,主要用于网络编程中处理多个网络连接或文件描述符的读写操作。I/O多路复用允许单个线程或进程同时监控多个I/O事件(如读、写、异常等),从而有效地管理多个并发的I/O操作,而不需要为每个I/O操作分配一个独立的线程或进程。
在Linux和类Unix系统中,I/O多路复用主要通过以下几种机制实现:
select
:最早的I/O多路复用接口。poll
:改进版的select
,可以处理更多的文件描述符。epoll
:Linux特有的高效I/O多路复用机制,适用于大规模并发连接。
一、select
select
是最早的I/O多路复用机制之一,用于在单个线程中监控多个文件描述符(如套接字、文件等)上的I/O事件(如可读、可写、异常等)。尽管select
已经被更现代的机制如poll
和epoll
所取代,但它依然是理解I/O多路复用基础原理的重要工具。
#include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <stdlib.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #define SERVERADDR "192.168.125.86" #define PORT 8888 int main(int argc, const char *argv[]) { // 创建套接字 int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd < 0) { perror("socket failed"); return -1; } // 绑定套接字 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); serveraddr.sin_addr.s_addr = inet_addr(SERVERADDR); if (bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0) { perror("bind failed"); return -1; } // 监听套接字 if (listen(listenfd, 20) < 0) { perror("listen failed"); return -1; } //定义文件描述符表 fd_set master_fds, read_fds; int max_fd = listenfd; //清空文件描述符表 FD_ZERO(&master_fds); //将监听套接字加入文件描述符表 FD_SET(listenfd, &master_fds); while (1) { read_fds = master_fds; // 监视文件描述符集,进程通知内核监视的内核是否准备就绪(是否有IO事件) int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL); if (activity < 0) { perror("select failed"); return -1; } // 处理新连接 if (FD_ISSET(listenfd, &read_fds)) { struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int client_fd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); if (client_fd < 0) { perror("accept failed"); continue; } printf("New connection from %s:%hu\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); //将新创建的客户端文件描述符加入到文件描述符表当中 FD_SET(client_fd, &master_fds); if (client_fd > max_fd) { max_fd = client_fd; } } // 处理已连接客户端的数据 for (int i = listenfd + 1; i <= max_fd; ++i) { if (FD_ISSET(i, &read_fds)) { char buf[100]; bzero(buf, 100); int ret = read(i, buf, 100); if (ret <= 0) { // 客户端关闭了连接 close(i); FD_CLR(i, &master_fds); } else { // 处理接收到的数据 printf("Received: %s\n", buf); write(i, buf, ret); } } } } close(listenfd); return 0; }
二、poll
poll
和select
的区别
文件描述符数量限制:
select
:对文件描述符数量有限制,通常是1024(可以通过编译时调整)。poll
:没有文件描述符数量的限制,仅受系统可用资源限制。接口设计:
select
:使用fd_set
结构体,通过宏操作(如FD_SET
、FD_CLR
等)对集合进行操作。poll
:使用pollfd
结构体数组,结构更加直观,不需要手动设置和重置集合。性能:
select
:每次调用都需要遍历所有的文件描述符,性能随着文件描述符数量增加而下降。poll
:同样需要遍历,但由于其数据结构设计上的优势,处理大量文件描述符时性能稍好。修改集合:
select
:每次调用都需要重新初始化fd_set
,因为select
会改变fd_set
的内容。poll
:直接修改pollfd
数组即可,无需像select
那样重新设置集合。
#include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <stdlib.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <poll.h> #define SERVERADDR "192.168.125.86" #define PORT 8888 #define MAX_CLIENTS 1024 int main(int argc, const char *argv[]) { // 创建监听套接字 int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd < 0) { perror("socket failed"); return -1; } // 绑定套接字 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); serveraddr.sin_addr.s_addr = inet_addr(SERVERADDR); if (bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0) { perror("bind failed"); return -1; } // 监听套接字 if (listen(listenfd, 20) < 0) { perror("listen failed"); return -1; } // 初始化pollfd数组 struct pollfd fds[MAX_CLIENTS]; for (int i = 0; i < MAX_CLIENTS; i++) { fds[i].fd = -1; // 初始化为-1表示未使用 } fds[0].fd = listenfd; fds[0].events = POLLIN; int nfds = 1; // 当前监控的文件描述符数量 while (1) { int activity = poll(fds, nfds, -1); // 阻塞等待事件 if (activity < 0) { perror("poll failed"); return -1; } // 处理新连接 if (fds[0].revents & POLLIN) { struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int client_fd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); if (client_fd < 0) { perror("accept failed"); continue; } printf("New connection from %s:%hu\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); // 将新连接的客户端添加到pollfd数组 for (int i = 1; i < MAX_CLIENTS; i++) { if (fds[i].fd == -1) { // 找到第一个空闲位置 fds[i].fd = client_fd; fds[i].events = POLLIN; if (i >= nfds) { nfds = i + 1; // 更新nfds } break; } } } // 处理已连接客户端的数据 for (int i = 1; i < nfds; i++) { if (fds[i].fd == -1) { continue; } if (fds[i].revents & POLLIN) { char buf[100]; bzero(buf, 100); int ret = read(fds[i].fd, buf, 100); if (ret <= 0) { // 客户端关闭了连接 close(fds[i].fd); fds[i].fd = -1; } else { // 处理接收到的数据 printf("Received: %s\n", buf); write(fds[i].fd, buf, ret); } } } } close(listenfd); return 0; }
三、epoll
epoll
的优点
-
高效性:
epoll
使用内核事件表来管理文件描述符,只通知实际发生事件的文件描述符,避免了select
和poll
中遍历所有文件描述符的开销。 -
扩展性:可以处理数以万计的并发连接,而
select
和poll
在文件描述符数量多时性能会显著下降。 -
边缘触发与水平触发:
epoll
支持边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT)模式,提供更灵活的事件通知机制。
#include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <stdlib.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <errno.h> #include <sys/epoll.h> #define SERVERADDR "192.168.125.86" #define PORT 8888 #define MAX_EVENTS 1024 int main(int argc, const char *argv[]) { // 创建监听套接字 int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd < 0) { perror("socket failed"); return -1; } // 绑定套接字 struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(PORT); serveraddr.sin_addr.s_addr = inet_addr(SERVERADDR); if (bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0) { perror("bind failed"); return -1; } // 监听套接字 if (listen(listenfd, 20) < 0) { perror("listen failed"); return -1; } // 创建epoll实例,使用epoll_create(0),0表示标准实例 int epollfd = epoll_create1(0); if (epollfd < 0) { perror("epoll_create1 failed"); return -1; } // 添加监听套接字到epoll实例 struct epoll_event event, events[MAX_EVENTS]; event.events = EPOLLIN; event.data.fd = listenfd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event) < 0) { perror("epoll_ctl: listenfd"); return -1; } while (1) { // 等待事件发生 int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds < 0) { perror("epoll_wait failed"); return -1; } // 处理事件 for (int i = 0; i < nfds; i++) { if (events[i].data.fd == listenfd) { // 处理新连接 struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); int client_fd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); if (client_fd < 0) { perror("accept failed"); continue; } printf("New connection from %s:%hu\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port)); // 将新客户端文件描述符添加到epoll实例 event.events = EPOLLIN; event.data.fd = client_fd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client_fd, &event) < 0) { perror("epoll_ctl: client_fd"); close(client_fd); } } else { // 处理客户端数据 int client_fd = events[i].data.fd; char buf[100]; bzero(buf, 100); int ret = read(client_fd, buf, 100); if (ret <= 0) { // 客户端关闭了连接 close(client_fd); epoll_ctl(epollfd, EPOLL_CTL_DEL, client_fd, NULL); } else { // 处理接收到的数据 printf("Received: %s\n", buf); write(client_fd, buf, ret); } } } } close(listenfd); close(epollfd); return 0; }