Chinaunix首页 | 论坛 | 博客
  • 博客访问: 701687
  • 博文数量: 102
  • 博客积分: 10
  • 博客等级: 民兵
  • 技术积分: 1748
  • 用 户 组: 普通用户
  • 注册时间: 2012-02-23 15:42
个人简介

寻找严肃、沉默和专注的力量。

文章分类

全部博文(102)

文章存档

2015年(26)

2014年(8)

2013年(68)

分类: C/C++

2013-05-14 18:19:57

本文转载自:http://blog.csdn.net/linyt/article/details/1722445

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函数,通常是readwrite,并确知该函数不会阻塞。


函数的定义:

#include

#include

#include


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返回-1errno设置为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的意思是“最大的fd1max fd plus 1)”。在三个描述符集中找出最大的描述符值,然后加1,这就是第一个参数。也可以将第一个参数设置为FD_SETSIZE,这是这义的一个常数,通常是2561024。但对于大部分应用程序来说,此值太大了。如果将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描述符,并加到客户端描述符数组clientfdreadsocket中。

}

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


最后,写本文的初衷是见到网上介绍select的资料不多,而且不够详细,故有感而写。上面的代码只能用来说明问题,也许表达得不够清楚。上面对select函数的描述来源于<<UNIX环境高级编程>>(中文版)一书。需要的话可以参考此书,此书不失为一本经典的UNIX书籍。


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