I/O复用的概念
I/O复用是一种允许单个线程或进程同时监控多个文件描述符(如套接字、管道等)的技术,当其中任何一个文件描述符就绪(可读、可写或异常)时,程序可以立即处理。这种技术避免了为每个I/O操作创建单独的线程或进程,提高了资源利用率。
I/O复用技术的核心优势
I/O复用技术通过单线程或少量线程监控多个I/O流的状态变化,解决了传统阻塞I/O模型中"一连接一线程"的资源浪费问题。典型实现包括select、poll、epoll(Linux)和kqueue(BSD)等系统调用。
举个栗子:
假设你是一个快递站管理员,传统方式是你雇100个快递员,每人盯一个快递柜(一个客户),哪怕柜子空着,快递员也得傻等着。结果人太多,工资(内存)发不起,管理(CPU调度)也乱套。
但是I/O复用可以一个线程监控多个文件描述符大幅提高了资源的利用率
提升系统资源利用率
传统阻塞I/O需要为每个连接创建独立线程/进程,当并发连接数增长时,线程切换和内存消耗呈线性增长。I/O复用使单个线程能处理成千上万的连接,显著降低CPU和内存开销。例如在C10K问题中,复用技术可将线程数从10,000降至个位数。
应对高并发场景
现代网络应用常需处理大量并发连接(如即时通讯、实时交易系统)。I/O复用技术通过事件驱动机制,在连接有实际数据到达时才触发处理逻辑,避免空转等待。Nginx、Redis等高性能服务器均采用此模型实现百万级并发。
实现非阻塞操作
复用技术与非阻塞I/O结合时,程序能在等待I/O就绪期间执行其他任务。例如Web服务器在等待磁盘I/O时仍可处理新请求,这种能力在单线程Node.js架构中体现尤为明显。
精确控制超时机制
通过复用API的超时参数(如select的timeval),程序可统一管理所有连接的超时逻辑,避免为每个连接单独设置定时器。这在需要严格响应时间的金融系统中至关重要。
跨平台事件通知
不同操作系统提供各自的I/O复用接口(epoll/kqueue/IOCP),抽象层(如libevent)基于这些原语实现跨平台事件循环。开发者无需针对每个平台重写事件处理逻辑。
典型应用场景
- 需要维持大量空闲连接(如长轮询服务)
- 延迟敏感型应用(如在线游戏服务器)
- 资源受限环境(嵌入式系统)
- 需要混合处理网络和本地I/O(GUI事件循环)
I/O复用的函数
Select
select的系统调用的用途是:在一段时间内,监听用户感兴趣的文件描述符的可读,可写,和异常等事件,
select的函数调用如下
cpp
#include<sys/select.h>
int select(int maxfd,fd_set *readfds,fd_set *writefds,
fd_set* exceptfds,struct timeval *timeout);
select成功时返回就绪(可读,可写,异常)的文件描述符的总数,如果在超时时间内没有任何文件描述符的返回,select将返回0,select失败返回-1,如果select在等待时间接收到信号,则select立即返回-1;
函数参数
maxfd指定被监听的文件描述符的总数,通常被设定为select要监听的所有文件描述符中的最大值+1,就是相当于快递柜的总数
readfds,writefds,exceptfds分别指向可读可写,异常事件对应的文件描述符的合集,应用程序调用select可以通过这三个参数传入自己感兴趣的文件描述符对待,select返回时内核将修改他们来通知应用程序的哪些文件描述符已经就位
fd_set的定义如下

通过下列宏可以访问fd_set中的位,
cpp
FD_ZERO(fd_set *fdset);//清楚fd_set中的所有位
FD_SET(int fd, fd_set *fdset)//设置fd_set的位
FD_CLR(int fd,fd_set *fdset)//清楚fd_set的位fd
int FD_ISSET(int fd,fd_set *fdset)//测试fd_set里面的位fd是否被设置
timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。timeval 结构的定义如下:
cpp
struct timeval
{
long tv_sec; //秒数
long tv_usec; // 微秒数
};
如果给timeout的两个成员都是0,则select将立即返回。如果timeout传递 NULL,则select将一直阻塞,直到某个文件描述符就绪.

例子
实现select监控标准输入
eg.代码如下:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/select.h>
int main(){
int fd=0;//文件描述符标准输入
fd_set fdset;
struct timeval time={5,0};
while(1){
FD_ZERO(&fdset);//清空文件描述符集合
FD_SET(fd,&fdset);//将标准输入流加入到集合中
int n= select(2,&fdset,NULL,NULL,&time);//select监控标准输入
if(-1==n){
printf("ERROR");
continue;
}
else if(0==n){
printf("TIME OUT");
continue;
}
else{
if(FD_ISSET(fd,&fdset))//验证文件描述符是否为标准输入
{
char buf[128] ={0};
read (fd,buf,127);
printf("BUF : %s/n",buf);
}
}
sleep(1);
}
}
程序结果如图所示

利用selec实现TCP链接
服务器端代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include<assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/select.h>
int Init_Socked()//初始化服务器端套接字
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//使用IPV4的TCP链接
if (-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;//地址族为IPV4
saddr.sin_port = htons(9999);//端口号
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int n = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));//绑定套接字到指定的地址
if (-1 == n)
{
printf("bind arror");
exit(1);
}
n = listen(sockfd, 5);//监听套接字最大监听队列为5
if (-1 == n)
return -1;
return sockfd;
}
void InitFds(int fds[], int n)//初始化记录服务器记录服务器套接字的数组
{
int i = 0;
for (; i < n; i++)
{
fds[i] = -1;
}
}
void AddFdToFds(int fds[], int fd, int n)//将套接字描述符加入到数组中
{
int i = 0;
for (; i < n; i++)
{
if (fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
void DelFdFromFds(int fds[], int fd, int n)//删除数组中的套接字描述符
{
int i = 0;
for (; i < n; i++)
{
if (fds[i] == fd)
{
fds[i] =-1;
break;
}
}
}
int SetFdToset(fd_set *fd_set, int fds[], int n)
//将数组中的套接字描述符设置到fd_set变量中,并返回最大的文件描述符值
{
FD_ZERO(fd_set);
int i = 0, maxfd = fds[0];
for (; i < n; i++)
{
if (fds[i] != -1)
{
FD_SET(fds[i], fd_set);
if (fds[i] > maxfd)
{
maxfd = fds[i];
}
}
}
return maxfd;
}
//链接客户端和服务器
void GetClientLink(int sockfd, int fds[], int n)
{
int c = accept(sockfd, NULL, NULL);
if (c < 0)
{
return;
}
printf("SUCCESS TO LINK");
AddFdToFds(fds, c, n);
}
//处理客户端的数据
void DealClientData(int fds[], int n, int clifd)
{
char data[1024] = {0};
int num = recv(clifd, data, 1024 - 1, 0);
if (num <= 0)
{
DelFdFromFds(fds, clifd, n);
close(clifd);
printf("A Client disconnected");
}
else
{
printf("%d : %s \n", clifd, data);
send(clifd, "OK", 2, 0);
}
}
//处理数据函数
void DealReadyEvent(int fds[], int n, fd_set *fdset, int sockfd)
{
int i = 0;
for (; i < n; ++i)
{
if (fds[i] != -1 && FD_ISSET(fds[i], fdset))//当文件描述符存在并且在fdset集合中
{
if (fds[i] == sockfd)
{
GetClientLink(sockfd, fds, n);//就绪的是服务器端的套接字说明有新的客户端链接
}
else
{
DealClientData(fds, n, fds[i]);
}
}
}
}
int main()
{
int sockfd = Init_Socked();
assert(sockfd!= -1);
fd_set readfds;
int fds[128];
InitFds(fds,128);
AddFdToFds(fds,sockfd,128);//将服务器端套接字加入到数组监听数组中
while(1){
int maxfd = SetFdToset(&readfds,fds,128);
struct timeval timeout;
timeout.tv_sec= 2;
timeout.tv_usec=0;
int n =select(maxfd+1,&readfds,NULL,NULL,&timeout);
if(n<0){
printf("select error");
break;
}
else if(n==0)
{
printf("time out");
continue;
}
DealReadyEvent(fds,128,&readfds,sockfd);
}
exit (0);
}
程序结构流程图如下
客户端代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/select.h>
int Init_Socked()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr, caddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
return -1;
return sockfd;
}
int main()
{
int sockfd = Init_Socked();
if(-1==sockfd){
return -1;
}
while(1)
{
printf("please InPut:");
char buff[128] = {0};
fgets(buff, 128, stdin);
if (strncmp(buff, "bye", 3) == 0)
{
break;
}
int n = send(sockfd, buff, strlen(buff) - 1, 0);
if (n <= 0)
{
printf("send Error");
break;
}
memset(buff, 0, 128);
n = recv(sockfd, buff, 127, 0);
if (n <= 0)
{
printf("recv error");
break;
}
printf("%s\n", buff);
}
close(sockfd);
exit(0);
}
结果如下


poll
poll函数调用和select类似,也是在指定的时间内轮询一定数量的文件描述符,以测试其中是否有就绪者
函数参数
cpp
#include<poll.h>
int poll(struct poll*fds,nfds_t nfds,int timeout);
poll系统调用成功后返回文件描述符的总数,超时返回0,失败返回-1
nds参数指定被监听事件的集合fds的大小
timeout参数指定poll的超时值,单位是毫秒,timeout为-1时,poll将永久阻塞
fds参数是一个struct pollfd结构类型的数组
cpp
struct pollfd
{
int fd;
short events;
short revents;
};
//fd为成员的指定文件描述符,events成员告诉poll监听fd上的哪些事件类型.
poll支持的事件类型

POLLIN读事件是接收缓冲区有数据了就有事件发生,POLLOUT写事件是发送缓冲区只要空着,还有空间,就会有事件发生,所以一开始发送缓冲区空着就会有写事件发生。这个写事件是应用于发送大量数据,但发送缓冲区没有呢么大,如果发送缓冲区满着,send就会阻塞住,所以可以用写事件的发生来检测发送缓冲区是否有空间可以写数据。
如何知道该事件发生?
因为struct pollfd结构体的第二个成员和第三个成员是一个短整型,用一位表示相应的事件,所以我们可以用实际发生的事件&用户关心的事件,看结果是否为真,revent&POLLIN。
利用poll实现TCP链接
服务器端代码
cpp
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>
#include <poll.h>
int Init_Socked()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr, caddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int n = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == n)
{
printf("bind arror");
exit(1);
}
n = listen(sockfd, 5);
if (-1 == n)
return -1;
return sockfd;
}
void InitPollFds(struct pollfd fds[])
{
int i = 0;
for (; i < 128; i++)
{
fds[i].fd = -1;
}
}
void InsertFdToPollfds(struct pollfd fds[], int fd, short event)
{
int i = 0;
for (; i < 128; i++)
{
if (fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = event;
break;
}
}
}
void GetClientLink(struct pollfd fds[], int sockfd)
{
struct sockaddr_in caddr;
memset(&caddr, 0, sizeof(caddr));
socklen_t len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (c < 0)
{
return;
}
printf("A client SUCCESSFUL");
InsertFdToPollfds(fds, c, POLLIN | POLLRDHUP);
}
int DealClientData(int clifd)
{
char data[1024] = {0};
int n = recv(clifd, data, 1024 - 1, 0);
if (n <= 0)
{
return -1;
}
printf("%d : %s\n", clifd, data);
send(clifd, "OK", 2, 0);
return 0;
}
int DealReadyEvent(struct pollfd fds[], int sockfd)
{
int i = 0;
for (; i < 128; i++)
{
if (fds[i].fd == -1)
{
continue;
}
else if (fds[i].revents & POLLRDHUP)
{
close(fds[i].fd);
fds[i].fd = -1;
printf("A Client disconnect");
continue;
}
else if (fds[i].revents & POLLIN)
{
if (fds[i].fd == sockfd)
{
GetClientLink(fds, sockfd);
}
else
{
if (-1 == DealClientData(fds[i].fd))
{
close(fds[i].fd);
fds[i].fd = -1;
printf("A client disconnected");
}
}
}
else
{
printf("ERROR");
}
}
}
int main()
{
int sockfd = Init_Socked();
assert(sockfd != -1);
struct pollfd fds[128];
InitPollFds(fds);
InsertFdToPollfds(fds, sockfd, POLLIN);
while (1)
{
int n = poll(fds, 128, 2000);
if (n < 0)
{
printf("Poll error\n");
continue;
}
else if (n == 0)
{
printf("timeout");
continue;
}
else
{
DealReadyEvent(fds, sockfd);
}
}
exit(0);
}
注意:
由于POLLRDHUP不是 POSIX 标准,GCC 编译器默认不暴露该 Linux 特有宏,必须通过定义_GNU_SOURCE来启用 GNU 扩展特性,才能让编译器识别该宏。
结果如下:
epoll
epoll是Linux独有的I/O复用函数,在实现上与select和poll有很大的差异,epoll使用一组函数而不是单个的函数,而且epoll把用户关心的文件描述符上的事件放在内核里面的一个事件表中,无需像select和poll那样每次调用都传入文件描述符或者事件集.
相关函数
epoll_creat()
用于创建内核的事件表
epoll_ctl()用于创建内核事件表
epoll_wait()用于在一段超时间内等待一组文件描述符上的事件
epoll_create(int size);
cpp
#include<sys/epoll.h>
int epoll_create(int size);//创建成功会返回内核事件表的文件描述符失败返回-1
epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
cpp
#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,struct epoll_event* event);
//成功返回0,失败返回-1
//epfd指定要操作的内核事件表的文件描述符
//fd指定要操作的文件描述符
//op指定操作的类型
//EPOLL_CTL_ADD 在内核事件表上注册fd上的事件
//EPOLL_CTL_MOD 修改fd上的注册事件
//EPOLL_CTL_DEL 删除fd上的注册事件
//epoll_event定义如下
struct epoll_event
{
_uint32_t events;//epoll事件
epoll_data_t data;//用户数据
}
