如何才能read多个描述符呢,有下面这些方法。
a.使用多个进程,每个进程执行阻塞read,但是这也产生了问题,操作什么时候停止?如果子进程接收到文件结束符,那么
子进程终止,然后父进程接收到SIGCHLD信号,但是,若父进程终止,那么父进程应该通知子进程停止,为此可以使用一个
信号(例如SIGUSR1),但这是程序变得更加复杂。
b.使用多线程,这避免了终止的复杂性,但是要求处理线程之间的同步同样是程序复杂。
c.使用非阻塞IO。将多个描述符设置成非阻塞。对第一个描述符进行read,如果该输入上有数据,则读数据并处理,如果
无数据可读,则read立即返回。然后对下一个描述符进行相同的处理,这种方法的不足之处是浪费cpu时间,如果大多数
时间实际上是无数据可读,但是仍花费时间不断反复执行read系统调用。
d.使用异步IO。其基本思想是进程告诉内核,当一个描述符已准备好可以进行IO时,用一个信号通知它。这种技术有两个
问题。第一,并非所有系统都支持这种机制。system V为此技术提供了SIGPOLL信号,但是仅当描述符引用STREAMS设备
时,此信号才能工作。BSD有一个类似的信号SIGO,但也有类似的限制,仅当描述符引用终端设备或网络时才能工作,其次,
这种信号对每个进程而言只有1个,如果使该信号对两个描述符都其作用,那么在接到信号时进程无法判别是哪一个描述符
已经准备好。
e.使用IO多路转换(IO multiplexing)。先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中一个已准备
好进行IO,该函数才返回,在返回时,它告诉进程哪些描述符已准备好。
poll,pselect和select函数使我们能够执行IO多路转换。
1.select函数
#include<sys/select.h> int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr);
//返回准备就绪的描述符,若超时则返回0,出错返回-1.
下面对每个参数进行说明:
tvptr:愿意等待的时间。
struct timeval{
long tv_sec; //second
long tv_usec; //microseconds
}
有三种情况:
tvptr == NULL :
永远等待。如果捕捉到一个信号则中断此无限等待,select返回-1,errno设置为EINTR。
tvptr->tv_sec == 0 && tvptr->usec == 0:
完全不等待。测试所指定的描述符并立即返回。
tvptr->tv_sec != 0 || tvptr->tv_usec != 0:
等待指定的秒数和微秒数。当指定的描述符之一已经准备好,或当指定的时间值已经超过时立即返回。如果在超时是还没有
一个描述符准备好,则返回0.与第一种情况一样,这种等待可被捕捉到的信号中断。
readfds,writefds和execptfds:指向描述符集的指针。这三个描述符集说明了我们关心的可读,可写或处于异常条件的各个
描述符。每个描述符集存放在一个fd_set数据类型中。这种数据类型为每个可能的描述符保持了一位。
可以使用下面的函数处理fd_set数据类型。
#include<sys/select.h> int FD_ISSET(int fd, fd_set *fdset); //若fd在描述符集中则返回非0值,否则返回0 void FD_CLR(int fd, fd_set *fdset); //清除fd在fdset中的位。 void FD_SET(int fd, fd_set *fdset); //设置fd在fdset中的位。 void FD_ZERO(fd_set *fdset); //清除整个fdset。
maxfdp1:意思是“最大描述符加1”。在三个描述符集中找到最大描述符编号值,然后加1,这就是第一个参数值。也可将这个
参数设置为FD_SETSIZE,这是<sys/select.h>中的一个常量,它说明了最大的描述符数(通常是1024)。
select函数有三个可能的返回值:
1.返回值-1表示出错。这种情况下,将不修改其中任何描述符集。
2.返回值0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经超过,则发生这种情况。此时描述符
集都被清0.
3.正返回值表示已经准备好的描述符数,该值是三个描述符集中已准备好的描述符之和。三个描述符集中仍旧打开的位对应
与已准备好的描述符。
对于准备好的意思要做一些更具体的说明:
a.若对读集readfds中的一个描述符的read操作将不会阻塞,则此描述符是准备好的。
b.若对写集writefds中的一个描述符的write操作将不会阻塞,则此描述符是准备好的。
c.若异常状态集exceptfds中的一个描述符有一个未决异常状态,则此描述符时准备好的。现在,异常状态包括(a)在网络
连接上到达的带外数据,或者(b)在处于数据包模式的伪终端上发生了某些状态。
实践:
#include <stdio.h> #include <sys/time.h> #include <unistd.h> #include <string.h> int main(void){ char rbuf[10]; fd_set rd_fds; int ret,len; struct timeval tv; tv.tv_sec = 3; tv.tv_usec = 0; while(1){ FD_ZERO(&rd_fds); FD_SET(STDIN_FILENO,&rd_fds); tv.tv_sec = 3; tv.tv_usec = 0; ret = select(1,&rd_fds,NULL,NULL,&tv); if(ret < 0){ perror("select"); break; }else if(ret == 0){ printf("timeout,goto next loop\n"); }else{ printf("ret = %d\n",ret); if(FD_ISSET(STDIN_FILENO,&rd_fds)){ len =read(STDIN_FILENO,rbuf,9); rbuf[len] = ‘\0‘; printf("%s",rbuf); } } } }运行结果:
root@gmdz-virtual-machine:~# ./a.out
timeout,goto next loop
123
ret = 1
123
321
ret = 1
321
当三秒不输入,select函数返回。输入字符后,select提示有1个描述符准备好,然后读出刚才输入的字符。
程序中需要注意的是,在select返回后,tv中保存的是等待剩余的时间,也就是说如果你在select函数后,等待1秒再输入字符串,
那么select返回后,tv中的时间为2秒。所以在下次select之前,还需要重新设置下时间。
2.pselect函数
POSIX.1也定义了一个select的变体,它被称为pselect。
#include<sys/select.h> int pselect(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict execptfds, const struct timespec *restrict tsptr, const sigset_t *restrict sigmask); //返回值是准备就绪的描述符,若超时返回0,出错返回-1pselect与select有以下几个不同:
a.select的超时用timeval结构指定,pselect使用timespec结构。
b.pselect的超时值被申明为const,这保证了调用pselect不会改变此值。
c.对于pselect可使用一个可选择的信号屏蔽字。若sigmask为空,那么在于信号有关的方面,pselect的运行状况和select
相同。否则,sigmask指向一个信号屏蔽字,在调用pselect时,以原子操作的方式安装该信号屏蔽字,在返回时回复以前
的信号屏蔽字。
3.poll函数
poll函数类似于select,但是其接口有所不同。
#include <poll.h> int poll(struct pollfd fdarray[], nfds_t nfds, int timeout); //返回就绪的描述符数,超时则返回0,出错则返回-1.poll不是为每个状态(可读,可写,异常状态)构造一个描述符集,而是构造一个pollfd结构数组。每个数组元素指定一个
描述符编号以及对其所关心的状态。
struct pollfd{
int fd; //file descriptor to check,or <0 to ignore
short events; //events of interest on fd;
short revents; //events that occurred on fd
}
fdarray数组中的元素数由nfds说明。
每个数组元素的events成员设置为下表中所示的值,通过这些值告诉内核我们对描述符关心的是什么。返回时,内核设置
revents程序,以说明对于该描述符已经发生了什么事件。(poll没有更改events成员)
当一个描述符被POLLHUP后,就不能再写向该描述符了,但是仍可能从该描述符读取数据。
timeout参数说明我们愿意等待多少时间。
timeout == -1:永远等待。
timeout == 0:不等待,测试所有的描述符并立即返回。
timeout>0:等待timeout毫秒,当指定的描述符之一已经准备好,或指定的时间值已经超过时立即返回。
实践:
#include <stdio.h> #include <sys/poll.h> #include <unistd.h> #include <string.h> int main(void){ char rbuf[10]; struct pollfd event; event.fd = STDIN_FILENO; event.events = POLLIN; int ret,len; while(1){ ret = poll(&event,1,3000); if(ret < 0){ perror("poll"); break; }else if(ret == 0){ printf("timeout,goto next loop\n"); }else{ printf("ret = %d\n",ret); if(event.revents == POLLIN){ len =read(STDIN_FILENO,rbuf,9); rbuf[len] = ‘\0‘; printf("%s",rbuf); } } } }运行结果:
root@gmdz-virtual-machine:~# ./a.out
timeout,goto next loop
123
ret = 1
123
321
ret = 1
321
当三秒不输入,poll函数返回。输入字符后,poll提示有1个描述符准备好,然后读出刚才输入的字符。