IO复用之select

seletc,poll和epoll都是io复用复用的接口。 I/O复用解决什么问题?I/O复用就是通过一种方法可以同时监听多个描述符的状态,一旦有描述符就绪就通过相应的接口返回通知用户程序进行相应的读写操作。同时你或许注意到它们都是同步的I/O监听。原因在于它们在外部读写事件就绪后自己负责控制读写,进一步说便是在此过程是堵塞的,有数据才读,没数据就等待。 然而异步的Io就不需要自己就行读写的控制,因为异步类io的实现会字节吧数据从内核拷贝到用户空间。

我们使用IO复用函数最大的好处在于它实现了对于多个IO端口的监听。

select原理:
在定义的最大描述符内将监听事件分为读事件,写事件和异常事件。当由外部客户端接入时(connect请求连接)时。服务器端会将该连接描述符c添加到预先定义的fd_set集合中,等到这个连接上准备好读或者写的时候,就通知程序进行io操作。进而与客户端进行数据通信。
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,  fd_set *exceptfds, struct timeval *timeout);
参数:
nfds(net work dile_descriptor): is the highest-numbered file descriptor in any of the three sets, plus 1.三个描述符集合中值最大的描述符加1
fd_set *readfds, fd_set *writefds, fd_set *exceptfds:读集,写集,异常事件集(套接字有带外数据的到达,控制状态信息的存在)。一般被定义成int类型数组。如果我们对于其中三个集合中的任意一个不感兴趣,那么就将它置为NULL。


struct timeval *timeout:is an upper bound on the amount of time elapsedbefore select() returns. If both fields of the timevalstucture are zero, then select() returns immediately. (This is useful for polling.) If timeout is NULL (no timeout),select() can block indefinitely.

用于设置select告诉内核等待上述三个事件集准备好所需的时间,如果在时间段内有事件发生,那么将会处理事件,待事件处理后继续重复该过程
#include<sys/time.h>
           struct timeval {
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
           };
如果上tomeout的连个成员都设置都被设置成NULL那么,在检查描述符状态之后,select将会立即返回(对于poll同样适用,此种情况下称为轮询,并且不等待);如果将其设置为NULL,那么就是没有超时时间,即永远等待下去,直到有描述符就绪为止。

返回值:-1select失败
0 超时 timeout
>0就绪描述符字的正数目
select函数对应的位操作宏
  void FD_CLR(int fd, fd_set *set);  //关闭set中的fd位
  int  FD_ISSET(int fd, fd_set *set); //检查fd是否在set集合中
  void FD_SET(int fd, fd_set *set); //将set中的fd位置1
  void FD_ZERO(fd_set *set);//将set置全0

对于集合的初始化时很有必要的,如何使用了一个未经过初始化的集合,将会带来不可预测的后果。
一般过程是我们先定义一个fd_set集合,然后将其置空
fds_init(fds);
fds_add(fds,sockfd);
并定义一个数组保存可用描述符,并初始化为-1表示最开始数组中所有描述符都是无效的。
fds_init(fds);
fds_add(fds,sockfd);
fd_set fdset;
接着我们计入select的状态查询圈(循环)。先找到当前描述符数组中存贮最大的描述符并且保存下来加一,最为select的第一个参数;
 79         fd_set fdset;
 80         while(1)
 81         {
 82             FD_ZERO(&fdset);
 83 
 84             int maxfd=-1;
 85             int i=0;
 86 
 87             for(;i<MAX;i++)
 88             {
 89                 if(fds[i]!=-1)   // ???
 90                 {
 91                     FD_SET(fds[i],&fdset);
 92                     if(maxfd<fds[i])
 93                     {
 94                         maxfd=fds[i];
 95                     }
 96                 }

当然还没有结束。接着我们要设置超时时间,接下来就是调用select函数,查看当前有哪些描述符是就绪的,接着select返回就绪描述符个数或者告诉我们在超时时间内没有就绪事件发生,或者出错返回-1.

 99             struct timeval tv={5,0};
100             int n=select(maxfd+1,&fdset,NULL,NULL,&tv);
101             if(n==-1)
102             {
103                 perror("select error");
104                 break;
105             }
106             else if(n==0)
107             {
108                 printf("timeout\n");
109                 continue;
110             }
如果有就绪事件,并且在fd_set集合上的,那么我们就必须查询当前数组描述符是否是可用的,不可用就继续找下一个可用的,找到之后先判断是不是监听套接字listenedgfd,如果就建立连接,
114                 for(;i<MAX;i++)
115                 {
116                     if(fds[i]==-1)
117                     {
118                         continue;
119                     }
120                     if(FD_ISSET(fds[i],&fdset))
121                     {
122                         if(fds[i]==sockfd)
123                         {
124                             struct sockaddr_in caddr;
125                             int len=sizeof(caddr);
126                             int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
127                             if(c<0)
128                             {
129                                 continue;
130                             }
131 
132                             printf("accept :%d\n",c);
133                             fds_add(fds,c);
134                         }
并将获得的连接描述符添加到fd_set集合中,下来跟前面一样,只不过如果检测到的是连接套接字,我们就接收数据,并发送反馈信息。如果客户端断开了(recv返回0)那么就关闭对应的套接字,并将描述符数组中的对应元素置为-1不可用描述符,并给出
客户端断开连接的提示
 else
136                         {
137                             char  buffer[128]={0};
138                             if((recv(fds[i],buffer,127,0))<=0)
139                             {
140                                 close(fds[i]);   //close file descriptor
141                                 fds_del(fds,fds[i]);
142                                 printf("CLient over\n");
143                             }
144                             printf("buffer %d  =%s\n",fds[i],buffer);
145                             send(fds[i],"ok",2,0);
146                         
另外在man手册还提到了select和poll和描述符非阻塞的联合使用
 Under Linux, select() may report a socket file descriptor as "ready for reading", while nevertheless  a  subsequent  read blocks.  This could for example happen when data has arrived but upon examination has wrong checksum  and  is  discarded.There  may be other circumstances in which a file descriptor is spuriously reported as ready.  Thus it may  be  safer  to use O_NONBLOCK on sockets that should not block.

   On  Linux,  select()  also  modifies  timeout if the call is interrupted by a  signal  handler  (i.e.,  the  EINTR  error return).   This is not permitted by POSIX.1-2001.
上述来自man手册的描述,在linux环境下,select通常要和O_NONBLOCK一起使用,原因在于:多路IO复用是非阻塞IO把轮询过程给了内核,在select的源码有体现的。那么在select,poll这一类调用返回的时候一定是由sockfd就绪的。此时进行read当然没问题,但是如果如上面的描述,如果现在有数据已经到达了就绪了,select返回。但是这个数据在传输过程出了问题,丢弃数据,那么在之后读取时就阻塞了,因为没有读到指定的完整数据。那么进程此时被阻塞而无法执行select,在此中场景下哪怕有sockfd就绪,那么也将无法读取。因为程序被阻塞起来了!!!想读取的数据又读取不了只能阻塞着。

最后我们总补充一下描述符就绪的条件:

在网络编程中,出现下情况我们认为socket是可读的。

(1)socket内核接收缓冲区的字节数大于或者等于第低水平标记的SO_RCVLOWAT.此时我可以无阻塞的读取socket,并且读操作返回的字节数大于零(数据缓存区有数据)

(2)scoket上有未处理了的错误。此时可用getsockpt来读取和清除该错误

(3)socket通信的对方关闭连接。此时对于该套接字的读操作返回0

(4)监听套接字上有新的连接

发生下面情况,我们认为socket是可写的

(1)套接字(socket)内核发送缓冲区中的可用字节数大于或者等于其低水位标记SO_SNDLOWAT,此时我们可以无阻塞的写socket,并且写操作返回的字节数大于零(数据缓存区有数据)

(2)套接字的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。

(3)套接字使用非阻塞connect连接成功或者失败(超时)之后。

  (4)套接字上有未处理了的错误。此时可用getsockpt来读取和清除该错误









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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值