目录
文件描述符简介
在进程中,每打开一个文件,操作系统就会创建相应的数据结构来描述这个文件,这就是描述文件的文件结构体,同时,在进程的PCB中,会有一个struct files_struct *files的指针,它实际上是指向一个指针数组,而这个指针数组中就存放了每个打开的文件的指针,而所谓的文件描述符,就是这个指针数组的下标,根据文件描述符就能找到对应的文件的指针,就能对该文件进行操作了。在文件描述符进行分配时,会找到当前没有被使用的最小的下标,作为新的文件描述符。其中,0,1,2分别对应标准输出文件描述符,标准输出文件描述符和标准错误文件描述符。因此当打开其他文件时,分配的文件描述符实际上是从3开始的。
select模型理解
在前面提到的多进程和多线程来实现服务端与客户端通信中,不管是多进程还是多线程,一般来说,每一个连接成功的文件描述符(socket)都需要一个进程/线程来进行监控,然后就需要不断的对每个进程/线程中的文件描述符进行轮询,例如accept、read等函数,如果没有连接请求、缓冲区中没有数据,它们就会一直阻塞住。这样一来一回就在反复切换进程/线程,系统的开销是非常大的。这两种并发模型都需要进程或者线程自己去等待。
还有一种就是I/O多路复用模型,select就是其中一种。之前多进程多线程是让进程和线程去阻塞等待,而现在则是直接让内核去等待,只需要主进程去直接询问内核哪些描述符准备好了即可。select模型的关键就是事先将感兴趣的文件描述符放到一个集合中,当用户进程调用了select,那么整个进程会被阻塞
,而同时,内核就会“监视”所有select感兴趣的文件描述符,当任何一个文件描述符中的数据准备好了,select就会返回
。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。
select缺点
1、内核中对select模型可监视的fd数量限制为1024,如果要修改这个数字就必须对内核重新编译;
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
select函数
函数原型
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
参数说明
maxfdp:被监听的文件描述符的最大值大1,因为文件描述符是从0开始计数的;
readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。
timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间
timeval结构体定义如下:
struct timeval
{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
};
返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。
fd_set 相关
int FD_ZERO(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0 (可理解为清空集合中的所有感兴趣描述符)
int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用 (可理解为清空集合中的某一个描述符)
int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位 (可理解为向集合中添加一个描述符)
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位 (可理解为判断一个描述符是否在集合中)
程序流程
1.绑定、监听.....
2.创建集合,由于调用select函数时,传入的集合参数在函数返回后可能会改变,因此创建一个集合来保存所有感兴趣的描述符allset,再创建一个集合rset用来作为select的调用参数;再创建一个数组client用来存放所有有效的描述符,并初始化各项为-1;
3.将监听描述符lfd加入allset,此时最大描述符maxfd = lfd;
4.创建while循环,将allset赋值给rset,将rset作为读集合参数,调用select函数开始阻塞等待;
5.select函数返回后,先判断监听描述符lfd是否还存在于rset中,判断方式为if(FD_SET(lfd,&rset))。
6.如果判断为真,说明有新连接,则调用accept函数新连接的文件描述符并存在变量connfd中,然后再将connfd加入allset中和client数组中;
7.然后处理除监听描述符以外的描述符。遍历client数组,查看有效描述符是否发生了读事件,判断方式为if(FD_SET(client[i],&rset));如果判断为真,说明有数据传来,就进行read和write操作;
8.继续下一次循环....
程序实例
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
#define SERV_IP "127.1.2.3"
#define SERV_PORT 8888
#define MAX_CONN 1024
int main()
{
sockaddr_in servaddr,clitaddr;
sockaddr_in clit_info[MAX_CONN]; //存放成功连接的客户端地址信息
int client[1024]; //存放成功连接的文件描述符
char buf[1024]; //读写缓冲区
int lfd; //用于监听
int connfd; //连接描述符
int readyfd; //保存select返回值
int maxfd = 0; //保存最大文件描述符
int maxi = 0; //maxi反映了client中最后一个成功连接的文件描述符的索引
socklen_t addr_len = sizeof(clitaddr);;
fd_set allset; //存放所有可以被监控的文件描述符
fd_set rset;
FD_ZERO(&allset);
FD_ZERO(&rset);
if((lfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
cout<<"creat socket fault : "<<strerror(errno)<<endl;
return 0;
}
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = inet_addr(SERV_IP);
if(bind(lfd,(sockaddr *)&servaddr,sizeof(servaddr)) == -1)
{
cout<<"bind fault : "<<strerror(errno)<<endl;
return 0;
}
if(listen(lfd,128) == -1)
{
cout<<"listen fault : "<<strerror(errno)<<endl;
return 0;
}
maxfd = lfd; //此时只用监控lfd,因此lfd就是最大文件描述符
//初始化client数组
for(int i=0;i<MAX_CONN;i++)client[i] = -1;
FD_SET(lfd,&allset);
cout<<"Init Success ! "<<endl;
cout<<"host ip : "<<inet_ntoa(servaddr.sin_addr)<<" port : "<<ntohs(servaddr.sin_port)<<endl;
cout<<"Waiting for connections ... "<<endl;
while(1)
{
rset = allset ; //rset作为select参数时,表示需要监控的所有文件描述符集合,select返回时,rset中存放的是成功监控的文件描述符。因此在select前后rset是可能改变的,所以在调用select前将rset置为所有需要被监控的文件描述符的集合,也就是allset
readyfd = select(maxfd+1,&rset,NULL,NULL,NULL); //服务端只考虑读的情况
//执行到这里,说明select返回,返回值保存在readyfd中,表示有多少个文件描述符被监控成功
if(readyfd == -1)
{
cout<<"select fault : "<<strerror(errno)<<endl;
return 0;
}
if(FD_ISSET(lfd,&rset)) //监听描述符监控成功,说明有连接请求
{
int i=0;
connfd = accept(lfd,(sockaddr *)&clitaddr,&addr_len); //处理新连接,此时accept直接可以返回而不用一直阻塞
if(connfd == -1)
{
cout<<"accept fault : "<<strerror(errno)<<endl;
continue ;
}
cout<<inet_ntoa(clitaddr.sin_addr)<<":"<<ntohs(clitaddr.sin_port)<<" connected ... "<<endl;
//成功连接后,就将connfd加入监控描述符表中
FD_SET(connfd,&allset);
for(;i<MAX_CONN;i++)
{
if(client[i] == -1)
{
client[i] = connfd;
clit_info[i] = clitaddr;
break;
}
}
if(connfd>maxfd)maxfd = connfd; //更新最大文件描述符
if(i>maxi)maxi = i;
readyfd --;
if(readyfd == 0)continue; //如果只有lfd被监控成功,那么就重新select
}
//处理lfd之外监控成功的文件描述符,进行轮询
for(int i=0;i<=maxi;i++)
{
if(client[i] == -1)continue; //等于-1说明这个描述符已经无效
if(FD_ISSET(client[i],&rset)) //在client数组中寻找是否有被监控成功的文件描述符
{
//此时说明client[i]对于的文件描述符监控成功,有消息发来,直接读取即可
int readcount = read(client[i],buf,sizeof(buf));
if(readcount == 0) //对方客户端关闭
{
close(client[i]); //关闭描述符
FD_CLR(client[i],&allset); //将该描述符从描述符集合中去除
client[i] = -1; //相应位置置为-1,表示失效
cout<<inet_ntoa(clit_info[i].sin_addr)<<":"<<ntohs(clit_info[i].sin_port)<<" exit ... "<<endl;
}
else if(readcount == -1)
{
cout<<"read fault : "<<strerror(errno)<<endl;
continue;
}
else
{
cout<<"(From "<<inet_ntoa(clit_info[i].sin_addr)<<":"<<ntohs(clit_info[i].sin_port)<<")";
for(int j=0;j<readcount;j++)cout<<buf[j];
cout<<endl;
for(int j=0;j<readcount;j++)buf[j] = toupper(buf[j]);
write(client[i],buf,readcount);
}
readyfd--;
if(readyfd == 0)break;
}
}
}
close(lfd);
return 0;
}