UNIX环境高级编程——I/O多路转接(select、pselect和poll)

时间:2023-12-22 20:24:44

I/O多路转接:先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行I/O时,该函数才返回。在返回时,它告诉进程哪些描述符已准备好可以进行I/O。

poll、pselect和select这三个函数使我们能够执行I/O多路转接。

一、select函数

在所有依从POSIX的平台上,select函数使我们可以执行I/O多路转接。传向select的参数告诉内核:

  • 我们所关心的描述符。
  • 对于每个描述符我们所关心的状态。(是否读一个给定的描述符?是否想写一个给定的描述符?是否关心一个描述符异常状态?)
  • 愿意等待多长时间(可以永远等待,等待一个固定量时间或完全不等待)。

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

  • 已准备好的描述符的数量。
  • 对于读、写或异常这三个状态中的每一个,哪些描述符已准备好。
#include <sys/select.h>
int select(int maxfdp1,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *tvptr);//返回值:准备就绪的描述符数,若超时则返回0,若出错则返回-1

先说明最后一个参数,它指定愿意等待的时间:

struct timeval{
long tv_sec; //seconds
long tv_usec;//and microseconds
}

有三种情况:

  • tvptr == NULL

永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的一个已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR

  • tvptr->tv_sec == 0 && tvptr->tv_usec == 0

完全不等待。测试所有指定的描述符并立即返回。

  • tvptr->tv_sec != 0 || tvptr->tv_usec != 0

等待指定的秒数和微妙数。当指定的描述符之一已准备好或当指定的时间值已经超过时立即返回。如果在超时时还没有一个描述符准备好,则返回值是0.与第一种情况一样,这种等待可被捕捉到的信号中断。

     中间三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符。每个描述符集存放在一个fd_set数据类型中。这种数据类型为每一可能的描述符保持了一位。
                                           UNIX环境高级编程——I/O多路转接(select、pselect和poll)
     对fd_set数据类型可以进行的处理是:分配一个这种类型的变量;将这种类型的一个变量值赋予同类型的另一个变量;或对于这种类型的变量使用下列四个函数中的一个。
#include <sys/select.>
int FD_ISSET(int fd,fd_set *fdset);//返回值:若fd在描述符集中则返回非0值,否则返回0
void FD_CLR(int fd,fd_set *fdset);
void FD_SET(int fd,fd_set *fdset);
void FD_ZERO(fd_set *fdset);
  • 调用FD_ZERO将一个指定的fd_set变量的所有位设置为0.
  • 调用FD_SET设置一个fd_set变量的指定位。
  • 调用FD_CLR则将一指定位清除。
  • 调用FD_ISSET测试一指定位是否设置。

select的中间三个参数(指向描述符集的指针)中的任意一个或全部都可以是空指针,这表示对相应状态并不关心。如果所有三个指针都是空指针,则select提供了较sleep更精确的计时器。
     select的第一个参数maxfdp1的意思是“最大描述符加1”。在三个描述符集中找出最大描述符值,然后加1,这就是第一个参数。也可以将第一个参数设置为FD_SETSIZE,这是<sys/select.h>中的一个常量,它说明了最大的描述符(经常是1024)。如果将第一个参数设置为我们关注的最大描述符编号值加1,内核就只需在此范围内寻找打开的位,而不必在三个描述符集中的数百位内搜索。

例如,若编写下列代码:
   fd_set readset,writeset;
FD_SERO(&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);

                               UNIX环境高级编程——I/O多路转接(select、pselect和poll)
     因为描述符编号从0开始,所以要在最大描述符编号值上加1。第一个参数实际上是要检查的描述符(从描述符0开始)。
select有三个可能的返回值:
  • 返回-1表示出错。出错是有可能的,例如在所指定的描述符都没有准备好时捕捉到一个信号。在此种情况下,将不修改其中任何描述符集。(即原先描述符值1的还是保持1,不变)
  • 返回0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经超时,则发生这种情况。此时,所有描述符集皆被清0
  • 正返回值表示已经准备好的描述符数,该值是三个描述符集中已经准备好的描述符数之和,所以如果同一描述符已准备好读和写,那么在返回值中将计2。在这种情况下,三个描述符集中仍旧打开的位对应于已准备好的描述符,其他没有准备好的清0。
     如果在一个描述符上碰到了文件结尾处,则select认为该描述符是可读的。然后调用read,它返回0,这是UNIX系统指示到达文件结尾处的方法。(很多人错误的认为,当到达文件结尾处时,select会指示一个异常状态)。
二、pselect函数
#include <sys/select.h>
int pselect(int maxfdp1,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,const struct timespec *tsptr,const sigset_t *sigmask);//返回值:准备就绪的描述符数,若超时则返回0,若出错则返回-1

除以下几点,pselect与select相同:

  • select的超时值用timeval结构指定,但pselect使用timespec,timespec结构以秒和纳秒表示超时值,而非秒和微妙。如果平台支持这样精细的粒度,那么timespec就提供了更精准的超时时间。
  • 对于pselect可使用一可选择的信号屏蔽字。若sigmask为空,那么在与信号有关的方面,pselect的运行状况和select相同。否则,sigmask指向一信号屏蔽字,在调用pselect时,以原子操作的方式安装该信号屏蔽字。在返回时恢复以前的信号屏蔽字。
三、poll函数
#include <poll.h>
int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);

与select不同,poll不是为每个状态(可读性、可写性和异常状态)构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号以及对其关心的状态。

struct pollfd{
int fd; //file descriptor to check,or < 0 ignore
short events; //events of interest on fd
short revents; //events that occurred on fd
}

fdarray数组中的元素数由nfds说明。
     应将每个数组元素的events成员设置为下图。通过这些值告诉内核我们对该描述符关心的是什么。返回时,内核设置revents成员,以说明对于该描述符已经发生了什么事件。(注意:poll没有更改events成员,这与select不同,select修改其参数以指示哪一个描述符已准备好了)

UNIX环境高级编程——I/O多路转接(select、pselect和poll)
     最后三行是由内核在返回时设置的。即使在events字段中没有指定这三个值,如果相应条件发生,则在revents中也返回它们。
     poll的最后一个参数说明我们愿意等待时间。有三种不同的情形:
  • timeout == -1 永远等待。当所指定的描述符中的一个已准备好,或捕捉到一个信号时则返回。如果捕捉到一个信号,则poll返回-1,errno设置为EINTR。
  • timeout == 0  不等待。
  • timeout > 0   等待timeout毫秒。当指定的描述符之一已准备好,或指定的时间值已超过时立即返回。如果已超时,但是还没有一个描述符准备好,则返回值是0。