Chinaunix首页 | 论坛 | 博客
  • 博客访问: 348001
  • 博文数量: 100
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 521
  • 用 户 组: 普通用户
  • 注册时间: 2014-10-31 11:37
个人简介

活到老,学到老

文章分类

全部博文(100)

文章存档

2018年(1)

2017年(2)

2016年(11)

2015年(82)

2014年(4)

我的朋友

分类: LINUX

2015-07-14 10:04:14

原文地址:Linux Select 使用 作者:sinbingzoo

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_CLR (int fd,fd_set *fdset); // turn off the bit for fd in fdset

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


具体解释select的参数:

1intmaxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加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); 

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

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

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

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

说明:

函数返回:

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最大可以对应8fd

1)执行fd_set set;FD_ZERO(&set);set用位表示是0000,0000

2)若fd5,执行FD_SET(fd,&set);set变为0001,0000(5位置为1)

3)若再加入fd2fd=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,取maxfdselect返回后利用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. }



阅读(810) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~