6.3 poll和select


poll和select

使用非阻塞IO的应用程序可以调用 poll, select, 和 epoll系统函数。 poll, select, 和 epoll本质上具有相同的功能:都是允许一个进程是否能通过非阻塞的方式读写一个或者多个文件。当给定的文件描述符集中没有文件可以读取,就会阻塞进程。因此,往往用于具有多个输入输出流的应用程序中,避免程序被某个输入输出流阻塞住。为什么相同的功能要实现多个函数呢?因为它们分别由不同的组实现,selectBSD Unix引入的概念,而 poll是由System V引入的。为了增加文件描述符的个数,从Linux内核2.5.45开始,又引入了 epoll

当然了,这肯定需要驱动程序的支持。这三种调用都是通过驱动程序的 poll 方法提供的,原型如下:

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

当用户空间程序调用 poll, select, 或 epoll时,它们处理的文件描述符与驱动程序相关联,然后,驱动程序的 poll 方法就会执行。

  1. 对于一个或多个 wait 队列调用 poll_wait,就能指示出轮询的状态。如果没有可用于I/O的文件描述符,内核就会让进程继续等待传递给系统调用的所有文件描述符
  2. 返回一个能够立即非阻塞执行的文件描述符掩码

这些操作简单明了,各个驱动程序的这类操作看起来很相似。然而,它们非常依赖驱动程序提供的信息,因此每个驱动程序必须单独实现。

结构体 poll_tablepoll 方法的第2个参数,用来在内核中实现poll, select, 和 epoll系统调用,其声明在 <linux/poll.h>文件中。 所以,在驱动程序的源代码中一定要包含这个文件。 驱动程序的作者不需要关心其内部细节, 把其当做一个 黑盒子使用即可。 它被传递给驱动程序的方法, 以至于驱动可以把等待队列载入,然后唤醒进程并改变 poll 的状态。 驱动程序通过调用函数 poll_wait添加一个等待队列到结构体 poll_table中。

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

poll执行的第2个任务就是返回描述那些操作可以立即完成的位掩码,这也很简单。 例如, 如果设备有数据可用, read就会立即无阻塞的执行; poll就应该指出这种情况。 下面的标志用来指明这些可能的操作,其定义位于文件 <linux/poll.h>中。

POLLIN

  要想设备能够非阻塞的可读,必须设置。

POLLRDNORM

  “正常”数据可读取,必须设置。 可读设备应该返回 (POLLIN | POLLRDNORM)。

POLLRDBAND

  通常只用于linux内核某一处(DECnet码)且通常不会用于设备驱动程序。

POLLPRI

  无阻塞读取高优先级的数据(out-of-band)。select方法会报异常(带外数据)。

POLLHUP

  看到文件结束时,必须设置此位。

POLLERR

  设备发生错误,设置此位。调用 `poll`,会返回设备可读写,因为read和write都会返回错误代码而不会阻塞。

POLLOUT

  设备可以非阻塞地写入时,设置此位。

POLLWRNORM

   与POLLOUT相同,甚至有时候都是相同的数字。可写设备应该返回(POLLOUT | POLLWRNORM)。

POLLWRBAND

  像POLLRDBAND,表示可以将非0优先级的数据写入设备。只有poll数据报实现中使用。

值得注意的是 POLLRDBANDPOLLWRBAND只对套接字文件描述符有意义, 正常情况下,设备驱动程序不使用这些标志。

poll描述了这么多, 但是使用很简单。 下面是 scullpipepoll方法实现:

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

    /*
     * scull_pipe中的buffer是循环的; 如果 "wp" 紧跟在 "rp" 后边, 认为满; 如果两者相等, 则认为空
     */
    down(&dev->sem);
    poll_wait(filp, &dev->inq, wait);
    poll_wait(filp, &dev->outq, wait);
    if (dev->rp != dev->wp)
        mask |= POLLIN | POLLRDNORM;            /* 可读 */
    if (space_free(dev))
        mask |= POLLOUT | POLLWRNORM;           /* 可写 */
    up(&dev->sem);
    return mask;
}

该段代码完成2部分工作, 添加 scullpipe 的两个等待队列到 poll_table 中, 然后设置数据是否可读写的掩码并返回。

如上所示的代码中, 缺少文件结束符的支持。 因为 scullpipe 设备不支持文件结束。 对于许多真实的设备, 如果没有数据可用 poll 应该返回 POLLHUP 。 如果调用者使用 select 系统调用, 则该文件被报告为可读。 无论使用 poll 还是 selectread 方法都会返回 0, 表明文件结束, 应用程序从而知道可以非阻塞的读取文件。

例如, 对于真正的FIFO, 当所有写入关闭文件时, 读取调用就会看到文件结尾, 而在 scullpipe中, 读取调用永远不会看到文件结束。 它们行为是不同的, FIFO旨在成为两个进程之间的通信通道, 而 scullpipe是一个垃圾桶, 每个人都可以放置数据。 而且, 重新实现内核中已有的内容是没有意义的。 所以, 在示例中, 实现不同的行为。

readpoll 中, 以与FIFO相同的方式, 实现对文件结束的支持, 就意味着检查 dev-> nwriters。 不幸的是, 通过这种实现, 如果读取在写入者之前打开了 scullpipe设备, 它将看到文件结束而没有机会等待数据。 解决这个问题的最好方法是在开放时实现阻塞, 就像真正的FIFO一样; 这个任务留给读者练习。


6.3.1 与read和write的交互

调用 pollselect 的目的是提前确定I/O操作是否会阻塞。 在这方面, 它们是 readwrite 的补充。 pollselect 更重要的作用是, 让应用程序同时等待几个数据流, 尽管在 scull 示例中没有利用这个特点。

正确实现这3个调用对于应用程序正常工作至关重要: 尽管以下规则已经或多或少地陈述过, 但在这儿, 我们还是要总结一下。

从设备中读取数据

  • 如果输入缓冲区中有数据, 即使就绪的数据比应用程序请求的少, 且驱动程序保证剩余的数据很快到达, read 调用还是能以难以察觉的延迟返回。 如果这样做很方便 ( 就像 scull 中实现的那样), 返回至少一个字节, 那么, 总是可以返回比请求少的数据。 在这种情况下, poll 应该返回 POLLIN | POLLRDNORM
  • 如果输入缓冲区中没有数据, 默认情况下, read 必须阻塞, 直到至少有一个字节。 另一方面, 如果设置了 O_NONBLOCKread会立即返回 -EAGAIN (尽管某些旧版本的System V在这种情况下返回0)。 在这些情况下, poll必须报告设备不可读, 直到至少一个字节到达。 一旦缓冲区中有一些数据,我们就会回到前一种情况。
  • 如果我们处于文件结尾, 则应立即返回0, 与 O_NONBLOCK 无关。 在这种情况下 poll 应该报告 POLLHUP

向设备中写入数据

  • 如果输出缓冲区有空间, write应立即返回。 可以接受比请求少的数据, 但至少有一个字节。 在这种情况下, poll应通过返回 POLLOUT|POLLWRNORM 报告设备可写。
  • 如果输入缓冲区满, 默认情况下 write立即阻塞, 直到有空间可写。 如果设置了 O_NONBLOCKwrite立即返回-EAGAIN (旧System V Unices返回0)。 在这种情况下, poll报告文件不可写。 另一方面, 如果设备不能接受更多数据, write返回 -ENOSPC(“No space left on device”), 无论是否设置 O_NONBLOCK
  • 即使 O_NONBLOCK被清除, 在 write调用返回前, 绝不可让其等待数据传输完成。 这是因为会有许多应用使用 select去查看是否有 write被阻塞。 如果设备报告可写, 调用就不会阻塞。 如果使用该设备的程序要保证数据缓冲区的数据被传输, 驱动程序就必须提供 fsync方法。 例如可移除设备就必须有 fsync入口点。

虽然这是一组很好的规则, 但是必须认识到, 每个设备都是独一无二的, 有时候, 就需要变通一下执行。 例如, 面向记录的设备 ( 例如磁带机)就必须按记录写入,而不能部分写数据。

刷新挂起的输出

我们已经看到 write 不能满足所有的数据输出要求。 这时候就需要 fsync 函数, 其原型是:

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

如果要确保应用程序的数据被发送到设备上, 无论是否设置 O_NONBLOCK, 都必须实现 fsync方法。 只有当设备完全被刷新后, fsync方法才能够返回。 参数 datasync 只是用来区分 fsyncfdatasync系统调用; 这个参数只对文件系统有用, 设备驱动不需要关心。

fsync 方法没有特殊的功能。 该调用对时间也不敏感, 因此每个设备驱动程序都可以根据作者的喜好来实现。 大多数时候, 字符驱动程序的 fops 中设置为 NULL指针。 而块设备总是使用通用的 block_fsync来实现该方法, 刷新设备的所有块, 等待I/O操作完成。


6.3.2 底层数据结构

The actual implementation of the poll and select system calls is reasonably simple, for those who are interested in how it works; epoll is a bit more complex but is built on the same mechanism. Whenever a user application calls poll, select, or epoll_ctl,* the kernel invokes the poll method of all files referenced by the system call, passing the same poll_table to each of them. The poll_table structure is just a wrapper around a function that builds the actual data structure. That structure, for poll and select, is a linked list of memory pages containing poll_table_entry structures. Each poll_table_entry holds the struct file and wait_queue_head_t pointers passed to poll_wait, along with an associated wait queue entry. The call to poll_wait sometimes also adds the process to the given wait queue. The whole structure must be maintained by the kernel so that the process can be removed from all of those queues before poll or select returns.

If none of the drivers being polled indicates that I/O can occur without blocking, the poll call simply sleeps until one of the (perhaps many) wait queues it is on wakes it up. What’s interesting in the implementation of poll is that the driver’s poll method may be called with a NULL pointer as a poll_table argument. This situation can come about for a couple of reasons. If the application calling poll has provided a timeout value of 0 (indicating that no wait should be done), there is no reason to accumulate wait queues, and the system simply does not do it. The poll_table pointer is also set to NULL immediately after any driver being polled indicates that I/O is possible. Since the kernel knows at that point that no wait will occur, it does not build up a list of wait queues.

When the poll call completes, the poll_table structure is deallocated, and all wait queue entries previously added to the poll table (if any) are removed from the table and their wait queues.

We tried to show the data structures involved in polling in Figure 6-1; the figure is a simplified representation of the real data structures, because it ignores the multipage nature of a poll table and disregards the file pointer that is part of each poll_table_entry. The reader interested in the actual implementation is urged to look in

猜你喜欢

转载自blog.csdn.net/shenwanjiang111/article/details/81219819
6.3