select系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。

select 机制的优势

为什么会出现select模型?

先看一下下面的这句代码:

  1. int iResult = recv(s, buffer,1024);

这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。

再看代码:

  1. int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
  2. 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流程伪代码如下:

  1. {
  2. select(socket);
  3. while(1)
  4. {
  5. sockets = select();
  6. for(socket in sockets)
  7. {
  8. if(can_read(socket))
  9. {
  10. read(socket, buffer);
  11. process(buffer);
  12. }
  13. }
  14. }
  15. }

select相关API介绍与使用

  1. #include <sys/select.h>
  2. #include <sys/time.h>
  3. #include <sys/types.h>
  4. #include <unistd.h>
  5. 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结构体定义如下:

  1. struct timeval
  2. {
  3. long tv_sec; /*秒 */
  4. long tv_usec; /*微秒 */
  5. };

返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。

以下介绍与select函数相关的常见的几个宏:

  1. #include <sys/select.h>
  2. int FD_ZERO(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0
  3. int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用
  4. int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位
  5. int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位

select使用范例:

当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如下:

  1. fd_set rset;
  2. int fd;
  3. FD_ZERO(&rset);
  4. FD_SET(fd, &rset);
  5. FD_SET(stdin, &rset);

然后调用select函数,拥塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行。

  1. select(fd+1, &rset, NULL, NULL,NULL);

select返回后,用FD_ISSET测试给定位是否置位:

  1. if(FD_ISSET(fd, &rset)
  2. {
  3. ...
  4. //do something
  5. }

下面是一个最简单的select的使用例子:

  1. #include <sys/select.h>
  2. #include <sys/time.h>
  3. #include <sys/types.h>
  4. #include <unistd.h>
  5. #include <stdio.h>
  6. int main()
  7. {
  8. fd_set rd;
  9. struct timeval tv;
  10. int err;
  11. FD_ZERO(&rd);
  12. FD_SET(0,&rd);
  13. tv.tv_sec = 5;
  14. tv.tv_usec = 0;
  15. err = select(1,&rd,NULL,NULL,&tv);
  16. if(err == 0) //超时
  17. {
  18. printf("select time out!\n");
  19. }
  20. else if(err == -1) //失败
  21. {
  22. printf("fail to select!\n");
  23. }
  24. else //成功
  25. {
  26. printf("data is available!\n");
  27. }
  28. return 0;
  29. }

我们运行该程序并且随便输入一些数据,程序就提示收到数据了。

深入理解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:

  1. #include <stdio.h>
  2. #include <sys/time.h>
  3. #include <sys/types.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. #include <string.h>
  9. #include <fcntl.h>
  10. #include <stdlib.h>
  11. int main(int argc, char* argv[])
  12. {
  13. if(argc <= 2)
  14. {
  15. printf("usage: ip address + port numbers\n");
  16. return -1;
  17. }
  18. const char* ip = argv[1];
  19. int port = atoi(argv[2]);
  20. printf("ip: %s\n",ip);
  21. printf("port: %d\n",port);
  22. int ret = 0;
  23. struct sockaddr_in address;
  24. bzero(&address,sizeof(address));
  25. address.sin_family = AF_INET;
  26. inet_pton(AF_INET,ip,&address.sin_addr);
  27. address.sin_port = htons(port);
  28. int listenfd = socket(PF_INET,SOCK_STREAM,0);
  29. if(listenfd < 0)
  30. {
  31. printf("Fail to create listen socket!\n");
  32. return -1;
  33. }
  34. ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
  35. if(ret == -1)
  36. {
  37. printf("Fail to bind socket!\n");
  38. return -1;
  39. }
  40. ret = listen(listenfd,5); //监听队列最大排队数设置为5
  41. if(ret == -1)
  42. {
  43. printf("Fail to listen socket!\n");
  44. return -1;
  45. }
  46. struct sockaddr_in client_address; //记录进行连接的客户端的地址
  47. socklen_t client_addrlength = sizeof(client_address);
  48. int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
  49. if(connfd < 0)
  50. {
  51. printf("Fail to accept!\n");
  52. close(listenfd);
  53. }
  54. char buff[1024]; //数据接收缓冲区
  55. fd_set read_fds; //读文件操作符
  56. fd_set exception_fds; //异常文件操作符
  57. FD_ZERO(&read_fds);
  58. FD_ZERO(&exception_fds);
  59. while(1)
  60. {
  61. memset(buff,0,sizeof(buff));
  62. /*每次调用select之前都要重新在read_fds和exception_fds中设置文件描述符connfd,因为事件发生以后,文件描述符集合将被内核修改*/
  63. FD_SET(connfd,&read_fds);
  64. FD_SET(connfd,&exception_fds);
  65. ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL);
  66. if(ret < 0)
  67. {
  68. printf("Fail to select!\n");
  69. return -1;
  70. }
  71. if(FD_ISSET(connfd, &read_fds))
  72. {
  73. ret = recv(connfd,buff,sizeof(buff)-1,0);
  74. if(ret <= 0)
  75. {
  76. break;
  77. }
  78. printf("get %d bytes of normal data: %s \n",ret,buff);
  79. }
  80. else if(FD_ISSET(connfd,&exception_fds)) //异常事件
  81. {
  82. ret = recv(connfd,buff,sizeof(buff)-1,MSG_OOB);
  83. if(ret <= 0)
  84. {
  85. break;
  86. }
  87. printf("get %d bytes of exception data: %s \n",ret,buff);
  88. }
  89. }
  90. close(connfd);
  91. close(listenfd);
  92. return 0;
  93. }

用select来解决socket中的多客户问题

上面提到过,,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。在网络编程中,当涉及到多客户访问服务器的情况,我们首先想到的办法就是fork出多个进程来处理每个客户连接。现在,我们同样可以使用select来处理多客户问题,而不用fork。

服务器端

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. #include <stdio.h>
  4. #include <netinet/in.h>
  5. #include <sys/time.h>
  6. #include <sys/ioctl.h>
  7. #include <unistd.h>
  8. #include <stdlib.h>
  9. int main()
  10. {
  11. int server_sockfd, client_sockfd;
  12. int server_len, client_len;
  13. struct sockaddr_in server_address;
  14. struct sockaddr_in client_address;
  15. int result;
  16. fd_set readfds, testfds;
  17. server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket
  18. server_address.sin_family = AF_INET;
  19. server_address.sin_addr.s_addr = htonl(INADDR_ANY);
  20. server_address.sin_port = htons(8888);
  21. server_len = sizeof(server_address);
  22. bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
  23. listen(server_sockfd, 5); //监听队列最多容纳5个
  24. FD_ZERO(&readfds);
  25. FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中
  26. while(1)
  27. {
  28. char ch;
  29. int fd;
  30. int nread;
  31. testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
  32. printf("server waiting\n");
  33. /*无限期阻塞,并测试文件描述符变动 */
  34. result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //FD_SETSIZE:系统默认的最大文件描述符
  35. if(result < 1)
  36. {
  37. perror("server5");
  38. exit(1);
  39. }
  40. /*扫描所有的文件描述符*/
  41. for(fd = 0; fd < FD_SETSIZE; fd++)
  42. {
  43. /*找到相关文件描述符*/
  44. if(FD_ISSET(fd,&testfds))
  45. {
  46. /*判断是否为服务器套接字,是则表示为客户请求连接。*/
  47. if(fd == server_sockfd)
  48. {
  49. client_len = sizeof(client_address);
  50. client_sockfd = accept(server_sockfd,
  51. (struct sockaddr *)&client_address, &client_len);
  52. FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中
  53. printf("adding client on fd %d\n", client_sockfd);
  54. }
  55. /*客户端socket中有数据请求时*/
  56. else
  57. {
  58. ioctl(fd, FIONREAD, &nread);//取得数据量交给nread
  59. /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
  60. if(nread == 0)
  61. {
  62. close(fd);
  63. FD_CLR(fd, &readfds); //去掉关闭的fd
  64. printf("removing client on fd %d\n", fd);
  65. }
  66. /*处理客户数据请求*/
  67. else
  68. {
  69. read(fd, &ch, 1);
  70. sleep(5);
  71. printf("serving client on fd %d\n", fd);
  72. ch++;
  73. write(fd, &ch, 1);
  74. }
  75. }
  76. }
  77. }
  78. }
  79. return 0;
  80. }

客户端

  1. //客户端
  2. #include <sys/types.h>
  3. #include <sys/socket.h>
  4. #include <stdio.h>
  5. #include <netinet/in.h>
  6. #include <arpa/inet.h>
  7. #include <unistd.h>
  8. #include <stdlib.h>
  9. #include <sys/time.h>
  10. int main()
  11. {
  12. int client_sockfd;
  13. int len;
  14. struct sockaddr_in address;//服务器端网络地址结构体
  15. int result;
  16. char ch = 'A';
  17. client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket
  18. address.sin_family = AF_INET;
  19. address.sin_addr.s_addr = inet_addr("127.0.0.1");
  20. address.sin_port = htons(8888);
  21. len = sizeof(address);
  22. result = connect(client_sockfd, (struct sockaddr *)&address, len);
  23. if(result == -1)
  24. {
  25. perror("oops: client2");
  26. exit(1);
  27. }
  28. //第一次读写
  29. write(client_sockfd, &ch, 1);
  30. read(client_sockfd, &ch, 1);
  31. printf("the first time: char from server = %c\n", ch);
  32. sleep(5);
  33. //第二次读写
  34. write(client_sockfd, &ch, 1);
  35. read(client_sockfd, &ch, 1);
  36. printf("the second time: char from server = %c\n", ch);
  37. close(client_sockfd);
  38. return 0;
  39. }

运行流程:

客户端:启动->连接服务器->发送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的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

Linux编程之select的更多相关文章

  1. Linux编程之ICMP洪水攻击

    我的上一篇文章<Linux编程之PING的实现>里使用ICMP协议实现了PING的程序,ICMP除了实现这么一个PING程序,还有哪些不为人知或者好玩的用途?这里我将介绍ICMP另一个很有 ...

  2. linux/unix网络编程之 select

    转自http://www.cnblogs.com/zhuwbox/p/4221934.html linux 下的 select 知识点 unp 的第六章已经描述的很清楚,我们这里简单的说下 selec ...

  3. Linux网络编程之select、poll、epoll的比较,以及epoll的水平触发(LT)和边缘触发(ET)

    Linux的网络通信先后推出了select.poll.epoll三种模式. select有以下三个问题: (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大. ...

  4. Linux 网络编程之 Select

    /*server*/ #include <stdio.h> #include <string.h> #include <unistd.h> #include < ...

  5. Linux编程之UDP SOCKET全攻略

    这篇文章将对linux下udp socket编程重要知识点进行总结,无论是开发人员应知应会的,还是说udp socket的一些偏僻知识点,本文都会讲到.尽可能做到,读了一篇文章之后,大家对udp so ...

  6. Linux编程之PING的实现

    PING(Packet InterNet Groper)中文名为因特网包探索器,是用来查看网络上另一个主机系统的网络连接是否正常的一个工具.ping命令的工作原理是:向网络上的另一个主机系统发送ICM ...

  7. socket编程之 select、poll、kqueue、epoll

    原生API select int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct tim ...

  8. Linux编程之epoll

    现在有这么一个场景:我是一个很忙的大老板,我有100个手机,手机来信息了,我的秘书就会告诉我"老板,你的手机来信息了."我很生气,我的秘书就是这样子,每次手机来信息就只告诉我来信息 ...

  9. 详述socket编程之select()和poll()函数

    转自:http://www.cppblog.com/myjfm/archive/2011/10/26/159093.aspx select()函数和poll()函数均是主要用来处理多路I/O复用的情况 ...

随机推荐

  1. 分针网—每日分享: 怎么轻松学习JavaScript

    js给初学者的印象总是那么的"杂而乱",相信很多初学者都在找轻松学习js的途径.   我试着总结自己学习多年js的经验,希望能给后来的学习者探索出一条"轻松学习js之路& ...

  2. SUSE 11下安装DPDK

    SUSE下安装与centos下安装有稍许不同: # cd  dpdk-1.8.0 # grep -rn Werror . |grep -iE "Makefile|mk" |awk ...

  3. MyBatis之级联小结

    在这之前我们知道了MyBatis为我们提供了三种级联:一对一关系(assocation).一对多关系(collection).鉴别器(discriminator).在最后一个鉴别器例子中,看到了当层级 ...

  4. 传感器系列之4.12GPS定位传感器

    4.12 GPS定位实验 一.实验目的 了解GPS的基本概念 了解NMEA-0183格式数据串的组成和关于GPS的常用语句 GPS的数据串解析 二.实验材料 具有串口通讯的电脑一台 ADS1.2开发环 ...

  5. PHP中array_merge函数与array+array的区别

    在PHP中可以使用array_merge函数和两个数组相加array+array的方式进行数组合并,但两者效果并不相同,下面为大家介绍两者具体的使用区别. 区别如下: 当下标为数值时,array_me ...

  6. javaWeb学习总结(8)- JSP原理

    一.什么是JSP? JSP全称是Java Server Pages,它和servle技术一样,都是SUN公司定义的一种用于开发动态web资源的技术. JSP这门技术的最大的特点在于,写jsp就像在写h ...

  7. Factoextra R Package: Easy Multivariate Data Analyses and Elegant Visualization

    factoextra is an R package making easy to extract and visualize the output of exploratory multivaria ...

  8. How to use data analysis for machine learning (example, part 1)

    In my last article, I stated that for practitioners (as opposed to theorists), the real prerequisite ...

  9. js常用的4种截取字符串方法

    平常经常把这几个api的参数记混了,于是打算记录下来,当不确定的时候在拿出来翻翻: 在做项目的时候,经常会需要截取字符串,所以常用的方法有slice().substr().substring().ma ...

  10. TCP:三次握手、四次握手、backlog及其他

    TCP是什么 首先看一下OSI七层模型: 然后数据从应用层发下来,会在每一层都加上头部信息进行封装,然后再发送到数据接收端,这个基本的流程中每个数据都会经过数据的封装和解封的过程,流程如下图所示: 在 ...