美文网首页LinuxLinux学习之路
APUE读书笔记-14高级输入输出(4)

APUE读书笔记-14高级输入输出(4)

作者: QuietHeart | 来源:发表于2020-07-09 18:10 被阅读0次

    5、多I/O

    当我们从一个文件描述符号读取,写入到另外一个文件描述符号的时候,我们可以在一个循环中使用"阻塞"I/O,如下:

    while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
            if (write(STDOUT_FILENO, buf, n) != n)
                    err_sys("write error");
    

    我们经常看到这种形式的I/O阻塞。如果我们需要读取两个文件描述符号的时候会怎么样呢?在这种情况下,我们不能在两个文件描述符号上面做阻塞的读取,因为当我们在一个文件描述符号阻塞读取的时候,数据可能会在另外一个文件描述符号中出现。所以需要一种不同的技术来处理这种情况。

    让我们来看一下telnet命令。在这个程序中,我们从终端(标准输入)中读取,然后写到网络连接中,我们又从网络连接中读取,然后再写到终端上(标准输出)。在网络的另一端,telnetd守护进程读取到我们键入的字符并且把它传递给shell,就好象我们登陆到了远程的机器上面一样。telnetd守护进程发送我们通过telnet命令键入的命令产生的任何输出给我们,然后显示到我们的终端上。

    本章的参考资料中给出了一个图示,这里简单描述一下:

    终端前面的用户<--->telnet命令<--->telnetd守护进程
    

    telnet进程有两个输入以及两个输出,这里我们不能在读取任何一个输入的时候阻塞,因为我们也不知道在哪个输入上面有我们所需要的数据。

    有一个处理这个问题的方法就是把进程分成两个部分(使用fork),每一个部分处理一个方向的数据。如下图所示:

                 /-->telnet命令(父) -->\
    终端前面的用户                     telent守护进程
                 \<--telnet命令(子) <--/
    

    如果我们使用进程,我们可以让每一个进程做阻塞读取。但是这样会在操作结束的时候导致一些问题。如果子进程收到了文件结束符号,子进程会终止,同时父进程收到SIGCHLD信号。但是如果父进程结束(用户在终端收到文件描述符号),那么父进程会告诉子进程结束(谁结束??????)。我们可以通过信号来实现这个(例如SIGUSR1信号),但是这让程序在一定的程度上复杂化了。

    我们也可以通过使用多线程来代替多进程的方案,这样就避免了结束时候的复杂机制,但是却增加了同步的问题,这让问题更为复杂。

    我们可以为两个文件描述符号设置非阻塞标志,然后在单个进程中使用非阻塞I/O,为第一个文件描述符号发起读操作。如果存在数据,那么我们就读取数据并且处理它们,如果不存在数据那么就立即返回。然后我们在第二个文件描述符号上面做同样的事情。在这些之后,我们会等待一定时间(例如几秒钟),然后再次尝试从第一个文件描述符号开始读取数据,这种类型的循环叫做polling(轮询),问题是它会浪费cpu时间,大多数的时候我们没有数据可以读取所以我们把时间浪费在执行系统调用上面,我们需要猜测每次循环的时候需要等待多少时间。虽然任何系统上面都支持非阻塞I/O,但是我们要在多任务系统上面避免轮询。

    另外一个技术叫做异步I/O。为了执行这个,我们告诉内核当有文件描述符号可以用来输入输出的时候通知我们。对于这个技术,有两个问题:首先,并不是所有的系统都支持这个特性(在Single UNIX Specification中这是一个可以选择的特性),System V提供了SIGPOLL信号用于这个技术,但是这个信号只有在文件描述符号引用一个流设备的时候才好用。BSD系统有一个类似的信号,SIGIO,但是这个也有类似的限制,它只有在文件描述符号引用一个网络设备或者终端的时候才有用。第二个问题是每个进程只能有一个这些信号(SIGPOLL或者SIGIO)。如果我们为两个文件描述符号激活这个信号(这个例子我们已经说过,也就是从两个文件描述符号中读取),那么发生信号的时候,我们无法分辨到底是哪个文件可以读取。为了能够可以确定是那个文件描述符号可以读取,我们还是需要对每个文件描述符号设置成非阻塞标志,然后依次对它们进行尝试。我们在后面会对异步I/O进行简单的介绍。

    一个更好的技术就是使用多I/O技术。通过这个技术,我们建立一个我们所感兴趣的文件描述符号列表,然后调用一个函数,这个函数不会返回,它会一直等待直到其中的一个文件描述符号可以I/O操作。在返回的时候,我们可以知道是哪个文件描述符号可以进行I/O了。

    有三个函数:poll,pselect,以及select允许我们执行多I/O操作。下面的表格告诉我们哪个平台支持它们(如果表格不够清晰可以参见参考资料中给出的表格)。需要注意的是,select是在POSIX.1的基础上定义的,但是poll却是一个XSI扩展标准基础上定义的。

                    不同系统上面支持的多I/O情况
    +-----------------------------------------------------------+
    | System        | poll | pselect | select | <sys/select.h>  |
    |---------------+------+---------+--------+-----------------|
    | SUS           | XSI  |    •    |   •    |        •        |
    |---------------+------+---------+--------+-----------------|
    | FreeBSD 5.2.1 |  •   |    •    |   •    |                 |
    |---------------+------+---------+--------+-----------------|
    | Linux 2.4.22  |  •   |    •    |   •    |        •        |
    |---------------+------+---------+--------+-----------------|
    | Mac OS X 10.3 |  •   |    •    |   •    |                 |
    |---------------+------+---------+--------+-----------------|
    | Solaris 9     |  •   |         |   •    |        •        |
    +-----------------------------------------------------------+
    

    POSIX指明包含<sys/select>来推送select的信息到你的程序中。然而在以前我们可能需要包含三个文件,并且有些实现并不符合标准。我们可以检查man手册来确定你的系统支持什么样的select。原来的系统可能需要你包含<sys/types.h>,<sys/time.h>,以及<unistd.h>.

    多I/O在4.2BSD中通过select函数实现。虽然一般都是用在终端I/O以及网络I/O中,但是这个函数可以用于任何文件描述符号。SVR3在加入流机制的时候添加了poll函数。然而最初,poll只能用于流设备,在SVR4的时候poll才增加了任何文件描述符号的支持。

    select和pselect函数

    select函数允许我们在所有的POSIX兼容的平台下面使用多I/O操作。我们传递给select的参数会告诉内核:

    1. 我们对哪些文件描述符号感兴趣
    2. 对于我们感兴趣的文件描述符号的条件性质(用于读取?用于写入?还是用于意外输出?)
    3. 我们可以等待多久。(我们可以设置永远等待,或者等待一个固定的时间,或者根本不等待)

    在从select中返回来的时候,内核会告诉我们:

    1. 已经准备好了的文件描述符号的总数目。
    2. 对于三个条件(读,写,意外)的文件描述符号,哪些已经准备好了。

    根据这些返回的信息,我们就可以调用合适的I/O函数(一般都是read或者write函数),并且知道这些被调用的函数是不会阻塞了。

    #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表示错误。

    我们先看看最后一个参数,这个参数指定我们需要等待多长时间:

    struct timeval {
            long tv_sec;     /* 秒数 */
            long tv_usec;    /* 微秒数 */
    };
    

    有三种情况:

    1. tvptr == NULL

      表示永远等待。这个无限的等待可以被我们捕捉的信号打断。当指定的文件描述符号准备好了,或者捕获到了一个信号的时候,这个函数会返回。如果是捕获到了一个信号的话,select会返回1并设置errno为EINTR。

    2. tvptr->tv_sec = 0 && tvptr->tv_usec = 0

      一点也不等待。检测所有指定的文件描述符号并且立即返回。采用这个方法对系统进行轮询,可以检测指定的多个文件描述符号的状态,而不用在select函数中阻塞了。

    3. tvptr->tv_sec != 0 || tvptr->tv_usec != 0

      等待指定数目的秒数或者微秒数,当指定文件描述符号中的一个准备就绪或者超时的时候,就会返回。(应该是阻塞式的等待)如果没有一个指定的文件描述符号准备好但是却超时了,那么返回0。如果系统没有提供微秒级别的精度那么会取整选择最接近的支持的值。同第一种情况类似这个等待也可以被信号打断。

    POSIX.1允许实现修改这个timeval结构,所以当select返回的时候,你不能假定这个结构变量的值和调用select之前的值一样。FreeBSD5.2.1,Mac OS X 10.3和Solaris9不会改变这个结构变量的值,但是Linux2.4.22会更新这个结构变量,如果select函数在超时之前返回,那么这个结构变量包含的就是剩余的等待时间。

    中间的三个参数readfds,writefds,和exceptfds指向文件描述符号集合的指针。这三个集合指定了我们所感兴趣的文件描述符号,以及它们可能的情况(用于读取,用于写入,以及用于例外条件)。一个文件描述符号集合存放在一个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);
    void FD_SET(int fd, fd_set *fdset);
    void FD_ZERO(fd_set *fdset);
    

    这些接口可以用宏也可以用函数来实现。一个fd_set可以通过FD_ZERO来将其所有的位设置为0。可以通过FD_SET将一个单个的位设置为1,FD_CLR可以将一个单个的位清零。最后,我们可以使用FD_ISSET来检测一个给定的位是否是打开状态(即为1)。

    声明完了一个文件描述符号集合之后,我们必须通过调用FD_ZERO将它清零。然后我们可以设置这个集合中的位表示每个我们感兴趣的文件描述符号,如下:

    fd_set   rset;
    int      fd;
    FD_ZERO(&rset);
    FD_SET(fd, &rset);
    FD_SET(STDIN_FILENO, &rset);
    

    从select返回的时候,我们使用FD_ISSET来检测文件集合中的一个给定的位是否仍然处于打开状态(也就是一个文件描述符号是否属于这个文件集合):

    if (FD_ISSET(fd, &rset)) {
            ...
    }
    

    如果我们对某一个条件不感兴趣,那么我们可以将中间的三个参数(也就是指向文件描述符号集合的指针)的任何设置为空。如果三个参数都是空,那么我们会得到一个比sleep函数更精度的定时器。(前面我们说过,sleep函数等待的时间精度是秒,而通过select我们可以等待小于1秒的时间,实际的精度取决于系统的时钟)本章后面有一个练习就涉及到这个。

    select函数的第一个参数maxfdp1代表最大的文件描述符号加1。我们会计算我们所感兴趣的文件描述符号(它们都包含在三个文件描述符号集合中,这三个文件描述符号集合由中间三个参数表示),然后对最大的文件描述符号加1,这样得到第一个参数。我们可以只设置第一个参数为FD_SETSIZE,这个常量在<sys/select.h>中定义,指定了最大的文件描述符号(一般为1024),但是这个值太大了。实际上,大多数应用程序一般只使用大约3到10的文件描述符号,(有些应用程序可能会使用更多的文件描述符号,但是这样的unix程序不是常见的程序),通过指定我们所感兴趣的最大的文件描述符号,我们可以防止内核为了查看一个打开的位而从这三个文件描述符号集合中检测非常多没有使用的位。

    下面是一个使用的例子:

    fd_set readset, writeset;
    FD_ZERO(&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);
    

    在参考资料中有相应的图,这里就不给出了。这个图表示的执行之后的结果意思大致如下:

    readset:1001......
    write:0110......
    

    并且最大文件描述符号maxfdp1的值为4。

    第一个参数的值是最大文件描述符号加1的原因是,这个参数表示我们将要检测的文件描述符号的数目,而文件描述符号的数值的开始为0。

    select函数有三种可能的返回值:

    1. 返回1表示一个错误的发生。例如,如果在任何一个文件描述符号准备好之后,捕获到一个信号,这个时候,文件描述符号集合没有被修改。
    2. 返回0表示没有文件描述符号被修改。如果在任何文件描述符号准备好了之前超时,那么就会发生这个情况。当发生这个情况的时候,所有的文件描述符号集合会被清0。
    3. 返回正数表示有一定数目的文件描述符号准备好了。这个返回值就是所有三个文件描述符号集合中准备好的文件描述符号的数目,所以如果同样的一个文件描述符号准备好了读写,那么将会在返回值中被计算两次。三个文件描述符号集合中留下来的位将只对应相应准备好了的文件描述符号。

    这里,准备好的含义如下:

    1. 一个在read文件描述符号集合(readfds)中的文件描述符号,当被读取的时候不会导致阻塞,那么就认为它已经准备好了。
    2. 一个在write文件描述符号集合(writefds)中的文件描述符号,当被写入的时候不会导致阻塞,那么就认为它已经准备好了。
    3. 一个在异常文件描述符号集合(exceptfds)中的文件描述符号,如果有一个异常的情况提交在那个文件描述符号上面,就认为它已经被准备好了。当前,异常情况一般对应的包含:在网络连接中,到达了一个带外的数据,或者有一个特定的条件在伪终端上发生而这个终端已经在packet模式(这个不太了解)。
    4. 普通文件的读,写,例外情况的文件描述符号总是准备好了的。

    有一个非常重要的,需要我们意识到的问题就是,文件描述符号是否是阻塞的并不会影响select函数是否阻塞。也就是说,如果我们有一个非阻塞的文件描述符号想要从中读取,并且我们调用select函数设置超时值为5秒,那么select也将会阻塞5秒(尽管文件描述符号是非阻塞的文件描述符号)。类似地,如果我们指定一个无限超时,那么select也会一直阻塞,直到有数据来或者接收到了一个信号。

    POSIX.1也定义了一个select函数的变种函数叫做pselect.

    #include <sys/select.h>
    int pselect(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds,
                                         const struct timespec *restrict tsptr, const sigset_t *restrict sigmask);
    

    返回已经准备好了的文件描述符号的数目,或者如果超时的时候返回0,或者如果错误的时候返回1。

    pselect函数和select函数是一样的,不同之处在于:

    1. select函数的超时值用timeval结构表示,但是pselect使用timespec结构(前面有提到过)。这个timespec不是像timeval那样使用秒和微秒表示时间,它使用秒和纳秒表示时间。如果平台支持这样级别的精度,那么它会有更高的精度。
    2. pselect的超时值被声明成常量,这样我们就能够确保,当pselect函数返回的时候,它的超时值不会发生变化。
    3. pselect有一个signal mask参数。如果sigmask参数为空,那么pselect处理信号的方式和select一样。否则,sigmask指向一个signal mask,这个signal mask在pselect被调用的时候会被自动地安装上。返回的时候,之前的signal mask会被恢复。

    poll函数

    poll函数和select函数类似,但是它们的编程接口不太一样。之前我们已经知道,poll本来是来自system V的,尽管我们可以将它用于各种文件类型,但是poll一般用于STREAMS系统。

    #include <poll.h>
    int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
    

    返回已经准备好的文件描述符号的数目,超时的时候返回0,错误的时候返回1。

    使用poll,不像select那样分别使用文件描述符号集合表示每种条件(读,写,例外),而是使用一个pollfd结构的数组,这个数组的每个成员指定了文件描述符号以及相应的条件。如下:

    struct pollfd {
            int   fd;       /* 将要检测的文件描述符号,如果小于0则表示忽略 */
            short events;   /* 对于文件描述符号所感兴趣的事件 */
            short revents;  /* 发生在fd上面的事件 */
    };
    

    fdarray数组元素的数目通过nfds来指定。

    对于nfds参数的声明,以前有所不同。SVR3指定数组中元素的数目为unsigned long类型,这样感觉有点多余。在SVR4的手册上面,poll函数的声明中的第二个参数类型是size_t。但是,实际上<poll.h>头文件中的第2个参数仍然是unsigned long类型。Single UNIX Specification定义了一个新的nfds_t类型允许选择合适的类型并且向应用程序隐藏了细节。需要注意的是,这个类型足以容纳一个整数,返回值代表满足事件的数组元素的数目。

    SVR4相应的SVID将第一个参数作为pollfd结构的数组fdarray[]。然而SVR4的手册上面却将其声明为pollfd *fdarray,在c语言中这两种声明是等价的,我们使用第一个方法只是为了强调它是一个数组而不是一个指向单个结构体的指针。

    为了告诉内核我们对文件描述符号的哪个事件感兴趣,我们需要如下设置每个数组元素的events成员(表格可能不清晰需要参见参考资料)。返回的时候revents成员由内核来进行设置,用来指定文件描述符号上面发生了什么事件(这里注意和select有所不同的是poll不会改变events成员,这样表示什么准备好了)。

    poll的events和revents

    1. POLLIN: events 且revents,除了高优先级别的数据可以非阻塞的读取(等价POLLRDNORM|POLLRDBAND)。
    2. POLLRDNORM: events 且revents,正常数据(优先级范围为0)可以非阻塞读取。
    3. POLLRDBAND: events 且revents,非0优先级别的数据可以被无阻塞的读取。
    4. POLLPRI: events 且revents,高优先级别的数据可以被无阻塞的读取。
    5. POLLOUT:events 且revents,正常数据(优先级范围为0)可以非阻塞写。
    6. POLLWRNORM:events 且revents,同POLLOUT.
    7. POLLWRBAND:events 且revents,非0优先级别的数据可以被无阻塞的写。
    8. POLLERR:revents,发生错误。
    9. POLLHUP:revents,发生hangup。
    10. POLLNVAL:revents,文件描述符号没有引用一个打开的文件。

    前4行用于读,接下来的3行用于写,最后3行表示例外条件。最后3行在返回的时候通过进行设置。甚至在没有指定events的时候,这三个值在发生条件的时候会返回到revents中。

    当一个文件描述符号挂起的时候(POLLHUP),我们不能向文件描述符号中写。然而在文件描述符中或许仍有数据需读取。

    poll的最后一个参数指定我们想要等待多长时间。类似select函数,它也有三种情况:

    1. timeout == -1:

      永远等待。(有些系统将<stropts.h>中的INFTIM定义为1)当有一个指定的文件描述符准备好了或者捕捉到信号之时,我们会返回。如果捕获到了信号,那么poll返回1并且设置错误号码(errno)为EINTR。

    2. timeout == 0:

      不进行等待。所有指定的文件描述符都会被测试,然后我们会立即返回。使用这种方法可以对系统进行轮询以获取多个文件描述符的状态,并且不会在poll函数中被阻塞。

    3. timeout > 0:

      等待指定的微妙数。我们会在一个文件描述符号准备好了之后或者超时之后返回。如果在文件描述符号准备好了之前就超时了,会返回0。(如果你的系统不提供微秒的精度,那么会取最接近的整数值)

    注意到文件结尾和hangup的区别是非常重要的。如果我们从终端输入数据,并键入文件结尾字符,POLLIN被打开这样我们可以读取到文件结束标记(read返回0)。POLLHUP不在revents中被打开。如果我们从modem读取并且telephone行hangup了,那么我们会接收到POLLHUP通知。

    和select类似,文件描述符号是否阻塞不会影响poll函数是否阻塞。

    select和poll函数的可中断性

    当4.2BSD中介绍了被中断的系统调用的自动重新启动的时候,select不被重新启动。这个特性在大多数系统中都是这样,指定了SA_RESTART选项的时候也是如此。但是在SVR4中,如果指定了SA_RESTART的时候,select和poll函数会自动重新启动。为了防止我们将软件从基于SVR4的系统移植的时候遇到这个问题,如果信号可以中断一个select或者poll函数,那么我们使用signal_intr函数(前面自己编写的一个防止被中断的系统调用重新启动的一个方法)。

    本书中的四种系统,在接收到信号的时候,甚至设置了SA_RESTART标记的时候,也不会重新启动poll或者select函数。

    译者注

    原文参考

    参考: APUE2/ch14lev1sec5.html

    相关文章

      网友评论

        本文标题:APUE读书笔记-14高级输入输出(4)

        本文链接:https://www.haomeiwen.com/subject/dcorfktx.html