用select实现I/O多路转接

来源:互联网 发布:java数组的长度 编辑:程序博客网 时间:2024/06/06 23:50

写网络程序服务器代码时,会遇到同时与多个client进行通信的问题,假如这些与client连接的socket都存储在SockArray[max_num]中,为了接受每个client的数据,可能会这么做:

这样做会遇到一些问题:

1.recv函数默认是阻塞式的,阻塞是什么意思呢,就是这个函数非要执行完才会返回,例如recv非要等到client有数据才返回,若此时另外的client有数据发给server就悲剧了。

   对此的解决办法有,采用非阻塞式的,recv不必要等到有数据,并接收成功才返回,只是测试一下有没有数据来了,没有直接返回,利用函数的返回值来了解函数真正的运行情况。

 

  由于linux下一切皆文件。所以不管open还是socket的返回值都是文件描述符,都是阻塞的,可以调用fcntl函数使其变成非阻塞的。

 

  fcntl函数改变文件描述符的属性,函数的几种形式:

int fcntl(int fd, int cmd);
int fcntl(int
fd, int cmd, long arg
);
int fcntl(int
fd, int cmd, struct flock *lock);


  cmd的几种形式:

F_GETFD
Read the file descriptor flags.
F_SETFD
Set the file descriptor flags to the value specified by arg.
F_GETFL
Read the file status flags.
F_SETFL
Set the file status flags to the value specified by arg.

 

要设置文件为非阻塞,要先用cmd = F_GETFD 获得其文件属性,然后设置非阻塞属性

可以写一个设置或移除文件的某个属性的函数:

 

如果要某个socket返回的文件描述符设置为非阻塞,只需要:

SetOrRemoveFlag(fd,true,O_NONBLOCK);

 

采用非阻塞IO机制来实现的代码,与原来的相比,结构和原理上没有改变,只是把所有的IO都设置为非阻塞的。根据I/O函数返回值>0来确定有数据进行I/O。

 

2.这时候的代码采用非阻塞式依然有一个问题,那就是for循环不停的在跑,来检查是否有socket可以进行I/O,浪费了大量的CPU时间。

 

3.如何解决呢?其实可以采用阻塞加轮询的方式,某个函数去检查是否有socket可以I/O,如果没有它就阻塞,过一定时间返回,如果有socket可以I/O,就进行I/O。

 

这就是select函数的功能:

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

 

各个参数含义:

fd_set,是一个文件描述符的集合。

如此,readfds则是一个读文件描述符集合,也就这个集合中的文件想检查其是否可以进行读操作。

同理,writefds中的文件,都想关注其是否可以进行写操作。

exceptfds,关心一个描述符的异常情况。

 

struct timeval的定义:

struct timeval {
    long    tv_sec;         /* seconds */
    long    tv_usec;        /* microseconds */
};
 *如果timeout = NULL,则这个函数变成阻塞式的,即永远等待

 *如果timeout = 0,则这个函数变成非阻塞式的,即完全不等

 *如果非以上两种情况,select函数在检查各个集合中的描述符若都没有准备好,则等待timeout的时间后,返回。

 

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

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

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

然后就可以进行相应的I/O操作。

 

select函数返回值:

*-1,函数出错

*0,等待超时

*n,已准备好的描述符数量

 

seletct函数第一个参数含义:

所有描述符集合中描述符最大值+1,内核要遍历检索这些描述符是否准备好相应操作。

 

关于结构体fd_set:

每个描述符对应于数据结构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(&rset);     //必须使用FD_ZERO清除其所有位

FD_SET(fd, &rset);  //然后设置我们所关心的位

if( FD_ISSET(fd, &rset))    //从select返回时,用FD_ISSET测试该集中的一个给定位是否仍旧设置

{

...

}

 

select函数的这三个参中的任一个(或全部)可以是空指针,这表示对相应的条件不关心。

值得一提的是:如果这三个指针全部为空,则select函数提供了比sleep更精确的计时器(sleep等待整数秒,而select函数可以等待少于1秒的时间,具体时间粒度取决于系统时钟),还有一个函数pselect更NB,可以提供ns的精度。

 

注意:

每次select函数超时返回后都要重新设置timeout的值,因为内核会把这个时间减少来计算何时函数返回。不然,一次超时返回后,timeout = 0。

 

通过select函数实现I/O多路转接:

 

采用select函数查询描述符集合中是否有描述符准备好相应操作,如果有则返回,进行操作;都没准备好则阻塞等待timeout时间,然后返回,避免了一直循环浪费CPU时间。

原创粉丝点击