author: hjjdebug
date: 2026年 01月 25日 星期日 15:08:37 CST
descrip: select 函数详解.
文章目录
- 0: I/O 多路复用是什么意思 ??
- 1. select 函数可以同时支持多少路I/O ?
- 1.1. server_fd 是一个整数
- 1.2 read_fds 是什么?
- 1.3 read_fds 赋值.
- 2. 使用select 的注意事项.
- 2.1 timeout 值必需每次都要初始化.
- 2.2 fd_set 值必需每次都要初始化.
0: I/O 多路复用是什么意思 ??
如果一个程序需要用到多个I/O, 例如从键盘读数, 从3个socket 中读数,这就是4路I/O 读操作.
因为对I/O 的读写, read, write 是阻塞的, 当条件未满足时会被挂起, 为了保证各I/O 互相独立,
通常用多线程来应对各I/O 的读写. 对每一个I/O 都启动一个线程来进行读写.
那么能不能用一个线程来监测多个I/O 呢, 有数据了或可以写了再对不同的fd 操作,
这就是select 函数出场的时机. 就是一个线程看管多个I/O
于是我们明白
I/O多路复用, 就是多个I/O, 复用同一个线程的意思. 复用就是重复使用.
所以这种简化, 让人容易迷糊, 你会问 “谁复用?”, “I/O 复用是什么意思?” “什么叫I/O复用”
你还不如直接说, “I/O多路复用” 就是 “多路I/O只用一个线程监测”.
前者说得高大上不明不白, 就是词不达意, 后者简单直接一目了然.
1. select 函数可以同时支持多少路I/O ?
谁说了也不好使, 我从网上抄了一段代码, 我们调试一下看看吧.
测试代码:
$ cat main.c#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/select.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<fcntl.h>#include<errno.h>#defineMAX_CLIENTS100// 定义最大客户端数量,远大于16#definePORT8080#defineBUFFER_SIZE1024intmain(){intserver_fd,new_socket;structsockaddr_inaddress;intopt=1;intaddrlen=sizeof(address);intmax_fd;intclient_sockets[MAX_CLIENTS]={0};// 存储所有客户端套接字fd_set read_fds;// 用于存储当前需要监控的文件描述符集合// 创建服务器套接字if((server_fd=socket(AF_INET,SOCK_STREAM,0))==0){perror("socket failed");exit(EXIT_FAILURE);}// 设置套接字选项,允许重用地址if(setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))){perror("setsockopt");exit(EXIT_FAILURE);}address.sin_family=AF_INET;address.sin_addr.s_addr=INADDR_ANY;address.sin_port=htons(PORT);// 绑定套接字if(bind(server_fd,(structsockaddr*)&address,sizeof(address))<0){perror("bind failed");exit(EXIT_FAILURE);}// 开始监听if(listen(server_fd,3)<0){perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d\n",PORT);// 初始化客户端套接字数组for(inti=0;i<MAX_CLIENTS;i++){client_sockets[i]=0;}// 主循环while(1){// 清空读描述符集合FD_ZERO(&read_fds);// 将服务器套接字添加到集合中FD_SET(server_fd,&read_fds);max_fd=server_fd;// 将所有活跃的客户端套接字添加到集合中for(inti=0;i<MAX_CLIENTS;i++){if(client_sockets[i]>0){FD_SET(client_sockets[i],&read_fds);if(client_sockets[i]>max_fd){max_fd=client_sockets[i];}}}// 调用 select,阻塞等待事件intactivity=select(max_fd+1,&read_fds,NULL,NULL,NULL);if(activity<0){perror("select error");break;}// 检查是否有新连接到来if(FD_ISSET(server_fd,&read_fds)){if((new_socket=accept(server_fd,(structsockaddr*)&address,(socklen_t*)&addrlen))<0){perror("accept");continue;}printf("New connection, socket fd is %d\n",new_socket);// 找到一个空闲的客户端槽位inti;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, rejecting new connection\n");close(new_socket);}}// 检查所有客户端套接字是否有数据可读for(inti=0;i<MAX_CLIENTS;i++){intsock=client_sockets[i];if(sock>0&&FD_ISSET(sock,&read_fds)){charbuffer[BUFFER_SIZE]={0};intvalread=read(sock,buffer,BUFFER_SIZE-1);if(valread<=0){// 客户端断开连接printf("Client disconnected, socket fd %d\n",sock);close(sock);client_sockets[i]=0;}else{// 收到数据,回显给客户端printf("Received: %s from client %d\n",buffer,i);write(sock,buffer,strlen(buffer));}}}}return0;}调试:
1.1. server_fd 是一个整数
(gdb) p server_fd
$1 = 3
只所以server_fd 为3, 是因为0,1,2已经被系统占用了, 懂的都懂.
1.2 read_fds 是什么?
(gdb) p read_fds
$2 = {
__fds_bits = {0 <repeats 16 times>}
}
(gdb) ptype read_fds
type = struct {
__fd_mask __fds_bits[16];
}
(gdb) ptype __fd_mask
type = long
原来read_fds 是一个16个长整形数.
当时看到16, 就朦胧的联想到是不是只能检测16个I/O呢? 实际上不是这意思.
16位长整形,在64位机器上是1024个bit位, 它能够监测1024个I/O
下面解释为什么bit位就对应着fd.
1.3 read_fds 赋值.
FD_SET(server_fd, &read_fds);
(gdb) p read_fds
$3 = {
__fds_bits = {8, 0 <repeats 15 times>}
}
server_fd 为3, read_fds 在bit3位置置1,形成8.
FD_SET(client_sockets[i], &read_fds);
(gdb) p client_sockets[i]
$6 = 4
(gdb) p read_fds
$7 = {
__fds_bits = {24, 0 <repeats 15 times>}
}
24==0x18, 8是bit3的1, 0x10 是bit4的1, 对应client_socket[0]的描述符4
至此我们就明白了, 你监测的fd, 要转换位bitmap 上的对应的位置1.
至此, select 的工作原理就算说明白了.
所以说,64位机器上最多监测1024个描述符.
2. 使用select 的注意事项.
如果select 是在一个循环中调用, 注意.
2.1 timeout 值必需每次都要初始化.
否则你就搞不对.
测试程序没有使用timeout, 所以也就没有值的初始化问题了.
如果你使用了timeout, 则执行完select 后, 其值 timeout.tv_sec, timeout.tv_usec
就都变成0 了, 不初始化, 意味着第二次调用时其值为0,调用select时会立即返回超时.
这是我测试2秒超时, 程序一运行, 程序哗哗的打印超时发现的.
这里timeout 即是一个输入参数,也是一个输出参数,传递的是timeout地址.
说实话,这种设计不太好,容易让人犯错误,不过知道了也就无所谓了.
2.2 fd_set 值必需每次都要初始化.
否则你就读不到数据.
这里也是一个坑, 因为select 函数也是传递的fd_set的地址,它是输入参数,也是输出参数, 当select 返回时,保留的是准备就绪的描述符集.
而不再是你感兴趣的描述符集合了, 如果在循环中不初始化,你将得不到预期结果.
这就是参数即当输入,也当输出的风险, 我发现它.
是我用select 监测键盘输入, 我设置超时1秒,2秒,就不能从键盘获取输入,而设置超时
5,6,7,8秒就总能从键盘接受到数据, 为什么?
因为设置2秒钟超时, 我启动程序后,2秒内来不及从键盘输入字符. 例如输入hello还要敲回车,
结果超时,结果read_fds 此时是空的,结果再进入循环调用select, 由于read_fds 没有初始化,将不监测键盘, 所以收不到键盘输入.
而5,6,7,8秒, 我有足够时间反应,启动程序后又输入测试字符加回车键,select 执行收到字符
将返回准备好的read_fds, 这跟初始化的read_fds 数值是相等的, 都是bit1 置1,
所以下一轮执行select, read_fds 未进行初始化还是能接受到数据,
如果设置都正确, 别说2秒, 0.2秒,0.02秒超时都是可以的.
这次经历让我认识到了初始化的重要性.
这是函数参数设计的缺陷,即当输入也当输出,但好处是节省了参数个数.
这也无所谓好赖,必需要符合原设计者的约定. 这就是审题的必要性了. 所以对于一个新的函数,新的接口要了解其用途,这是学习成本.
所以若非必要,我们也不愿意学习新接口.
用select函数很久了,也只是用用而已,也没碰到什么问题,没有认真想过,
这次再用,自己手写时,碰到了一点问题,认真的思考了一下,
感觉算是搞懂了, 写篇博客吧, 也没多少内容, 就是select 函数本身,起名就叫select 函数详解吧.
连个函数原型都没有写也叫详解吗? 那些基本的东西大家搜一下就行了,我这就忽略了.
这里侧重的是实现原理和注意事项.