转载:
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时间。