IO多路转接------select

        在五种IO模型一文中介绍了五种IO模型。在数据通信过程中,分为两部分:一个是等待数据到达内核,一个是将数据从内核拷贝到用户区。在实际应用中,等待的时间往往比拷贝的时间多,所以要提高IO的效率,就要减少等的比重。在阻塞IO,非阻塞IO,信号驱动IO和异步IO中,虽然等待的方式或等待的主体不同,但是无论是谁在等,无论如何等,等待的时间总长是不变的。

        在IO多路转接中,由于一次等待多个文件描述符,在单位时内就绪事发生的概率就越大,所以等的比重就会越小,此时IO的效率就会越高。同时因为IO多路转接单进程一次可以等待多个文件描述符,所以,比较适用于服务器处理多客户端的情形。因此IO多路转接一般用于实现服务器与多客户端之间的网络通信。

        IO多路转接的实现方式有很多,在不同的操作系统下实现的方式也会有所不同。如Linux下有:select,poll,epoll等,本文将重点介绍通过select来实现IO多路转接。

        在五种IO模型一文中提到,在服务器与客户端进行通信时,服务器要处理多客户端的情形。所以服务器程序首先要使用selcet等系统调用一次等待多个文件描述符,当至少有一个文件描述符满足就绪条件时,select返回,然后进程调用read对满足就绪条件的文件描述符进行读写。将满足就绪条件的所有文件描述符处理完之后,之前的客户端可能还会再次发送消息,所以,此时就需要不断调用select来循环式的等待满足就绪条件的文件描述符,然后对其进行处理。

select

        select的函数原型

 #include <sys/select.h>
 int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

        参数说明:

nfds:表示一次等待的文件描述符的个数加1;用于限定操作系统遍历的区间

fd_set:该结构实际是一个位图,因为文件描述符其实是数组下标,也就是从0开始的整数,所以对于位图的每一个比特位表示的是一个文件描述符的状态,如果是1表示关心该文件描述符上的事件,如果是0,表示不关心该文件描述符上的事件。而具体关心文件描述符上的什么事件,则由中间三个参数决定。

readfds:可以理解为读事件位图。如果该变量某一个比特位为1,则表示关心该比特位所表示的文件描述符的读事件。如:该变量的第2个比特位为1,表示关心文件描述符为1上的读事件。

writefds:写事件位图

exceptfds:异常事件位图

        注意:以上三个位图参数都是输入输出型参数,作为输入参数,表示的是关心的相应事件集合,作为输出参数,表示满足相应就绪条件的事件集合。

timeout:用于设置select阻塞等待的时间,取值如下:

        NULL:表示select阻塞等待,关心的多个文件描述符上没有时间发生时,进程会一直阻塞在select的函数调用处。如果至少有一个文件描述符上有事件发生,则select返回。

        0:表示select非阻塞等待,只是用于检测等待事件的状态。当调用该函数时,不管有无事件发生,该函数都会立即返回,进程不会挂起等待事件的发生。

        特定的时间值:表示select只会阻塞等待一定的时间,在该时间段内,如果有事件发生,则select返回,进程结束阻塞。如果达到规定的时间,还没有事件发生,此时select将会超时返回。

        timeval的结构如下:

struct timeval {
               long    tv_sec;         /* seconds :秒*/
               long    tv_usec;        /* microseconds:微秒 */
           };//头文件<sys/time.h>

        返回值说明:   

        如果select是阻塞等待,当等待的文件描述符中至少有一个就绪事件满足时,select就会返回。返回值表示的是满足就绪条件的事件的个数。如果所有文件描述符上的就绪条件都没有满足,此时进程会挂起等待;

        如果是非阻塞等待,当调用select时,如果等待的所有文件描述符上均没有事件发生,此时select直接返回0。如果有事件发生,则返回满足就绪条件的事件个数;

        如果是特定的时间值,当调用select时,在规定的时间内有事件发生,则返回满足就绪条件的事件个数;如果达到规定的时间还没有事件发生,则超时返回0;

        如果select函数调用出错,则返回-1;

        综上几种情形,select的返回值可以总结为:

如果函数调用成功,则返回满足就绪条件的事件个数(大于0)(包含以上三种情形);

如果在规定的时间内没有事件发生,则超时返回0(包含非阻塞和特定的时间值两种情形);

如果函数调用出错,则返回-1。错误原因由errno标识。此时中间三个参数的值变得不可预测。        


        在了解了上述的select函数之后,接下来要使用select来实现IO多路转接。

1. 首先要将关心的事件集合存放在上述的三个位图中:

        在之前的文章中有提及,在对位图进行操作时,不能直接对其进行赋值或打印等操作,而是通过特定的接口来实现:

void FD_ZERO(fd_set* set);//将set位图中的所有位设置为0
void FD_SET(int fd,fd_set* set);//将位图set中的相应fd位设置为1
void FD_CLR(int fd,fd_set* set);//将位图set中的相应fd为设置为0
int FD_ISSET(int fd,fd_set* set);//判断位图set中的相应位fd是否为1,为1返回1,不为1返回0

2. 知道如何设置参数之后,开始调用select函数,该函数的执行过程如下:

首先,清空位图集(这里只关心读事件集):FD_AERO(set);

再调用将关心的文件描述符添加到位图中:FD_SET(fd,set);

然后,调用select进行等待:select(nfds,set,NULL,NULL,NULL);(这里为阻塞等待)

最后,select返回后,遍历整个关心的文件描述符集,对输出型参数set进行判断哪些文件描述符上的事件发生了:FD_ISSET(fd,set);

3. 调用select时,要检查相应的事件有无发生,也就是相应的文件描述符上就绪条件是否满足。不同的事件对应不同的就绪条件:

        读就绪:

(1)当对内核中的数据进行读取时,如果接收缓冲区中的字节数,大于等于低水位SO_RCVLOWAT,此时就说明读就绪,在对该文件描述符调用read等进行读取时,不会阻塞,且返回值大于0;

(2)在TCP通信中,如果服务器的socket上有新的连接到达时,客户端会发送SYN数据包给服务器,说明读就绪。此时在调用accept接收新连接时,不会阻塞。而且会返回新的文件描述符与客户端进行通信;

(3)TCP通信中,如果对端关闭连接,此时会发送FIN数据包。也说明读就绪,此时在调用read对文件描述符进行读取时,会返回0;

(4)当socket上有未处理的错误时,也说明读就绪,在对文件描述符进行read读取时,会返回-1;

        写就绪:

(1)在socket内核中,如果发送缓冲区中的可用字节数大于等于低水位标记SO_SNDLOWAT时,说明写就绪,此时调用write进行写操作时,不会阻塞,且返回值大于0;

(2)当一方要进行写操作(即关心的是写事件),而对端将文件描述符关闭,此时写就绪。调用write时会触发SIGPIPE信号;

(3)当socket使用非阻塞connect连接成功或失败之后,写就绪;

(4)当socket上有未读取的错误时,写就绪,此时write进行写时,会返回-1;

        异常就绪:

当socket上收到带外数据时,异常事件就绪。

        下面利用select编写TCP服务器实现单进程处理多客户端的情形。

1. 首先绑定设置监听套接字;对应于listen_server函数

2. 在本程序中,只关心读事件,所以首先要设置一个读事件集readfds。

        因为select中的三个位图都是输入输出型参数,所以,先将关心的事件作为输入型参数传递给select之后,当select返回时,会将就绪的事件以输出型参数的形式返回,而没有就绪的事件就会被清空。待下一次要继续关心上一次没有达到就绪条件的文件描述符时,就会找不到了。同时,要判断输出型参数中哪些比特位被设置成了1,此时也需要提供一个判断的范围。比如说,如果关心的文件描述符是1,3,5,6。当select返回时,只需要判断1,3,5,7上有哪些文件描述符上事件就绪。

        所以,在select之前需要将关心的文件描述符集合保存起来。这样便可设置下一次需要关心的文件描述符集,同时select返回时也可由此判断哪些文件描述符上的事件就绪了。这里,可以提供一个数组来保存关心的文件描述符。将文件描述符作为数组元素保存在数组中。

        再设置一个数组。然后将数组元素均清空为-1.(因为文件描述符的值大于等于0)。因为监听套接字要一直接收来自客户端的连接,所以它只关心读事件,因此,首先将监听套接字在数组中保存起来。

3. 然后将关心的事件设置进读事件集合中。

        首先清空读事件集。然后根据数组内容将关心的文件描述符设置进读事件集合中。

4. 调用select开始等待事件就绪。

5. 当select返回时,如果返回值为0,说明超时返回,如果返回值为-1.说明select调用出错。如果返回值大于0,则进行如下处理(对应server_fd函数):根据数组中保存的关心的文件描述符集合,判断哪些文件描述符上有事件就绪。

(1)如果就绪的文件描述符是监听套接字,则进行接收连接处理(对应Accept_Server函数);

(2)如果就绪的文件描述符是普通套接字,则进行读取数据处理。(对应Read_Server函数)。

6. 在Accept_Server函数中,调用accept接收新连接。

        接收成功之后,将新创建的文件描述符保存在数组中,因为该文件描述符用于处理来自客户端的请求,所以首先要读取来自客户端的数据,所以,此时该文件描述符上关心的必然是读事件。但是,又不能立即对该文件描述符进行读取,因为客户端此时,不一定有数据传来,一旦没有数据,在对该文件描述符进行读取时,就会使整个进程挂起。

7. 在Read_Server函数中,调用read来对文件描述符进行数据的读取。此时,read一定不会阻塞。

        如果返回值大于0,表示数据读取成功。

        如果返回值等于0,说明对端将文件描述符关闭,即将连接断开。此时将该文件描述符关闭,并将其从数组中删除即可。

        如果返回值小于0,说明read调用失败。此时将该文件描述符关闭,并将其从数组中删除即可。

        整个服务器程序的编写思路上述已给出,完整的程序代码见:Select实现TCP服务器。客户端代码与之前的TCP客户端代码相同,完整的代码见:客户端程序

        运行结果如下:

服务器端:


客户端:


select的特点:

1. 在select中关心的文件描述符被添加在fd_set位图结构中,所以,fd_set的大小决定了能够关心的最大文件描述符数量。可以通过sizeof(fd_set)来进行查看(fd_set的大小可以调整);

2. 上述也有提到,在调用select之前需要设置一个数组来保存关心的文件描述符集合。作用有如下两方面:

(1)select返回之后,根据数组中的元素来判断关心的文件描述符上的哪些事件就绪了;

(2)select返回之后,会清空fd_set就够,所以在下一次调用select时还需要根据数组来再次设置fd_set来添加关心的文件描述符。

select的缺点:

1. 每次在调用select之前,都要遍历数组将关心的文件描述符添加进fd_set集中;

2. 在调用select时,需要将fd_set集合将用户态拷贝到内核态;然后操作系统还要遍历fd_set集合来等待关心的文件描述符集上的事件发生;

3. 在select调用结束后,还有遍历数组来判定哪些文件描述符上的事件就绪;

        上述的三点,在操作时都需要很大的开销。

4. 因为fd_set大小的限制,所以select所关心的文件描述符的个数有限。(fd_set的大小受限是因为它表示的是文件描述符的个数,在一个单进程中,一个进程对应一张文件描述符表,该表中的数组个数是确定的(受操作系统内核的限制),因此对应的文件描述符的个数也是确定的,所以fd_set的大小也是确定的,不过可以进行调整)。







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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值