今天看到一个I/O性能的问题。就对这个问题思考了下。
分析阻塞、非阻塞I/O,这两种I/O一个共同点是,很多I/O中无法确认那些I/O是准备好,只能通过一个个轮询的方式,这种方式下,准备好与没有准备好的I/O均会被轮询,这种效率极其低下。异步I/O,提供了一种方式,准备好的I/O,就会发送一个信号给内核,其他时间继续进行其他的操作,这种方式一个不好的就是多个I/O同时准备好,发出的信号不好处理,不知道那个信号对应于那个描述符。
这样就思考,能不能有一种I/O呢。只考虑准备好的I/O,没准备好的I/O,不进行操作。
其实是有的,这种I/O就是多路复用I/O。在处理I/O之前,我们需要调用函数进行处理所监控的I/O,判断读写事件,等待其就绪后进行操作。这些函数有select、pselect、poll、epoll(Linux2.6内核后支持)。
首先我们来看下select函数,分别需要看传递的参数与返回的结果。
传递给select的函数,主要告诉内核,我们感兴趣的描述符,对于每个描述符,我们所关心的状态,监控这些I/O需要等待多久。
select函数返回的时候,我们可以知道已经准备好的描述符的数量;对于读、写、异常这三个状态的每一个,那些描述符已经具备了。
select函数的原型如下:
#include<sys/select.h> int select(int maxfdp1,fd_set *restirct readfds, fd_set *restrict writefds,fd_set *restrict exceptfds, struct timeval *resttrict tvptr );
这里需要说明下这个等待时间tvptr。上面参数中每个指针都添加了关键字restrict,限定只有声明的该指针才能访问这块内存,其他指针访问都是无效的。
当tvptr==NULL,select函数会永远等待,直到捕捉到监控的描述符中有准备好的或者一个出错信号,若出错则返回-1,否则返回准备就绪的描述符的数量;
当tvptr->tv_sec==0 && tvptr->tv_usec==0,select函数不等待,测试所有指定的描述符立刻返回,得到每个描述符的状态,非阻塞I/O轮询每个描述符;
当tvptr->tv_sec!=0 || tvptr->tv_usec!=0,select函数会等待指定的时间,超时返回0。如果在等待的时间内有准备好的句柄,则返回准备好的句柄的数量。在等待的时间内,select函数可以被信号打断。
第一个参数maxfdp1,这个参数的意思是最大的描述符加1,这样的话就需要在三个集合中找出最大描述符,然后加一传递给这个变量。其实这个值可以传递一个默认值,FD_SETSIZE,这是内核维护的一个常量,其值一般为1024,表示select函数最多可以处理的描述符的个数为1024个。若想要让内核支持多一些,可以修改此参数。
然后,我们继续看函数,里面有三个参数需要注意readfds、writefds、exceptfds,这三个分别表示处于可读、可写、异常的句柄的集合,这些句柄都存储在一个叫做fd_set数据类型里。现在看下关于这个集合的四种基本的操作:
#include<sys/select.h> int FD_ISSET(int fd,fd_set *fdset); void FD_SET(int fd,fd_set *fdset); void FD_CLR(int fd,fd_set *fdset); void FD_ZERO(fd_set *fdset);
首先看第一个函数,FD_ISSET该函数用于测试句柄fd是否在fdset内,如果再返回一个非零值,如果不在则返回0。FD_SET将句柄fd添加到集合fdset中,FD_CLR从集合fdset中清除句柄fd,FD_ZERO,初始化集合fdset,使得每个位置都为0.再以往select函数中添加参数为例,详细了解下操作的过程,可以看下面的代码:
fd_set readset,writeset; FD_ZERO(&readset); FD_ZERO(&writeset); FD_SET(0,&readset); FD_SET(3,&readset); FD_SET(2,&writeset); FD_SET(4,&writeset); FD_SET(1,&writeset); select(5,&readset,&writeset,NULL,NULL);
下面说下pselect,这是select的一个升级版本,提供更加精细的定时操作与信号屏蔽字,函数原型如下:
int pselect(int maxfdp1,fd_set *restirct readfds, fd_set *restrict writefds,fd_set *restrict exceptfds, const struct timespec *resttrict tsptr,const sigset_t *restrict sigmask);
这个函数与select不同的一点是,首先是定时器方面由timeval换成了timespec,这个更加精细,可以精确到微秒级别。还有一个信号屏蔽字sigmask。可以以原子的方式安装信号屏蔽字,函数返回时恢复以前的信号屏蔽字。关于怎么设置信号屏蔽字,可以看看这篇博客。
上面所有的就是对select函数的全部理解。
下面看下另外一个函数,叫做poll。
poll函数起源于system v,poll函数与STREAM系统紧紧相关。其函数原型如下:
#include<poll.h> int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);
poll与select不同的一点,select将描述符按照状态编入一个集合中,poll形成一个pollfd结构数组,数组内每个元素指定一个描述符编号以及对其所关心的状态。
struct pollfd{ int fd; short events; short revents; };
fdarray的元素个数由nfds决定。结构体中的events告诉内核对该描述符应当关心的是什么,revents是内核操作完后返回值,用以说明内核对该描述符进行了什么操作。
当一个描述符被挂断掉后,不能向描述符写数据,但是可以读取文件描述符的数据。
poll函数的最后一个timeout,当为-1时候,永远等待,直到捕捉到信号,poll返回-1。如果所指定的描述符准备好了,则返回准备好的文件描述符的个数。当为0的时候,不等待,测试所有描述符并返回,这里会以非阻塞轮询的方式处理所有描述符。当timeout大于0,就等待timeout毫秒,超时返回0,否则返回准备好的个数。
最后一种epoll方式,可以看看本博客前面的总结:epoll
版权声明:本文为博主原创文章,未经博主允许不得转载。