高级字符驱动程序操作——poll和select

时间:2022-07-24 00:05:57

poll和select

使用非阻塞I/O的应用程序也经常使用poll、select和epoll系统调用。poll、select和epoll的功能本质上是一样的:都允许进程决定是否对一个或多个打开的文件做非阻塞的读取或写入(换句话说就是,确定是否可以在该文件描述符上做非阻塞的操作)。这些调用也会阻塞进程,直到给定的文件描述符集合中的任何一个可读取或可写入。因此,它们常常用于那些要使用多个输入或输出流而又不会阻塞于其中任何一个流的应用程序中。

同一功能之所以要由多个独立的函数提供,是因为其中两个几乎是同时由两个不同的unix团体分别实现的:select在BSD Unix中引入,而poll由System V引入。epoll系统调用在2.5.45中引入,它用于将poll函数扩展到能够处理数千个文件描述符。

对上述系统调用的支持需要来自设备驱动程序的相应支持。所有三个系统调用均通过驱动程序的poll方法提供。该方法的原型如下:

unsigned int (*poll) (struct file *filp, poll_table *wait);

当用户空间程序在驱动程序关联的文件描述符上执行poll、select和epoll系统调用时,该驱动程序方法将被调用。该设备方法分为两步处理:

  1. 在一个或多个可指示poll状态变化的等待队列上调用poll_wait。如果当前没有文件描述符可用来执行I/O,则内核将使进程在(传递到该系统调用的所有文件描述符)对应的等待队列上等待。
  2. 返回一个用来描述操作是否可以立即无阻塞执行的位掩码。

以上操作简单明了,各个驱动程序的这些操作看起来也都非常类似。然而实际上,它们依赖于只有驱动程序才能提供的信息,因此必须为每个驱动程序分别实现对应的操作。

传递给poll方法的第二个参数,poll_table结构,用于在内核中实现poll、select以及epoll系统调用。它在<linux/poll.h>中声明,通过pall_wait函数,驱动程序向poll_table结构中添加一个等待队列:

void poll_wait(struct file *, wait_queue_head_t *, poll_table *);

poll方法执行的第二项任务是返回描述哪个操作可以立即执行的位掩码,例如,如果设备已有数据就绪,一个read操作可以立即完成而不用休眠,那么poll方法应该指出这种情况。下面几个标志在poll.h中定义,用来指明可能的操作:

POLLIN——如果设备可以无阻塞地读取,就设备该位。

POLLRDNORM——如果“通常”的数据已经就绪,可以读取,就设置该位。一个可读设备返回(POLLIN|POLLRDNORM)。

POLLRDBAND——这一位指示可以从设备读取out-of-band(频带之外)的数据。它当前只可以在Linux内核的DECnet代码中使用,通常不用于设备驱动程序。

POLLPRI——可以无阻塞地读取高优先级(即out-of-band)的数据。设置该位会导致select报告文件发生一个异常,这是由于select把“out-of-band"的数据作为异常对待。

POLLHUP——当计读取设备的进程到达文件尾时,驱动程序必须设置POLLHUP(挂起)位。依照select的功能描述,调用select的进程会被告知设备是可读的。

POLLERR——设备发生了错误。如果调用poll,就会报告设备既可读也可以写,因为读写都会无阻塞地返回一个错误码。

POLLOUT——如果设备可以无阻塞地写入,就在返回值中设置该位。

POLLWRNORM——该和POLLOUT的意义一样,有时其实就是同一个数字。一个可写的设备将返回(POLLOUT|POLLWRNORM)。

POLLRWBAND——与POLLRDBAND类似类似,这一位表示具有非零优先级的数据可以被写入设备。只有数据报(datagram)的poll实现中使用了这一位,因为数据报可以传输out-of-band数据。

POLLRDBAND和POLLWRBAND只有在套接字相关的文件描述符中才是有意义的。设备驱动程序通常用不到这两具标志。

下而scullpipe的poll实现:

static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
    struct scull_pipe *dev = filp->private_data;
    unsigned int mask = 0;

    /* the buffer is circular; it is considered full
       if "wp" is right behind "rp" and empty if the 
       two are equal.
     */
    down(&dev->sem);
    poll_wait(filp, &dev->inq, wait);
    poll_wait(filp, &dev->outq, wait);
    if(dev->rp != dev->wp)
        mask |= POLLIN | POLLRDNORM; /*可读取*/
    if(spacefree(dev))
        mask |= POLLOUT | POLLWRNORM;/*可写入*/
    up(&dev->sem);
    return mask;
} 

这段代码简单地增加两个scullpipe等待队列到poll_table中,然后根据可读可写状态设置相应的位掩码。

上述poll代码中缺少对文件尾的支持,这是因为scullpipe不支持文件尾条件。

与read和write的交互

poll和select调用的目的是确定接下来的I/O操作是否会阻塞。从这个方面来说,它们是read和write的补充。poll和select的更重要的用途是它们可以使应用程序同时等待多外数据流,尽管在scull的例子中没有利用这个特点。

为了使应用程序正常工作,正确实现这三个调用是非常重要的。所以尽管下面的规则多多少少已提到过了,但是我们还是要在这里总结一下。

从设备读取数据

  • 如果输入缓冲区有数据,那么即使就绪的数据比程序所请求的少,并且驱动程序保证剩下的数据马上就能到达,read调用仍然应该以难以察觉的延迟立即返回。如果为了某种方便(比如我们的scull),read甚至可以一直返回比所请求数目少的数据,当然,前提是至少得返回一个字节。
  • 如果输入缓冲区中没有数据,那么默认情况下read必须阻塞等待,直到至少有一个字节到达。另一方面,如果设置了O_NONBLOCK标志,read应立即返回,返回值是-EAGAIN(有些System V的老版本返回0)。在这种情况下poll必须报告设备不可读,直到至少有一个字节到达。一旦缓冲区有了数据,我们就回到了前一种情况。
  • 如果已经到达文件尾,read应该立即返回0,无论O_NONBLOCK是否设置。此时poll应该报告POLLHUP。

向设备写数据

  • 如果缓冲区中有空间,则write应该无延迟立即返回。它可以接收比请求少的数据,但至少要接收一个字节。在这种情况下,poll报告设备可写。
  • 如果输出缓冲区已满,那么默认情况下write应该被阻塞直到有空间释放。如果设置了O_NONBLOCK标志,write应立即返回,返回值是-EAGAIN(老版本的System V系统返回0)。这此poll应该报告文件不可写。另一方面,如果设备不能再接收任何数据,则write返回-ENOSPC("No space left on device,设备无可用空间"),而不管O_NONBLOCK标志是否设置。
  • 永远不要让write调用在返回前等待数据的传输结束,即使O_NONBLOCK标志被清除。这是因为,许多应用程序用select来检查write是否会阻塞。如果报告设备可以写入,调用就不能被阻塞。如果使用设备的程序需要保证输出缓冲区中的数据确实已经被传送出去,驱动程序就必须提供一个fsync方法。例如,可移除设备应该有一个fsync的入口点。

尽管这些已经是一个很好的通用规则集合,但是还是应该主承认每个设备都有其独特之处,所有有时需要稍稍改变一下规则。例如,面向记录的设备(如磁带机)不能执行部分写入(必须以记录为单位)。

刷新待处理输出

我们已经看到为什么write方法不能满足所有数据输出的需求,fsync函数可以弥补这一空隙,它通过同名系统调用来调用。该方法的原型是:

int (*fsync) (struct file *file, struct dentry *dentry, int datasync);

如果应用程序需要确保数据已经被传送到设备上,就必须实现fsync方法。一个fsync调用只有在设备已被完全刷新(输出缓冲区全空)时才会返回,即使这要花一些时间。是否设备O_NONBLOCK标志对此没有影响。参数datasync用于区分fsync和fdatasync这两个系统调用。这里它只和文件系统的代码有关,驱动程序可以忽略它。

fsnyn方法没有什么特别之处。这个调用对时间没有严格要求,所以每个驱动程序都可以按照作者的喜好实现它。大多数时候,字符设备驱动程序在它们fops只有一个NULL指针,而块设备总是用通用的block_fsync来实现这个方法,block_fsync会依次刷新设备的所有缓冲区,并等待所有I/O结束。