Linux下多路复用I/O接口

分享到:
           

    多路复用

    1.函数说明

    前面的fcntl()函数解决了文件的共享问题,接下来该处理I/O复用的情况了。

    总的来说,I/O处理的模型有5种。

    ● 阻塞I/O模型:在这种模型下,若所调用的I/O函数没有完成相关的功能,则会使进程挂起,直到相关数据到达才会返回。如常见对管道设备、终端设备和网络设备进行读写时经常会出现这种情况。
    ● 非阻塞I/O模型:在这种模型下,当请求的I/O操作不能完成时,则不让进程睡眠,而且立即返回。非阻塞I/O使用户可以调用不会阻塞的I/O操作,如open()、write()和read()。如果该操作不能完成,则会立即返回出错(如打不开文件)或者返回0(如在缓冲区中没有数据可以读取或者没空间可以写入数据)。
    ● I/O多路转接模型:在这种模型下,如果请求的I/O操作阻塞,且它不是真正阻塞I/O,而是让其中的一个函数等待,在此期间,I/O还能进行其他操作。如本小节要介绍的select()和poll()函数,就是属于这种模型。
    ● 信号驱动I/O模型:在这种模型下,进程要定义一个信号处理程序,系统可以自动捕获特定信号的到来,从而启动I/O。这是由内核通知用户何时可以启动一个I/O操作决定的。

    它是非阻塞的。当有就绪的数据时,内核就向该进程发送SIGIO信号。 无论我们如何处理SIGIO信号,这种模型的好处是当等待数据到达时,可以不阻塞。主程序继续执行,只有收到SIGIO信号时才去处理数据即可。

    ● 异步I/O模型:在这种模型下,进程先让内核启动I/O操作,并在整个操作完成后通知该进程。这种模型与信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知进程I/O操作何时完成的。现在,并不是所有的系统都支持这种模型。

    可以看到,select()和poll()的I/O多路转接模型是处理I/O复用的一个高效的方法。它可以具体设置程序中每一个所关心的文件描述符的条件、希望等待的时间等,从select()和poll()函数返回时,内核会通知用户已准备好的文件描述符的数量、已准备好的条件(或事件)等。通过使用select()和poll()函数的返回结果(可能是检测到某个文件描述符的注册事件或是超时,或是调用出错),就可以调用相应的I/O处理函数了。

    2.函数格式

    select()函数的语法要点如表2.8所示。

表2.8 select()函数语法要点

所需头文件 #include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
函数原型 int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exeptfds, struct timeval *timeout)
函数传入值 numfds:该参数值为需要监视的文件描述符的大值加1
readfds:由select()监视的读文件描述符集合
writefds:由select()监视的写文件描述符集合
exeptfds:由select()监视的异常处理文件描述符集合
timeout NULL:永远等待,直到捕捉到信号或文件描述符已准备好为止
具体值:struct timeval类型的指针,若等待了timeout时间还没有检测到任何文件描符准备好,就立即返回
0:从不等待,测试所有指定的描述符并立即返回
函数返回值 成功:准备好的文件描述符
0:超时; 1:出错

    可以看到,select()函数根据希望进行的文件操作对文件描述符进行了分类处理,这里对文件描述符的处理主要涉及4个宏函数,如表2.9所示。

表2.9 select()文件描述符处理函数

FD_ZERO(fd_set *set) 清除一个文件描述符集
FD_SET(int fd, fd_set *set) 将一个文件描述符加入文件描述符集中
FD_CLR(int fd, fd_set *set) 将一个文件描述符从文件描述符集中清除
FD_ISSET(int fd, fd_set *set) 如果文件描述符fd为fd_set集中的一个元素,则返回非零值,可以用于调用select()后测试文件描述符集中的哪个文件描述符是否有变化

    一般来说,在每次使用select()函数之前,首先使用FD_ZERO()和FD_SET()来初始化文件描述符集(在需要重复调用select()函数时,先把一次初始化好的文件描述符集备份下来,每次读取它即可)。在select()函数返回后,可循环使用FD_ISSET()来测试描述符集,在执行完对相关文件描述符的操作后,使用FD_CLR()来清除描述符集。

    另外,select()函数中的timeout是一个struct timeval类型的指针,该结构体如下所示:

    struct timeval
    {
        long tv_sec; /* 秒 */
        long tv_unsec; /* 微秒 */
    }

    可以看到,这个时间结构体的精确度可以设置到微秒级,这对于大多数的应用而言已经足够了。
    poll()函数的语法要点如表2.10所示。

表2.10 poll()函数语法要点

所需头文件     #include <sys/types.h>
    #include <poll.h>
函数原型 int poll(struct pollfd *fds, int numfds, int timeout)
函数传入值 fds:struct pollfd结构的指针,用于描述需要对哪些文件的哪种类型的操作进行监控
struct pollfd
{
  int fd; /* 需要监听的文件描述符 */
  short events; /* 需要监听的事件 */
  short revents; /* 已发生的事件 */
}
events成员描述需要监听哪些类型的事件,可以用以下几种标志来描述。
POLLIN:文件中有数据可读,下面实例中使用到了这个标志
POLLPRI::文件中有紧急数据可读
POLLOUT:可以向文件写入数据
POLLERR:文件中出现错误,只限于输出
POLLHUP:与文件的连接被断开,只限于输出
POLLNVAL:文件描述符是不合法的,即它并没有指向一个成功打开的文件
numfds:需要监听的文件个数,即第一个参数所指向的数组中的元素数目
timeout:表示poll阻塞的超时时间(毫秒)。如果该值小于等于0,则表示无限等待
函数返回值 成功:返回大于0的值,表示事件发生的pollfd结构的个数
0:超时; 1:出错

    3.使用实例

    当使用select()函数时,存在一系列的问题,例如,内核必须检查多余的文件描述符,每次调用select()之后必须重置被监听的文件描述符集,而且可监听的文件个数受限制(使用FD_SETSIZE宏来表示fd_set结构能够容纳的文件描述符的大数目)等。实际上,poll机制与select机制相比效率更高,使用范围更广。下面以poll()函数为例实现某种功能。

    本实例中主要实现通过调用poll()函数来监听三个终端的输入(分别重定向到两个管道文件的虚拟终端及主程序所运行的虚拟终端)并分别进行相应的处理。在这里我们建立了一个poll()函数监视的读文件描述符集,其中包含三个文件描述符,分别为标准输入文件描述符和两个管道文件描述符。通过监视主程序的虚拟终端标准输入来实现程序的控制(如程序结束);以两个管道作为数据输入,主程序将从两个管道读取的输入字符串写入到标准输出文件(屏幕)。

    为了充分表现poll()函数的功能,在运行主程序时,需要打开3个虚拟终端:首先用mknod命令创建两个管道in1和in2。接下来,在两个虚拟终端上分别运行cat>in1和cat>in2。同时在第三个虚拟终端上运行主程序。

    在程序运行后,如果在两个管道终端上输入字符串,则可以观察到同样的内容将在主程序的虚拟终端上逐行显示。

    如果想结束主程序,只要在主程序的虚拟终端下输入以“q”或“Q”字符开头的字符串即可。如果三个文件一直在无输入状态中,则主程序一直处于阻塞状态。为了防止无限期的阻塞,在程序中设置超时值(本实例中设置为60s),当无输入状态持续到超时值时,主程序主动结束运行并退出。该程序的流程图如图2.3所示。


图2.3 多路复用实例流程图

    /* multiplex_poll.c */
    #include <fcntl.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <time.h>
    #include <errno.h>
    #include <poll.h>
    #define MAX_BUFFER_SIZE 1024 /* 缓冲区大小 */
    #define IN_FILES 3 /* 多路复用输入文件数目 */
    #define TIME_DELAY 60000 /* 超时时间秒数:60s */
    #define MAX(a, b) ((a > b)?(a):(b))

    int main(void)
    {
        struct pollfd fds[IN_FILES];
        char buf[MAX_BUFFER_SIZE];
        int i, res, real_read, maxfd;

        /* 首先按一定的权限打开两个源文件 */
        fds[0].fd = 0;
        if((fds[1].fd = open ("in1", O_RDONLY|O_NONBLOCK)) < 0)
        {
            printf("Open in1 error\n");
            return 1;
        }
        if((fds[2].fd = open ("in2", O_RDONLY|O_NONBLOCK)) < 0)
        {
            printf("Open in2 error\n");
            return 1;
        }
        /* 取出两个文件描述符中的较大者 */
        for (i = 0; i < IN_FILES; i++)
        {
            fds[i].events = POLLIN;
        }

        /* 循环测试是否存在正在监听的文件描述符 */
        while(fds[0].events || fds[1].events || fds[2].events)
        {
            if (poll(fds, IN_FILES, 0) < 0)
            {
                printf("Poll error or Time out\n");
                return 1;
            }
            for (i = 0; i< IN_FILES; i++)
            {
                if (fds[i].revents) /* 判断在哪个文件上发生了事件 */
                {
                    memset(buf, 0, MAX_BUFFER_SIZE);
                    real_read = read(fds[i].fd, buf, MAX_BUFFER_SIZE);
                    if (real_read < 0)
                    {
                        if (errno != EAGAIN)
                        {
                            return 1; /* 系统错误,结束运行 */
                        }
                    }
                    else if (!real_read)
                    {
                        close(fds[i].fd);
                        fds[i].events = 0; /* 取消对该文件的监听 */
                    }
                    else
                    {
                        if (i == 0) /* 如果在标准输入上有数据输入时 */
                        {
                            if ((buf[0] == 'q') || (buf[0] == 'Q'))
                            {
                                return 1; /* 输入“q”或“Q”则会退出 */
                            }
                        }
                        else
                        { /* 将读取的数据先传送到终端上 */
                            buf[real_read] = '\0';
                            printf("%s", buf);
                        }
                    } /* end of if real_read*/
                } /* end of if revents */
            } /* end of for */
        } /*end of while */
        exit(0);
    }

    读者可以将以上程序交叉编译,并下载到开发板上运行,以下是运行结果:

    $ mknod in1 p
    $ mknod in2 p
    $ cat > in1            /* 在第一个虚拟终端 */
    SELECT CALL
    TEST PROGRAMME
    END
    $ cat > in2            /* 在第二个虚拟终端 */
    select call
    test programme
    end
    $ ./multiplex_select   /* 在第三个虚拟终端 */
    SELECT CALL            /* 管道1的输入数据 */
    select call            /* 管道2的输入数据 */
    TEST PROGRAMME         /* 管道1的输入数据 */
    test programme         /* 管道2的输入数据 */
    END                    /* 管道1的输入数据 */
    end                    /* 管道2的输入数据 */
    q                      /* 在第三个终端上输入“q”或“Q”则立刻结束程序运行 */

    程序的超时结束结果如下:

    $ ./multiplex_select
    …(在60s之内没有任何监听文件的输入)
    Poll error or Time out

    本文选自华清远见嵌入式培训教材《从实践中学嵌入式Linux应用程序开发》

   热点链接:

   1、linux 文件锁的实现及其应用
   2、底层文件I/O操作的系统调用
   3、Linux中的文件及文件描述符
   4、Linux文件系统之虚拟文件系统(VFS)
   5、Linux系统调用及用户编程接口(API)

更多新闻>>