目录
[一、select 方法概述](#一、select 方法概述)
[二、select 系统调用原型](#二、select 系统调用原型)
[三、select 的基本原理](#三、select 的基本原理)
[四、使用 select 的步骤](#四、使用 select 的步骤)
[六、select 的优缺点](#六、select 的优缺点)
一 、select 方法概述
select 是 Unix/Linux 系统中一种经典的 I/O 多路复用技术,允许程序监视多个文件描述符(如套接字、管道等),直到其中一个或多个描述符准备好进行 I/O 操作(如可读、可写或发生异常)。select 适用于需要同时处理多个 I/O 通道的场景,如网络服务器、客户端等。
二、select 系统调用原型
cpp
#include <sys/select.h>
int select( int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,struct timeval* timeout);
参数设置:
1、nfds参数:指定被监听的文件描述符的总数。它通常被设置为select 监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。
2、readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这3个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。一般只关注读,后面两个参数设置为空指针。这三个参数是fd_set结构指针类型。
3、timeout:代表超时时间;设为 NULL 表示无限阻塞,设为 0 表示非阻塞轮询。
三、select 的基本原理
select 通过三个文件描述符集合(fd_set)来监视不同类型的 I/O 事件:
- readfds:监视描述符是否可读(如数据到达、连接关闭)。
- writefds:监视描述符是否可写(如发送缓冲区空闲)。
- exceptfds:监视描述符是否发生异常(如带外数据到达)。
调用 select 时,内核会阻塞进程,直到至少一个描述符就绪或超时。返回后,内核会修改这些集合,仅保留就绪的描述符。
给select传入一个集合,会返回告诉这个集合中有多少个就绪,集合中就绪对应的位会被设置成1,没有就绪就会设置成0。最多可以检测1024给描述符。同时select为周期性的调用。

四、使用 select 的步骤
cpp
int main(){
int fd=STDIN;//输入
fd_set fdset;
while (1)//把描述符添加到集合中
{
FD_ZERO(&fdset);//清空集合
FD_SET(fd,&fdset);//将描述符添加到集合里
struct timeval tv={5,0};//设置超时时间,时间每次重置
int n=select(fd+1,&fdset,NULL,NULL,&tv);//阻塞,最多阻塞5s
if(n==-1){
printf("select err\n");
}else if(n==0){//超时
printf("time out\n");
}else{
if(FD_ISSET(fd,&fdset))//真,fd有读事件发生
{
char buff[128]={0};
read(fd,buff,127);
printf("read:%s\n",buff);
}
}
}
初始化描述符集合: 使用 FD_ZERO 清空集合,FD_SET 添加需要监视的描述符。
调用 select 传入初始化后的集合和超时时间。select 会阻塞直到事件就绪或超时。
检查返回值
- 返回 -1 表示错误(如被信号中断)。
- 返回 0 表示超时。
- 返回正数表示就绪的描述符数量。
处理就绪的描述符 使用 FD_ISSET 检查哪些描述符就绪,并执行相应操作
五、将select应用到tcp服务端
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define MAXFD 10
int socket_init();
void fds_init(int fds[]){//初始化数组
for(int i = 0; i < MAXFD; i++){
fds[i] = -1;
}
}
//添加
void fds_add(int fds[], int fd){
for(int i = 0; i < MAXFD; i++ ){
if( fds[i] == -1){
fds[i] = fd;
break;//当找到一个-1,就停止
}
}
}
//移除
void fds_del(int fds[], int fd){
for(int i = 0; i < MAXFD; i++){
if( fds[i] == fd){
fds[i] = -1;
break;
}
}
}
int main(){
int sockfd = socket_init();//套接字
if( sockfd == -1){
exit(1);
}
int fds[MAXFD];//数组
fds_init(fds);//-1,初始化数组,为空
fds_add(fds,sockfd);//添加描述符
fd_set fdset;//select
while( 1 ){
FD_ZERO(&fdset);//清空
int maxfd = -1;
//将所有描述符添加到集合中
for(int i = 0; i < MAXFD; i++){
if( fds[i] == -1){//找到有效的
continue;
}
FD_SET(fds[i],&fdset);//添加
if( maxfd < fds[i]){//找到最大的描述符
maxfd = fds[i];
}
}
struct timeval tv = {5,0};//超时时间
int n = select(maxfd+1,&fdset,NULL,NULL,&tv);//阻塞,5s
if( n == -1){
printf("select err\n");
}else if( n == 0 ){//超时
printf("time out\n");
}else{//有n个描述符就绪,遍历整个数组找到有效的
for(int i = 0; i < MAXFD; i++){
if( fds[i] == -1){//无效描述符
continue;
}
if( FD_ISSET(fds[i],&fdset)){//真,有事件发生,检测有无事件发生
//判断套接字类型
if( fds[i] == sockfd ){//监听套接字 accept
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
if( c < 0 ){
continue;
}
printf("accept c=%d\n",c);//接受链接
fds_add(fds,c); //把c加入到数组中,
/*因为等待该循环将事件监测完毕,进入下一轮循环时,又重新清空集合,
将数组的所有元素重新添加至集合,此时c也被加入到集合中,
再次进行select检测,下一轮就有两个描述符以供检测*/
}
else{//连接套接字有数据 recv
char buff[128] = {0};
int num = recv(fds[i],buff,127,0);
if( num <= 0 ){
close(fds[i]);
fds_del(fds,fds[i]);
printf("close\n");
}
else{
printf("recv:%s\n",buff);
send(fds[i],"ok",2,0);
}
}
}
}
}
}
}
int socket_init(){//创建监听套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1){//创建套接字失败
return -1;
}
struct sockaddr_in saddr;//ipv4专用,制定ip端口
memset(&saddr,0,sizeof(saddr));//清空
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定,通用套接字
if( res == -1){
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);//监听队列
if( res == -1){
return -1;
}
return sockfd;
}
客户端:

服务端:

六、select 的优缺点
优点
-
跨平台支持,几乎在所有 Unix-like 系统上可用。
-
实现简单,适合少量连接或对性能要求不高的场景。
缺点
-
文件描述符数量受限(通常为 1024),高并发场景性能较差。
-
每次调用需重新传入描述符集合,内核和用户空间频繁拷贝数据。
-
线性扫描所有描述符,效率随描述符数量增加而下降。