《UNIX环境高级编程》笔记--IO多路转换

如何才能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数据类型中。这种数据类型为每个可能的描述符保持了一位。

《UNIX环境高级编程》笔记--IO多路转换_第1张图片

可以使用下面的函数处理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,出错返回-1
pselect与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成员)

《UNIX环境高级编程》笔记--IO多路转换_第2张图片

当一个描述符被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个描述符准备好,然后读出刚才输入的字符。

你可能感兴趣的:(《UNIX环境高级编程》笔记--IO多路转换)