Linux 网络通信 --- Select 使用

Linux Select 使用-sinbingzoo-ChinaUnix博客

在Linux中,我们可以使用select函数实现I/O端口的复用,同时监视多个文件描述符变化,同时具备超时返回特点。
    传递给 select函数的参数会告诉内核:
    * 我们所关心的文件描述符

    * 对每个描述符,我们所关心的状态。(我们是要想从一个文件描述符中读或者写,还是关注一个描述符中是否出现异常)

    * 我们要等待多长时间。(我们可以等待无限长的时间,等待固定的一段时间,或者根本就不等待)

   从 select函数返回后,内核告诉我们一下信息:

    * 对我们的要求已经做好准备的描述符的个数

    * 对于三种条件哪些描述符已经做好准备.(读,写,异常)

   有了这些返回信息,我们可以调用合适的I/O函数(通常是 read 或 write),并且这些函数不会再阻塞.

#include    

    int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

   返回:
    >0:就绪描述字的正数目
    0:超时
    -1:出错
首先我们先看一下最后一个参数。它指明我们要等待的时间:

struct timeval{      

        long tv_sec;   /*秒 */

        long tv_usec;  /*微秒 */   

    }

   有三种情况:

    timeout == NULL  等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或者是捕获到一个信号时函数会返回。如果捕获到一个信号, select函数将返回 -1,并将变量 erro设为 EINTR。

    timeout->tv_sec == 0 &&timeout->tv_usec == 0不等待,直接返回。加入描述符集的描述符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文件描述符状态。

    timeout->tv_sec !=0 ||timeout->tv_usec!= 0 等待指定的时间。当有描述符符合条件或者超过超时时间的话,函数返回。在超时时间即将用完但又没有描述符合条件的话,返回 0。对于第一种情况,等待也会被信号所中断。

    

中间的三个参数 readset, writset, exceptset,指向描述符集。这些参数指明了我们关心哪些描述符,和需要满足什么条件(可写,可读,异常)。一个文件描述集保存在 fd_set 类型中。fd_set类型变量每一位代表了一个描述符。我们也可以认为它只是一个由很多二进制位构成的数组。如下图所示:

   对于 fd_set类型的变量我们所能做的就是声明一个变量,为变量赋一个同种类型变量的值,或者使用以下几个宏来控制它

        void FD_ZERO (fd_set *         void FD_SET (int fd,fd_set *fdset); // turn on the bit for fd in fdset

        void FD_CLR (int fd,fd_set *fdset); // turn off the bit for fd in fdset

        int

FD_ZERO宏将一个 fd_set类型变量的所有位都设为 0,使用FD_SET将变量的某个位置位。清除某个位时可以使用 FD_CLR,我们可以使用 FD_ISSET来测试某个位是否被置位。

具体解释select的参数:

(1)intmaxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。

说明:对于这个原理的解释可以看上边fd_set的详细解释,fd_set是以位图的形式来存储这些文件描述符。maxfdp也就是定义了位图中有效的位的个数。注意select函数的第一个参数,是所有加入集合的句柄值的最大那个值还要加1。比如我们创建了3个句柄:

  1. int sa, sb, sc;  
  2. sa = socket(...); /* 分别创建3个句柄并连接到服务器上 */  
  3. connect(sa,...);  
  4. sb = socket(...);  
  5. connect(sb,...);  
  6. sc = socket(...);  
  7. connect(sc,...);  
  8. FD_SET(sa, &rdfds);/* 分别把3个句柄加入读监视集合里去 */  
  9. FD_SET(sb, &rdfds);  
  10. FD_SET(sc, &rdfds); 

(2)fd_set*readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

(3)fd_set*writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

(4)fd_set*errorfds同上面两个参数的意图,用来监视文件错误异常文件。

(5)structtimeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

说明:

函数返回:

(1)当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,内核(I/O)根据状态修改文件描述符集,并返回一个大于0的数。

(2)当没有满足条件的文件描述符,且设置的timeval监控时间超时时,select函数会返回一个为0的值。

(3)当select返回负值时,发生错误。

4 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模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。


理解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而不是fork来解决socket中的多客户问题,例程如下。服务器端
 

  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. int main()
  9. {
  10.     int server_sockfd, client_sockfd;
  11.     int server_len, client_len;
  12.     struct sockaddr_in server_address;
  13.     struct sockaddr_in client_address;
  14.     int result;
  15.     fd_set readfds, testfds;
  16.     server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket
  17.     server_address.sin_family = AF_INET;
  18.     server_address.sin_addr.s_addr = htonl(INADDR_ANY);
  19.     server_address.sin_port = htons(9734);
  20.     server_len = sizeof(server_address);
  21.     bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
  22.     listen(server_sockfd, 5);
  23.     FD_ZERO(&readfds);
  24.     FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中
  25.     while(1)
  26.     {
  27.         char ch;
  28.         int fd;
  29.         int nread;
  30.         testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
  31.         printf("server waiting/n");
  32.         /*无限期阻塞,并测试文件描述符变动 */
  33.         result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0);
  34.         if(result < 1)
  35.         {
  36.             perror("server5");
  37.             exit(1);
  38.         }
  39.         /*扫描所有的文件描述符*/
  40.         for(fd = 0; fd < FD_SETSIZE; fd++)
  41.         {
  42.             /*找到相关文件描述符*/
  43.             if(FD_ISSET(fd,&testfds))
  44.             {
  45.               /*判断是否为服务器套接字,是则表示为客户请求连接。*/
  46.                 if(fd == server_sockfd)
  47.                 {
  48.                     client_len = sizeof(client_address);
  49.                     client_sockfd = accept(server_sockfd,
  50.                     (struct sockaddr *)&client_address, &client_len);
  51.                     FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中
  52.                     printf("adding client on fd %d/n", client_sockfd);
  53.                 }
  54.                 /*客户端socket中有数据请求时*/
  55.                 else
  56.                 {
  57.                     ioctl(fd, FIONREAD, &nread);//取得数据量交给nread
  58.                     
  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. }

客户端

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. #include <stdio.h>
  4. #include <netinet/in.h>
  5. #include <arpa/inet.h>
  6. #include <unistd.h>
  7. int main()
  8. {
  9.     int client_sockfd;
  10.     int len;
  11.     struct sockaddr_in address;//服务器端网络地址结构体
  12.      int result;
  13.     char ch = 'A';
  14.     client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket
  15.     address.sin_family = AF_INET;
  16.     address.sin_addr.s_addr = inet_addr(“127.0.0.1”);
  17.     address.sin_port = 9734;
  18.     len = sizeof(address);
  19.     result = connect(client_sockfd, (struct sockaddr *)&address, len);
  20.     if(result == -1)
  21.     {
  22.          perror("oops: client2");
  23.          exit(1);
  24.     }
  25.     write(client_sockfd, &ch, 1);
  26.     read(client_sockfd, &ch, 1);
  27.     printf("char from server = %c/n", ch);
  28.     close(client_sockfd);
  29.     zexit(0);
  30. }

Linux下select使用陷阱

        select基本可以满足大部分的应用需求,如果连接数很大(几万或者几十万的连接数) select 将不再适合了;

Select函数使用简单,其工作原理大家通常也知道,但是在实际的使用过程中可能并没有严格遵守,而且确实也比较难以完全遵守,除非不使用它。

Select采用一个bit表,每个fd对应表中的一个bit位,宏FD_SETSIZE为表的大小,添加到fd_set中的fd值必须小于FD_SETSIZE,否则就会越界,假设有如下一段代码:

fd_set  readfds;

FD_ZERO(&readfds);

FD_SET(fd,  &readfds);

那么,这里的fd必须满足:fd < FD_SETSIZE,否则即会发生越界,使用valgrind和purify等内存检测工具能够检测到这个问题,但通常很少人去注意,会认为是一个可以忽略的warning,其后果是导致某个不能理解的crash问题。

通过ulimit命令和setrlimit函数来修改进程内句柄数的限制,并不会影响FD_SETSIZE的值,所以即使通过ulimit命令或setrlimit函数将进程允许的句柄改成很大了,但如果FD_SETSIZE值没有修改,则仍可能发生crash。

在什么情况下最容易遇到这个问题?

较容易发生在服务端程序中,因为服务端程序同一时刻的连接数很容易超过默认的FD_SETSIZE值,而服务端的代码可能是使用epoll使用的,所以它本身并不会存在问题,但是程序中可能还有个客户端,比如使用了select来实现超时连接,这个时候问题就来了,当连接数超过FD_SETSIZE时,超时连接处的select调用就发生了越界,进程就会在某个可能完全不相干的地方crash,要定位这个问题的成本是很高的,不具备一定经验,很难在短时间内定位出来。

如何去避免这个问题了?那就是尽量不使用select,而应当使用更安全的poll函数来替代,因为poll使用的数组是调用者自己维护的,完全可以保证不越界。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值