概述
I/O多路复用之select()系统调用
为什么要使用I/O多路复用
应用程序常常需要在多于一个文件描述符上阻塞:例如响应键盘输入(stdin)、进程间通信以及同时操作多个文件。
在不使用线程(怎么理解线程的存在呢?我么可以举一个例子。当我们运行qq这个进程的时候,是可以执行不同的任务的。例如,我们可以在使用qq发送消息的同时来收发文件。而这两个不同的任务就是利用线程来完成的),尤其是独立处理每一个文件的情况下,进程无法在多个文件描述符上同时阻塞。如果文件都处于准备好被读写的状态,同时操作多个文件描述符是没有问题的。但是,一旦在该过程中出现一个未准备好的文件描述符(就是说,如果一个read()被调用,但没有读入数据),则这个进程将会阻塞,不能再操作其他文件。可能阻塞只有几秒钟,但是应用无响应也会造成不好的用户体验。然而,如果文件描述符始终没有任何可用数据,就可能一直阻塞下去。
如果使用非阻塞I/O,应用可以发起I/O请求并返回一个特别的错误,从而避免阻塞。但是,从两个方面来讲,这种方法效率较差。首先,进程需要以某种不确定的方式不断发起I/O操作,直到某个打开的文件描述符准备好进行I/O。其次,如果程序可以睡眠的话将更加有效,可以让处理器进行其他工作,直到一个或更多文件描述符可以进行I/O时再唤醒。
三种I/O多路复用方案
I/O多路复用允许应用在多个文件描述符上同时阻塞,并在其中某个可以读写时收到通知。这时I/O多路复用就成了应用的关键所在。
I/O多路复用的设计遵循一下原则:
1、I/O多路复用:当任何文件描述符准备好I/O时告诉我
2、在一个或更多文件描述符就绪前始终处于睡眠状态
3、唤醒:哪个准备好了?
4、在不阻塞的情况下处理所有I/O就绪的文件描述符
5、返回第一步,重新开始
Linux提供了三种I/O多路复用方案:select、poll、epoll。
select函数使用
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
/*
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; // seconds
long tv_usec; // microseconds
};
int类型返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。
*/
void FD_CLR(int fd, fd_set *set); // 把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); // 测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); // 把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
其中fd_set是select机制中提供的一种数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不仅是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一个socket或文件发生了可读或可写事件
监测的文件描述符可以分为三类,分别等待不同的事件。监测readfds集合中的文件描述符,确认其中是否有可读数据(也就是说,确认好了的文件描述符的读操作可以无阻塞的完成)。监测writefds集合中的文件描述符,确认其中是否有一个写操作可以不阻塞地完成。监测exceptfds中的文件描述符,确认其中是否有出现异常发生或者出现带外数据(这种情况只适用于套接字)。指定的集合可能为空(NULL)。相应的,select()则不对此类事件进行监测。
成功返回时,每个集合只包含对应类型的I/O就绪的文件描述符。举个例子,readfds集合中有两个文件描述符:7和9.当调用返回时,如果7还在集合中,该文件描述符就准备好进行无阻塞I/O了。如果9已不在集合中,它可能在被读取时会发生阻塞。出现错误返回-1。
第一个参数n,等于所有集合中文件描述符的最大值加1。这样,select()的调用者需要找到最大的文件描述符值,并将其加1后传给第一个参数。
timeout
参数是一个指向timeval
结构体的指针,定义如下:
#include <sys/time.h>
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
如果这个参数不是NULL,即使此时没有文件描述符处于I/O就绪状态,select()调用也将在tv_sec秒、tv_usec微秒后返回。即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回。
如果时限中的两个值都是0,调用会立即返回,并报告调用时所有事件对应的文件描述符均不可用,且不等待任何后续事件。
若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止
fd_set结构体
fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。
#include <WINSOCK2.H>
int main()
{
fd_set fdset;
FD_ZERO(&fdset);
FD_SET(1, &fdset);
FD_SET(2, &fdset);
FD_SET(3, &fdset);
FD_SET(7, &fdset);
int isset = FD_ISSET(3, &fdset);
printf("isset = %dn", isset);
FD_CLR(3, &fdset);
isset = FD_ISSET(3, &fdset);
printf("isset = %dn", isset);
return 0;
}
然后经过FD_CLR以后,fd_array[2]就被清除了,数组后面的数据依次往前提,即7被放到了fd_array[2]
所以isset前后两次打印的值分别为1和0
select模型
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set); 则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环加fd,取maxfd,select返回后利用FD_ISSET判断是否有事件发生。
最后
以上就是微笑御姐为你收集整理的linux select 函数使用fd_set结构体的全部内容,希望文章能够帮你解决linux select 函数使用fd_set结构体所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复