「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
前言
接触这几个概念是在整理BIO和NIO的时候,开始只知道epoll和select都是I/O多路复用的技术,都可以实现同时监听多个I/O事件的状态。只知道select是轮询模式效率较低,epoll是事件监听模式效率更高,底层基于linux,其实也是云里雾里,特此出此系列文章作为自己进一步学习和整理。(笔者技术栈有限,linux底层不会涉及太深只是对概念/机制基本的了解。)
上篇:Linux之select
确定学习目标
- poll的本质是啥
poll
名称
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,效率还是很低。