Linux多路I/O基础


        非阻塞I/O是linux系统中非常重要的概念和技术,本文将从用户空间到内核空间,对非阻塞I/O进行梳理。



一、多路I/O之用户空间


        我们在开发应用程序时,经常会有这样的需要:同时对多个文件进行I/O操作。实现这种需要的常见技术就是多路I/O(multiplexed I/O)。多路I/O可以允许一个应用程序同时阻塞在多个文件描述符(File Descriptor)上。当其中任何一个文件描述符可以进行非阻塞I/O时,应用程序会收到一个唤醒通知,然后进行有效的I/O操作。


        多路I/O是应用程序的信息枢纽,使用多路I/O技术的应用程序主要工作流程可归纳为以下几步:


        (1)、应用程序将要查询的文件描述符传递给内核,当其中任何文件可以进行I/O操作时,内核将通知应用程序:已经就绪。


        (2)、如果所有文件都没有做好I/O的准备,那么应用程序对应的进程将会休眠。


        (3)、有文件做好I/O准备,进程被唤醒。


        (4)、对所有已经做好I/O准备的文件,执行无阻塞I/O。


        (5)、回到第(1)步。


        下图是使用多路I/O技术的应用的工作流程。


              

存储多路径 ALUA配置_存储多路径 ALUA配置


图1


        Linux系统为用户空间的应用程序提供了比较完善的多路I/O接口,其中主要的有两种:select和poll。两个接口调用都可以让应用程序实现多路I/O的作用,前者select源自于BSD,而poll源自于system V。


          1、select


        下面对select的主要用法进行说明。


         使用select函数需要包含glibc的头文件:#include <sys/select.h>。这个头文件是标准C库提供的,对于嵌入式系统而言,可以在交叉编译工具


链相关的目录中找到,它的实现则可以在glibc的源代码中看到。对于X86的PC机而言,一般可以在/usr/include目录下面找到这个头文件。


        select.h文件中定义了select库函数的原型、相关数据结构和一组有用的宏。


        select函数的原型如下:


                                            int select ( int n,


                                                              fd_set *readfds,


                                                              fd_set *writefds,


                                                              fd_set *exceptfds,


);


fd_set实际上是一个long int的整数,用其中的各个位来表示文件描述符。


        应用程序首先需要定义最多三个fd_set,用于保存要监听的文件描述符的集合。例如,下面的语句定义了一个readfds,可以用于保存所有要进行读I/O的文件描述符。


                                            fd_set readfds;


        配套的有几个宏定义。FD_SET, FD_CLR, FD_ISSET, FD_ZERO。


        FD_ZERO的作用是将定义的fd_set变量各个位全部清零。例如对上面定义的readfds,可以使用宏FD_ZERO进行清零。


                                            FD_ZERO(&readfds);


        如果要往readfds中添加一个要监听的文件描述符fd,可以将这个文件的句柄传递给宏FD_SET。


                                            FD_SET(fd, &readfds);


        如果要判断一个fd_set变量中是否已经包含指定的文件描述符fd。 


                                            FD_ISSET(fd, &readfds);




        应用程序需要将被监听的文件分成三组并定义相应的fd_set: readfds, writefds, exceptfds,并用上述的宏将文件描述符写入对应的fd_set变量。


最后将这些fd_set变量的指针传递给select函数。


        回过头来,参数里面的三个fd_set指针,分别对就到三类文件。


                                             fd_set  *readfds       ----   这个是对应将要发起读I/O操作的文件描述符


                                             fd_set  *writefds      ----  这个是对应将要发起写I/O操作的文件描述符


                                             fd_set  *exceptfds   ----  这个是对应发生异常的文件描述符


        这三个参数既是输入性质,又是输出性质,如果指定的文件描述符可进行相应的I/O操作,会在select返回时,透过fd_set指针将fd_set变量的相应bit置位,应用程序再使用宏FD_ISSET就可判断这个文件是否可进行I/O操作。


        再说第一个参数n,这个参数有点奇怪。在所有要监听的文件描述符中,选择一个最大的,然后加一,作为参数n传入给select函数。


        最后一个参数 timeout。当所有指定的文件描述符I/O都不可用时,进程会休眠,timeout用于指定出现这种情况时,休眠的最长时间。


        select函数的返回值是int类型的。 当返回值大于0时,返回值表示当前可进行I/O操作的文件描述符的个数。如果是因为超时而返回的,那么返回值可能是0。当返回值为-1时,表示出错。如果select出错,那么errno全局变量会被修改,可通过查看errno的值来确定错误的类型。


        select函数还有一个变种: pselect,功能与select相同,用法大同小异,在此不再赘述。




          2、poll


        select是使用三个基于位掩码的fd_set来处理文件描述符,当要监听的文件描述符数值很大时,select的效率会比较低。poll是system V引入的,与select相比,poll使用的是poll_fd类型的数组来组织要监听的文件描述符。


                  #include <poll.h>


                  int poll (struct poll_fd *fds, nfds_t nfds, int timeout);


        poll_fd是一个结构体,用于描述一个要监听的文件的信息。


                struct pollfd{


                        int fd;  //  这个是要监听的文件描述符


                        short events;  //这个是请求的I/O操作类型


                        short revents; //这个是返回的当前可进行的I/O操作类型


                }


        events和revents是通过对代表各种I/O操作的位掩码与行或运算而构成的。events包含要监听的事件,poll用当前可用的事件来填充revents。


        这些掩码及含义如下:


掩码

含义

POLLIN

有数据可读

POLLRDNORM

有普通数据可读

POLLRDBAND

优先级带数据可读

POLLPRI

高优先级数据可读

POLLOUT

可以非阻塞地写数据

POLLWRNORM

可以非阻塞地写普通数据

POLLWRBAND

可以非阻塞地写优先级带数据

POLLERR

发生错误

POLLHUP

发生挂起

POLLNVAL

描述字不是一个打开的文件


        poll函数的第二个参数nfds表示要监听的文件描述符的个数。


        如果指定的文件描述符中,有能以非阻塞方式进行I/O的,poll函数会在与之对应的revents中作标记。如果所有的文件描述符都不能以非阻塞的方式进行I/O,那么当前进程会休眠,这个休眠的时间,通过poll函数的第三个参数timeout指定。timeout的单位是毫秒。当timeout的值为0时,表示不休眠立即返回,当timeout的值为-1时,表示永远等待直到有某个文件描述符可进行I/O。当休眠timeout的时间过后,poll会再次检查是否有可以进行的非阻塞I/O,如果有,则填入revents中,如果仍然没有,则返回0。


        poll函数的返回值:


        当返回值大于0时,表示当前能以非阻塞方式进行I/O的文件描述符的个数。


        当返回值等于0时,表示返回的原因是超时,也就是说当前没有任何文件描述符能以非阻塞的方式进行I/O。


        当返回值小于0时,表示此次poll调用出错,错误原因会更新到errno全局变量中。


二、多路I/O之内核空间


        select和poll是标准的C库函数,它是把linux系统调用封装后提供给应用程序使用的。select和poll对应的系统调用在内核的fs/select.c文件中实现。


在select.c文件中,通过宏SYSCALL_DEFINE 定义了select和poll两个系统调用。这两个系统调用的执行流程大致相同,在这里,以poll为例进行说明。

存储多路径 ALUA配置_linux_02


图2


        上图是poll从系统调用的定义到驱动中的主要调用流程。应用程序调用poll函数后,会调用到内核空间的do_sys_poll函数,do_sys_poll会调用do_poll函数,在do_poll函数中,有一个大的死循环。每次循环,都会对所有传入的fd(用户空间需要监听的文件描述符)调用do_pollfd函数。


也就是扫描每个fd,对它调用do_pollfd。do_pollfd函数会调用给定文件的字符驱动的poll函数,我们知道,在驱动的poll函数中,主要工作有两个,


一个是调用poll_wait,把当前进程添加到等待对列中,另一个就是检查当前是否有可以立即进行的I/O,并将检查结果返回。所有驱动的poll函数本身是


不会睡眠的。


        当do_poll完成对所有fd的“扫描-轮询”后,会记录当前可以立即进行I/O的文件的个数count,如果count不为0,也就是至少存在一个文件可以马上进行I/O,那么do_poll将跳出死循环,直接返回,然后一路返回到应用程序。反之,将当前进程在wait上睡眠timeout指定的时间。那么进程退出睡眠就有两种情况,一种是超时(仍然没有文件可进行I/O),另一种是在睡眠的过程中,有I/O事件可用,进程被唤醒。两种情况下进程被唤醒后,都将进入do_poll的下一轮死循环。如果是超时,在下一轮循环中,将直接退出循环体;如果是因为有I/O事件可用,在下一轮循环中,调用do_pollfd就可以查询到可以进行I/O的fd,也会跳出列循环。



存储多路径 ALUA配置_存储多路径 ALUA配置_03


图3        


       综上所述:


        1、poll系统调用在内核中,会对所以指定的文件描述符进行扫描,查询是否有可用的I/O,如果有则返回相应的I/O事件和文件描述符个数;如果没有,则将当前进程睡眠,如果进程睡眠的过程中被到达的I/O事件唤醒,那么将可用的I/O事件和文件描述符个数返回。如果设置的超时到达时,仍然没有可用的文件可以马上进行I/O,则返回0。---- 这部分是linux的poll机制,不需要修改。


        2、对驱动编写者而言,如果要支持poll轮询机制,需要实现字符驱动的poll回调函数,在这个函数中主要工作有两个:调用poll_wait将当前进程添加到等待队列中;检查并返回当前可用的I/O情况。


        3、select系统调用的机制与poll类似,最终也会调用到文件驱动的poll函数。所以当前仅当文件驱动实现了poll回调函数时,才支持select和poll机制。