select系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。
select 机制的优势
为什么会出现select模型?
先看一下下面的这句代码:
int iresult = recv(s, buffer,1024);
这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。
再看代码:
int iresult = ioctlsocket(s, fiobio, (unsigned long *)&ul);iresult = recv(s, buffer,1024);
这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:wsaewouldblock,意思就是请求的操作没有成功完成。
看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。
select模型的出现就是为了解决上述问题。
select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。
如上所示,用户首先将需要进行io操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。
从流程上来看,使用select函数进行io请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的io请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个io请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
select流程伪代码如下:
{ select(socket); while(1) { sockets = select(); for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } }}
select相关api介绍与使用
#include #include #include #include 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的整数,这个整数表示就绪描述符的数目。
以下介绍与select函数相关的常见的几个宏:
#include int fd_zero(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0int fd_clr(int fd, fd_set *fdset); //清除某个位时可以使用int fd_set(int fd, fd_set *fd_set); //设置变量的某个位置位int fd_isset(int fd, fd_set *fdset); //测试某个位是否被置位
select使用范例:
当声明了一个文件描述符集后,必须用fd_zero将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如下:
fd_set rset; int fd; fd_zero(&rset); fd_set(fd, &rset); fd_set(stdin, &rset);
然后调用select函数,拥塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行。
select(fd+1, &rset, null, null,null);
select返回后,用fd_isset测试给定位是否置位:
if(fd_isset(fd, &rset) { ... //do something }
下面是一个最简单的select的使用例子:
#include #include #include #include #include int main(){ fd_set rd; struct timeval tv; int err; fd_zero(&rd); fd_set(0,&rd); tv.tv_sec = 5; tv.tv_usec = 0; err = select(1,&rd,null,null,&tv); if(err == 0) //超时 { printf(select time out!\n); } else if(err == -1) //失败 { printf(fail to select!\n); } else //成功 { printf(data is available!\n); } return 0;}
我们运行该程序并且随便输入一些数据,程序就提示收到数据了。
深入理解select模型:
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; fd_zero(&set); 则set用位表示是0000,0000。
(2)若fd=5,执行fd_set(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行fd_isset判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(fd_zero最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环加fd,取maxfd,select返回后利用fd_isset判断是否有事件发生。
用select处理带外数据
网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。
什么是带外数据?
带外数据(out—of—band data),有时也称为加速数据(expedited data),
是指连接双方中的一方发生重要事情,想要迅速地通知对方。
这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。
带外数据设计为比普通数据有更高的优先级。
带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。
我们写的select程序经常都是用于接收普通数据的,当我们的服务器需要同时接收普通数据和带外数据,我们如何使用select进行处理二者呢?
下面给出一个小demo:
#include #include #include #include #include #include #include #include #include #include int main(int argc, char* argv[]){ if(argc <= 2) { printf(usage: ip address + port numbers\n); return -1; } const char* ip = argv[1]; int port = atoi(argv[2]); printf(ip: %s\n,ip); printf(port: %d\n,port); int ret = 0; struct sockaddr_in address; bzero(&address,sizeof(address)); address.sin_family = af_inet; inet_pton(af_inet,ip,&address.sin_addr); address.sin_port = htons(port); int listenfd = socket(pf_inet,sock_stream,0); if(listenfd < 0) { printf(fail to create listen socket!\n); return -1; } ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address)); if(ret == -1) { printf(fail to bind socket!\n); return -1; } ret = listen(listenfd,5); //监听队列最大排队数设置为5 if(ret == -1) { printf(fail to listen socket!\n); return -1; } struct sockaddr_in client_address; //记录进行连接的客户端的地址 socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength); if(connfd < 0) { printf(fail to accept!\n); close(listenfd); } char buff[1024]; //数据接收缓冲区 fd_set read_fds; //读文件操作符 fd_set exception_fds; //异常文件操作符 fd_zero(&read_fds); fd_zero(&exception_fds); while(1) { memset(buff,0,sizeof(buff)); /*每次调用select之前都要重新在read_fds和exception_fds中设置文件描述符connfd,因为事件发生以后,文件描述符集合将被内核修改*/ fd_set(connfd,&read_fds); fd_set(connfd,&exception_fds); ret = select(connfd+1,&read_fds,null,&exception_fds,null); if(ret < 0) { printf(fail to select!\n); return -1; } if(fd_isset(connfd, &read_fds)) { ret = recv(connfd,buff,sizeof(buff)-1,0); if(ret <= 0) { break; } printf(get %d bytes of normal data: %s \n,ret,buff); } else if(fd_isset(connfd,&exception_fds)) //异常事件 { ret = recv(connfd,buff,sizeof(buff)-1,msg_oob); if(ret <= 0) { break; } printf(get %d bytes of exception data: %s \n,ret,buff); } } close(connfd); close(listenfd); return 0;}
用select来解决socket中的多客户问题
上面提到过,,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的io请求。在网络编程中,当涉及到多客户访问服务器的情况,我们首先想到的办法就是fork出多个进程来处理每个客户连接。现在,我们同样可以使用select来处理多客户问题,而不用fork。
服务器端
#include #include #include #include #include #include #include #include int main() { int server_sockfd, client_sockfd; int server_len, client_len; struct sockaddr_in server_address; struct sockaddr_in client_address; int result; fd_set readfds, testfds; server_sockfd = socket(af_inet, sock_stream, 0);//建立服务器端socket server_address.sin_family = af_inet; server_address.sin_addr.s_addr = htonl(inaddr_any); server_address.sin_port = htons(8888); server_len = sizeof(server_address); bind(server_sockfd, (struct sockaddr *)&server_address, server_len); listen(server_sockfd, 5); //监听队列最多容纳5个 fd_zero(&readfds); fd_set(server_sockfd, &readfds);//将服务器端socket加入到集合中 while(1) { char ch; int fd; int nread; testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量 printf(server waiting\n); /*无限期阻塞,并测试文件描述符变动 */ result = select(fd_setsize, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //fd_setsize:系统默认的最大文件描述符 if(result < 1) { perror(server5); exit(1); } /*扫描所有的文件描述符*/ for(fd = 0; fd 连接服务器->发送a->等待服务器回复->收到b->再发b给服务器->收到c->结束
服务器:启动->select->收到a->发a+1回去->收到b->发b+1过去
测试:我们先运行服务器,再运行客户端
select总结:
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1、单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以cat/proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历fd_setsize个socket来完成调度,不管哪个socket是活跃的,都遍历一遍。这会浪费很多cpu时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
拆解海康威视摄像头DS-2SCD2612F4
走秀网CEO被抓!走秀网创始人涉走私潜逃国外被抓!是电商难做还是心术走偏?
具有输出滤波器的RFID阅读器ASK接收器图
瓦肯Vulcan122RGB机械键盘高清图集
iQOO新机性能参数曝光搭载骁龙855处理器相机支持超级HDR
需要了解Linux编程中的select
机器人产业纳入2030的国家重大项目 产业风口来袭
佳能推出FPA-1200NZ2C纳米压印半导体制造设备
生物医疗市场前景将大有可为
新型VDM28激光测距传感器的应用优势介绍
如何使用MAX31875温度传感器测量温度
为什么说全光谱LED能拯救近视危机?靠的是什么?
人工智能在能源行业的5个应用方向和对应的案例
iPhone SE价格?比国产中端机卖的便宜,你心动了吗?
安捷伦S系列示波器硬件设计详解
“物联网芯片”推进边缘计算发展
汽车连接器接插问题
传三星显示将韩国8代LCD产线设备出售给TCL华星
如何使用绝对寻址组态地址指针?
震撼人心的降噪蓝牙耳机来了,先进声学技术,带来非凡音质体验