前提了解:
1.fd_set结构体,fd_set是long型的数组,提供给select()机制使用的一种数据结构,每1位表示1个文件描述符。
fd_set是一个长度为64的数组,
fd_set readfds; //监视可读文件描述符的集合,监测读取是不是阻塞了。
fd_set writefds; //监视可写文件描述符的集合,监测写入是不是阻塞了。
fd_set exceptfds; //监视发生错误异常文件。
2.void FD_ZERO(fd_set *set); //清空集合中的文件描述符,将每一位都设置为0;
FD_ZERO(&readfds); //清空将要监测的可读文件描述符集合,将每一位都置为0;
FD_ZERO(&writefds); //清空将要监测的可写文件描述符集合,将每一位都置为0;
3.void FD_SET(int fd, fd_set *set); //添加一个文件描述符,将set中的某一位设置成1;
FD_SET(socketFd, &readfds); 添加socket()申请得到的文件描述符到readfds集合中。
4.int FD_ISSET(int fd, fd_set *set); //测试一个文件描述符是否是集合中的一员
FD_ISSET(socketFd, &readfds);
5.void FD_CLR(int fd, fd_set *set); //清除某一个被监视的文件描述符。
FD_CLR(socketFd, &readfds);
举例:
fd_set readfds;
fd_set writefds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(1, &readfds);
FD_SET(3, &readfds);
FD_SET(5, &writefds);
select(6, &readfds, &writefds, NULL, timeout);
假如文件描述符0、1、2、3分别满足条件,那么select返回值为多少?
因为maxfd为6,所以select会监控文件描述符[0-5],下面逐步分析:
文件描述符0:虽然0满足条件,但是没有加入到readfds或writfds集合中,因此select不会监控文件描述符0,同理还有文件描述符2、4。
文件描述符1、3:文件描述符1、3满足条件且被加入到了readfds集合中,因此select会返回他们。
文件描述符5:虽然文件描述符5加入到了writefd,但是不满足条件,所以不会返回文件描述符5.
综上所述,文件描述符5会返回值2。
意思是select只会检测集合已经添加的文件描述符中有多少是符合条件的。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds:需要监控的文件描述符最大值+1,例如,如果监控的文件描述符是0、1、2,那么nfds就应该是3.
readfds:指向fd_set的指针,表示需要监控可读状态的文件描述符集合。如果不需要监控可读状态,写为NULL。
writefds:指向fd_set的指针,表示需要监控可写状态的文件描述符集合。如果不需要监控可读状态,写为NULL。
exceptfds:指向fd_set的指针,表示需要监控异常状态的文件描述符集合。如果不需要监控可读状态,写为NULL。
timeout:指向 struct timeval 的指针,表示 select() 的超时时间。如果 timeout 为 NULL,select() 会一直阻塞,直到有文件描述符准备好。如果 timeout 设置为 0,select() 会立即返回,用于轮询(polling)。
return:大于0:表示准备好的文件描述符的数量(前提是加入到readfds、writefds、exceptfds的Fd中满足的文件描述符数量)。
==0:表示超时,没有文件描述符准备好。
==-1:表示出错,错误原因存储在errno中。
1.select() 是 Linux 中用于 I/O 多路复用 的系统调用。它可以同时监控多个文件描述符(file descriptors),检查它们是否处于可读、可写或异常状态。
主要用途是让程序能同时处理多个 I/O 操作,而不需要为每个文件描述符创建单独的线程或进程。
1.select() 可以监控以下三种类型的文件描述符集合:
可读集合(readfds):检查文件描述符是否有数据可读。
可写集合(writefds):检查文件描述符是否可写。
异常集合(exceptfds):检查文件描述符是否发生异常。
2.select() 会阻塞程序,直到以下情况之一发生:
至少一个被监控的文件描述符准备好。
超时时间到达。
被信号中断。
2.select机制:
select() 的内部实现是通过遍历文件描述符集合来检查每个文件描述符的状态。为了提高效率,select() 只会检查从 0 到 nfds - 1 的文件描述符。
然后把所有符合条件的文件描述符ID号写进提前设置的fd_set集合中。
最后再调用FD_ISSET(socketFd, &readfds);去检查socketFd是否在select筛出的集合中,一般详细流程如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int ClientConnect(int timeout)
{
if(isConnected){
return 0;
}
clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(clientSocket < 0){
isConnected = -1;
return -1;
}
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(serverIP);
serv_addr.sin_port = htons(serverPort);
int flags = fcntl(clientSocket, F_GETFL, 0);
if(flags < 0){
printf("Get Flags Error!\n");
close(clientSocket);
isConnected = false;
return -1;
}
fd_set fdw;//申请1个fd_set集合,用于select存放满足条件描述符ID。
struct timeval stTimeout;
int rc = connect(clientSocket, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if(rc != 0){
if(errno == EINPROGRESS){
printf("Doing Connection.\n");
//establish connection.
FD_ZERO(&fdw);// 清空集合
FD_SET(clientSocket, &fdw);// 添加到集合中
if(timeout != -1){
stTimeout.tv_sec = timeout / 1000;
stTimeout.tv_usec = (timeout % 1000) * 1000;
}
rc = select(clientSocket + 1, NULL, &fdw, NULL, &stTimeout);
if(rc < 0){
fprintf(stderr, "Connect Error:%s\n", strerror(errno));
close(clientSocket);
isConnected = false;
return -1;
}
else if(rc == 0){//connect timeout.
fprintf(stderr, "Connect Timeout.\n");
close(clientSocket);
isConnected = false;
return -1;
}
else{
int err = 0;
uint32_t errlen = sizeof(err);
if(getsockopt(clientSocket, SOL_SOCKET, SO_ERROR, &err, &errlen) != 0){
fprintf(stderr, "getsockopt(SO_ERROR): %s", strerror(errno));
close(clientSocket);
isConnected = false;
return -1;
}
if(err){
errno = err;
fprintf(stderr, "Connect Error:%s\n", strerror(errno));
close(clientSocket);
isConnected = false;
return -1;
}else{
isConnected = true;
return 0;
}
}
}
else{
fprintf(stderr, "Connect Failed, error: %s.\n", strerror(errno));
close(clientSocket);
isConnected = false;
return -1;
}
}
isConnected = true;
if(pthread_create(&workThreadID, NULL, worker, NULL) != 0){
printf("Create Worker Thread Failed.\n");
}
printf("connect success\n");
return 0;
}
疑问:
1.正常的流程是FD_ZERO清除、FD_SET添加Fd,select监测,最后再调用FD_ISSET检查文件描述符Fd是否在集合中,那么FD_SET不是已经把文件描述符Fd添加到集合中了吗?
select()监控集合中的文件描述符,并修改集合,只保留已经准备好的文件描述符。因此在调用 select() 后,需要调用FD_ISSET再次检查哪些文件描述符仍然在集合中(即哪些文件描述符已经准备好)。
2.对于套接字而言,为什么要检测read属性?
检测 read 属性(即可读状态)的主要目的是 判断文件描述符是否有数据可读,对于 TCP 套接字,如果对方发送了数据,套接字会变为可读状态。
3.fcntl和select的区别是什么,fcntl不是也可以获取文件描述符的状态吗
fcntl(文件控制)是主要功能包括
获取或设置文件描述符的标志(如 O_NONBLOCK 非阻塞模式)。
获取或设置文件锁(如 F_SETLK、F_GETLK)。
复制文件描述符(如 F_DUPFD)。
获取文件状态标志(如 F_GETFL)。
4.为什么不直接检查1个具体的文件描述符,而是用遍历的方式去检查。
这是为了支持 I/O 多路复用,即一个程序可以同时处理多个 I/O 操作,因为可能存在需要同时检测多个文件描述符的情况。
浙公网安备 33010602011771号