select函数与I/O多路转接

Posted ricks

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了select函数与I/O多路转接相关的知识,希望对你有一定的参考价值。

select函数与I/O多路转接

 

相作大家都写过读写IO操作的代码,例如从socket中读取数据可以使用如下的代码:

while( (n = read(socketfd, buf, BUFSIZE) ) >0)

if( write(STDOUT_FILENO, buf, n) = n)

{

printf(“write error”);

exit(1);

}


当代码中的socketfd描述符所对应的文件表项是处于阻塞时,它会一直阻塞,直到有数据从网络的另一端发送过来。如果它是一个服务器程序,它要读写大量的socket,那么在某一个socket上的阻塞很明显会影响与其它socket的交互过程。类似的问题不单单出现在网络上,还可以出现在读写加锁的文件和FIFO等等一系列的情况。

一种比较好的解决方法似乎是采用非阻塞IO来实现。把所要读取数据的socketfd设置为非阻塞状态,依次用read函数检查是否有数据到来,如有,它会返回接到数据的个数,否则它会返回-1以表示当前还没有数据到达。这样,对于每个socket,如有数据到来则读取,没有也会马上返回。这就是非阻塞IO的好处拉。部分代码如下:

//clientfd[] 为客户端的socket描述符组数,假设数组的大小为MAX,并且所有客户端socket描述符都设置为非阻塞状态时。

for(i = 0; i < MAX; ++i)

{

int n;

if( (n = read(clientfd[i], buf, SZIE)) >0)

{

//send response to client in here.

}

}


这里代码看起来与上面的代码没有太大的区别,其实是有很大的区别;区别就是使使用了非阻塞IO进行整个交互过程,使得各个客户端都得到相对平等的时间待遇。这种模式我们通常称为这“轮询”模式。轮询模式同样有它的不足之处,在执行read函数时,实际上大部分时间还是没有数据可读的,但仍不断地执行read,浪费了很多CPU时间。

 

实际,对于上述的问题,一种比较好的技术就是I/O多路转接(I/O multiplexing)它可谓是上面两种方法的接衷:先构造一张有关描述符的数据表,然后调用一个函数,仅当有一个或多个描述符已准备可以进行IO操作时才返回,否则一直阻塞。在返回时,它会告诉进程那些描述符已准备好可以进行IO。

现在实现多路转接的任务落在select函数的身上了,现在给大家详细介绍select函数的使用。我们的主角出场了,呵呵!掌声!

函数的功能:实现多路转接,通过调用内核来实现。它向内核提供如下的参数

1)我们所关心的描述符

2)对于每个描述符,我们所关心的条件(是否读一个给定的描述符,还是想写一个给定的描述符,还是关心一个描述符的异常条件)

3)希望等待多久时间(可以永远等待,等待一个固定时间,或完全不等待)

从select返回时,内核告诉我们:

1)已准备好的描述符数量

2)哪一个描述符已准备好读、写或异常条件

使用这种返回值,就可调用相应的I/O函数,通常是read或write,并确知该函数不会阻塞。

 

函数的定义:

#include <sys/types.h>

#include <sys/time.h>

#include <unistd.h>

 

int select( int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, sturct timeval *tvptr);

返回:准备就绪的描述符,若超时则为0,若出错则为-1

 

最后一个参数为struct timeval的指针变量,它指定愿意等待的时间。

struct timeval{

long tv_sec; /*秒数*/

long tv_usec; /*微秒数*/

};

 

对于参数tvptr有三种情况:

如果tvptr == NULL 则永远等待。如果捕捉到一个信号则中断此无限期等待。当指定的描述符中的一个或多个已准备好或捕捉到一个信号则返回。如果是捕捉到一个信息,则select返回-1,errno设置为EINTR.

如果tvptr->sec ==0 && tvptr->tv_usec == 0 则完全不等待。即测试所有的描述符后马上返回。这是得到多个描述符的状态而不阻塞select函数的轮询方法。

如果tvptr->tv_sec != 00 || tvptr->tv_usec != 0 则等待指定的秒数和微秒数。当指它的描述符之一已准备好,或指定的时间值已超时则返回。如果在超时时还没有一个描述符准备好,则返回值是0。与第一种情况类似,这种等待可能被信号所中断。

 

中间三个参数readfds, writefds, exceptfds是指向描述符集的指针,它们描述了我们关心的可读、可写和处异常条件的各个描述符。这种描述符集存在一种叫fd_set的数据类型中(在头文件select.h中有定义)。具体做法每个描述符对应于数据结构fd_set所占用内存空间的一个位,如果第i位为0则表示值为i的描述符不包含在该集中,反之亦然。为了方便用户使用,系统提供了如下的四个宏进行操作。

FD_ZERO(fd_set *fdset); //清空fdset中的所有位

FD_SET(int fd, fd_set *fdset); //在fdset中打开fd所对应的位

FD_CLR(int fd, fd_set *fdset); //在fdset中关闭fd所对应的位

FD_ISSET(int fd, fd_set *fdset); //测试fd是否在fdset中

 

通常做法是,先定义一个描述符集

fd_set rset;

int fd;

必须使用FD_ZERO清除其所有位

FD_ZERO(&rset);

然后设置我们所关心的位

FD_SET(fd, &rset);

FD_SET(STDOUT_FILENO,&rset);

从select返回时,用FD_ISSET测试该集中的一个给定位是否仍旧设置

if( FD_ISSET(fd, &rset)){

...

}

 

select函数的这三个参中的任一个(或全部)可以是空指针,这表示对相应的条件不关心。值得一提的是:如果这三个指针全部为空,则select函数提供了比sleep更精确的计时器(sleep等待整数秒,而select函数可以等待少于1秒的时间,具体时间粒度取决于系统时钟)。

select第一个参数 maxfdp1的意思是“最大的fd加1(max fd plus 1)”在三个描述符集中找出最大的描述符值,然后加1,这就是第一个参数也可以将第一个参数设置为FD_SETSIZE,这是<sys/types.h>这义的一个常数,通常是256或1024。但对于大部分应用程序来说,此值太大了。如果将maxfdp1设置为最大的描述符值加1,内核只需要在此范围内寻找打开位,而不必在上数百个的大范围内搜索。

如下是示例代码:

fd_set readset, writeset;


FD_ZERO(&readset);

FD_ZERO(&writeset);

 

FD_SET(0, &readset);

FD_SET(3, &readset);

FD_SET(1, &writeset);

FD_SET(2, &writeset);

 

select(4, &readset, &writeset, NULL, NULL); //注意第一个参数为4

 

select有三个可能的返值:

1)返回值-1表示出错。例在未有描述符准备好数据时捕捉到一个信号时

2)返回值0表示没有描述符准备好。若指定的描述符都没有准备好,并且指定的时间已到,则发生这种情况。

3)返回一个正数,说明已经准备好的描述符数,在这种情况下。三个描述符集中仍旧打开的位是已准备好的描述符位。


对于“准备好”的意思,要作一些列具体的说明:

1)对于读集中的一个描述符的read不会阻塞,则此描述符是准备好的。

2)对于写集中的一个描述符的write不会阻塞,则此描述符是准备好的。

3)对于异常条件集中的一个描述符有一个未决异常条件,则此描述符是准备好的。

 

如果在一个描述符中碰到文件结束符,则select认为描述符是可读的,然后调用read,它返回0,这是unix指示到达文件尾处的方法。

 

通过select函数实现I/O多路转接,上面第二个例子的代码可改写成如下:

//clientfd[] 为客户端的socket描述符组数,假设数组的大小为MAX。

//serverfd表示服务器描述符,非阻塞。

//readsocket表示客户端socket描述集,同样包括服务的socket描述符

//maxfdp1 表示readsocket中最大 socket值加1

while(1)

{

int n = select(maxfdp1, &readsocekt, NULL, NULL, NULL)

if(n >0)

{

//is that some connectiion request

if(FD_ISSET(serverfd, &readsocket))

{

//用accept函数来获取连接的客户socket描述符,并加到客户端描述符数组clientfd和readsocket中。

}

for(int i = 0; i < MAX; ++i)

{

if(FD_ISSET(clientfd[i], &readsocket))

{

//response to client here.

}

}

}

}

 

在本例代码每次循环时,都采用select函数查询是否有描述符准备好,有则处理。无则阻塞,直到有数据准备好为止。在这段时间里面,可以让CPU做其它事情,避免了轮询方法所占用的大量CPU时间。

 

最后关于I/O多路转接问题的情况。I/O多路转接至今还不是POSIX的组成部分。SVR4和4.3+BSD都提供select函数以执行I/O多路转接。SVR4实际上用poll实现select。同时,在SVR4和BSD的select实现之间,有些差异,BSD系统总是返回一个所有准备好的描述符数之和,如果某个描述符同时在两个集中(如读集和写集),则返回值把它的描述符中累加两次。不同的是,SVR4更正了这一点,只计一次。于此,唯有POSIX标准化了select这样的函数才能解决此问题。

 

最后,写本文的初衷是见到网上介绍select的资料不多,而且不够详细,故有感而写。上面的代码只能用来说明问题,也许表达得不够清楚。上面对select函数的描述来源于<<UNIX环境高级编程>>(中文版)一书。需要的话可以参考此书,此书不失为一本经典的UNIX书籍。
---------------------
作者:海枫
来源:CSDN
原文:https://blog.csdn.net/linyt/article/details/1722445
版权声明:本文为博主原创文章,转载请附上博文链接!










以上是关于select函数与I/O多路转接的主要内容,如果未能解决你的问题,请参考以下文章

IO多路转接 ——— selectpollepoll

I/O多路转接之select和非阻塞IO

I/O多路转接之poll

I/O多路转接模型 [select] [poll] [epoll]

I/O多路转接模型 [select] [poll] [epoll]

I/O多路转接之select——基于TCP协议