Linux之poll

578 阅读6分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

前言

接触这几个概念是在整理BIO和NIO的时候,开始只知道epoll和select都是I/O多路复用的技术,都可以实现同时监听多个I/O事件的状态。只知道select是轮询模式效率较低,epoll是事件监听模式效率更高,底层基于linux,其实也是云里雾里,特此出此系列文章作为自己进一步学习和整理。(笔者技术栈有限,linux底层不会涉及太深只是对概念/机制基本的了解。)

上篇:Linux之select

确定学习目标

  • poll的本质是啥

poll

我们这里看下man手册对应章节,并结合大佬的文章

名称

poll, ppoll - wait for some event on a file descriptor

概要

       #include <poll.h>

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

       #define _GNU_SOURCE /* See feature_test_macros(7) */
       #include <poll.h>

       int ppoll(struct pollfd * fds , nfds_t nfds ,
                 const struct timespec * tmo_p , const sigset_t * sigmask ); 

描述

poll()执行与select(2)类似的任务:它等待fd set中的一个文件描述符准备就绪执行I/O。Linux 特定的 epoll(7) API 执行类似的任务,但提供的功能超出了 poll() 中的功能。

要监视的文件描述符集在 fds 参数中指定,该参数是以下形式的结构数组:

           struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

调用者应在nfds中指定fds数组中的项目数。

字段fd包含打开文件的文件描述符。如果此字段为负数,则忽略相应的events字段并且revents字段返回零。(这提供了一个简单的单个poll()调用忽略文件描述符方法:只需要把fd字段置为负数。但是请注意,此技术不能用于忽略为0的文件描述符。)

字段events是一个输入参数,一位掩码,指定应用程序对文件描述符fd感兴趣的事件。这个字段也许指定为零,在这种情况下,可以在revents 中返回的事件只有 POLLHUP、POLLERR 和 POLLNVAL(见下文)。

字段 revents 是一个输出参数,由内核填充实际发生的事件。revents 中返回的位可以包括任何在 events 中指定的位,或POLLERR、POLLHUP 或 POLLNVAL 之一的值 。 (这三个位在 events 字段中是没有意义的,只要相应的条件为真,就会在 revents 字段中设置。)

如果任何文件描述符都没有发生任何请求的事件(并且没有错误),则 poll() 会阻塞,直到其中一个事件发生。

timeout参数指定poll()应该阻塞等待文件描述符准备好的毫秒数。 调用将阻塞,直到:

  • 文件描述符准备就绪
  • 调用被信号处理程序中断
  • 超时到期

请注意,超时间隔将四舍五入到系统时钟粒度,内核调度延迟意味着阻塞间隔可能会超出少量。在 timeout 中指定负值意味着无限超时。 将超时指定为零会导致 poll() 立即返回,即使没有就绪好的文件描述符。

可以在events和revents中设置/返回的位在中定义:

POLLIN: 有数据要读取。

POLLPRI: 文件描述符中存在异常情况。可能性包括:

    • TCP socket上有带外数据(请参阅tcp(7))。
    • 包模式下的伪终端主机已经发现在从属设备上状态发生更改(请参阅 ioctl_tty(2))。
    • cgroup.events 文件已被修改(请参阅 cgroups(7))。

POLLOUT: 现在可以写了,尽管大于socket或pipe中可用空间的写入仍然会阻塞(除非设置了 O_NONBLOCK)。

POLLRDHUP(自 Linux 2.6.17 起): Stream socket peer 关闭连接,或者关闭写入一半的连接。必须定义 _GNU_SOURCE 功能测试宏(在包含任何头文件之前)才能获得此定义。

POLLERR: 错误条件(仅在revents中返回;在events中被忽略)。当读取端已关闭时,该位也为引用管道写入端的文件描述符设置。

POLLHUP: 挂断(仅在revents中返回;在events中被忽略)。请注意,当从channel、pipe或流socket读取时,此事件仅表示对等方关闭了其channel的末端。 只有在channel中所有未完成的数据都被消耗完后,从通道中读取的后续数据才会返回 0(文件结尾)。

POLLNVAL: 无效请求:fd 未打开(仅在revents中返回;在events中被忽略)。

在定义 _XOPEN_SOURCE 进行编译时,还具有以下内容,除了上面列出的位之外,它们不会传达更多信息:
POLLRDNORM: 相当于POLLIN。

POLLRDBAND: 优先读取带数据(在 Linux 上通常不使用)。

POLLWRNORM: 相当于POLLOUT。

POLLWRBAND: 写入优先级数据。

Linux 也知道但不使用POLLMSG。

ppoll()

poll()和ppoll()之间的关系类似于select(2)和pselect(2)之间的关系:像select(2)一样,ppoll()允许应用程序安全地等待,直到文件描述符准备好或直到捕获到信号。

除了 timeout 参数的精度不同之外,以下 ppoll() 调用:

ready = ppoll(&fds, nfds, tmo_p, &sigmask);

几乎等同于以原子方式执行以下调用:

           sigset_t origmask;
           int timeout;

           timeout = (tmo_p == NULL) ? -1 :
                     (tmo_p->tv_sec * 1000 + tmo_p->tv_nsec / 1000000);
           pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
           ready = poll(&fds, nfds, timeout);
           pthread_sigmask(SIG_SETMASK, &origmask, NULL);

上述代码段被描述为几乎等效,因为 poll() 的负超时值被解释为无限超时,而 *tmo_p 中表示的负值会导致 ppoll() 出错。

请参阅 pselect(2) 的描述,了解为啥需要ppoll()。

如果 sigmask 参数指定为 NULL,则不执行信号掩码操作(因此 ppoll() 与 poll() 的区别仅在于 timeout 参数的精度)。

mo_p 参数指定 ppoll() 将阻塞的时间量的上限。 此参数是指向以下形式的结构的指针:

           struct timespec {
               long    tv_sec;         /* seconds */
               long    tv_nsec;        /* nanoseconds */
           };

如果tmo_p指定为 NULL,则ppoll () 可以无限期地阻塞。

返回值

成功时,poll() 返回一个非负值,pollfds中revents字段已被设置为非0(表示事件或错误)的元素个数。如果在任何文件描述符准备好之前超时过期,则返回值可能为零。

出错时返回 -1,并设置 errno 以指示错误。

版本

poll() 系统调用是在 Linux 2.1.23 中引入的。 在缺少此系统调用的旧内核上,glibc poll() 包装函数使用 select(2) 提供模拟。

ppoll() 系统调用在内核 2.6.16 中添加到 Linux。 ppoll() 库调用是在 glibc 2.4 中添加的。

小结

  • 使用链表存储fd及其监听事件类型和返回发生的事件类型,所以没有select的FD_SETSIZE限制。
  • poll可以监控多个I/O事件的状态。
  • poll()会阻塞调用直到:FD准备就绪、调用被信号处理程序中断、超时到期。
  • poll()返回fds中revents字段被设置为非0的元素个数。如果在任何文件描述符准备好之前超时过期,则返回值可能为零。
  • ppoll()相当于poll()的升级:poll的超时单位为毫秒,负数为无限超时、ppoll为结构体(分为秒和纳秒),负数会导致错误,指定NULL可以无限等待;增加sigmask参数避免死锁。

poll本质

可以看出poll和select类似,虽然没有FD_SETSIZE限制但是仍然需要遍历FDs,效率还是很低。