EPOLLET 的用例是什么?
What is the use case for EPOLLET?
epoll
边缘触发模式是一个奇怪的野兽。它要求流程跟踪每个受监视 FD 的最后响应是什么。它要求流程无误地处理每个报告的事件(否则我们可能会认为 FD 没有报告任何内容,而实际上它被边缘触发行为静音了)。
边缘触发 epoll
有意义的用例是什么?
据我所知,EPOLLET
的主要用例是微线程。
回顾一下 - 用户 space 正在根据要处理的内容的可用性在微线程(我将其称为 "fibers" 因为它更短)之间进行上下文切换。这也叫"collaborative multi-tasking".
文件描述符的基本处理是通过包装相关的 IO 函数,如下所示:
ssize_t read(int fd, void *buffer, size_t length) {
// fd should already be in O_NONBLOCK mode
while(true) {
ssize_t result = ::read(fd, buffer, length); // The real read
if( result!=-1 || (errno!=EAGAIN && errno!=EWOULDBLOCK) )
return result;
start_monitoring(fd, READ);
wait_event();
}
}
start_monitoring
是一个确保 fd
被监视以获取读取可用性的函数。 wait_event
执行上下文切换,直到调度程序重新唤醒此纤程,因为 fd
现在已准备好读取数据。
使用 epoll
实现此功能的通常方法是在 start_monitoring
内的 fd
上调用 EPOLL_CTL_MOD
以添加对 EPOLLIN
的监听,然后再次调用epoll 已报告事件停止监听 EPOLLIN
.
这意味着具有可用数据的 read
将在 1 个系统调用内完成,但读取 returns EAGAIN
至少需要 4 次系统调用(最初的 read
,两次 EPOLL_CTL_MOD
,最后的 read
成功。
请注意,上述内容并未计算还必须发生的 epoll_wait
。我没有计算它,因为我慷慨地假设其他光纤也将被同一个系统调用唤醒,因此将其成本完全归因于我们的光纤是不公平的。总而言之,这个机制需要4+x个系统调用,其中x在0到1之间。
降低成本的一种方法是使用 EPOLLONESHOT
。这样做会自动从监控中删除 fd
,从而将我们的成本降低到 3+x。更好,但我们可以做得更好。
输入EPOLLET
。先前的 fd
状态可以是武装或未武装(即 - 下一个事件是否会触发 epoll
)。此外,fd 当前可能(在 read
的入口点)准备好数据,也可能不准备好数据。四个州。让我们把它们分散开来。
就绪(是否布防):第一次调用read
returns数据。 1个系统调用。这条路径不会改变武装状态,就绪状态取决于我们是否读取所有内容。
Not ready (whether armed or not): 第一次调用read
returns EAGAIN
,从而使fd。我们在 wait_event
中进入睡眠状态,而无需执行另一个系统调用。一旦我们醒来,我们就处于非武装模式(因为我们刚刚醒来)。因此,我们不需要调用 epoll_ctl
来禁用对 fd 的监听。我们称 read
其中 returns 数据。我们让函数准备好或不准备好,但不准备。
总费用:2+x。
我们将不得不面对每个 fd
一次虚假唤醒,因为 fd
开始武装。我们的代码必须处理 epoll
报告没有光纤正在侦听的 fd 的情况。在这种情况下,处理只是意味着忽略并继续前进。 FD 不会再被虚假报告。
epoll
边缘触发模式是一个奇怪的野兽。它要求流程跟踪每个受监视 FD 的最后响应是什么。它要求流程无误地处理每个报告的事件(否则我们可能会认为 FD 没有报告任何内容,而实际上它被边缘触发行为静音了)。
边缘触发 epoll
有意义的用例是什么?
据我所知,EPOLLET
的主要用例是微线程。
回顾一下 - 用户 space 正在根据要处理的内容的可用性在微线程(我将其称为 "fibers" 因为它更短)之间进行上下文切换。这也叫"collaborative multi-tasking".
文件描述符的基本处理是通过包装相关的 IO 函数,如下所示:
ssize_t read(int fd, void *buffer, size_t length) {
// fd should already be in O_NONBLOCK mode
while(true) {
ssize_t result = ::read(fd, buffer, length); // The real read
if( result!=-1 || (errno!=EAGAIN && errno!=EWOULDBLOCK) )
return result;
start_monitoring(fd, READ);
wait_event();
}
}
start_monitoring
是一个确保 fd
被监视以获取读取可用性的函数。 wait_event
执行上下文切换,直到调度程序重新唤醒此纤程,因为 fd
现在已准备好读取数据。
使用 epoll
实现此功能的通常方法是在 start_monitoring
内的 fd
上调用 EPOLL_CTL_MOD
以添加对 EPOLLIN
的监听,然后再次调用epoll 已报告事件停止监听 EPOLLIN
.
这意味着具有可用数据的 read
将在 1 个系统调用内完成,但读取 returns EAGAIN
至少需要 4 次系统调用(最初的 read
,两次 EPOLL_CTL_MOD
,最后的 read
成功。
请注意,上述内容并未计算还必须发生的 epoll_wait
。我没有计算它,因为我慷慨地假设其他光纤也将被同一个系统调用唤醒,因此将其成本完全归因于我们的光纤是不公平的。总而言之,这个机制需要4+x个系统调用,其中x在0到1之间。
降低成本的一种方法是使用 EPOLLONESHOT
。这样做会自动从监控中删除 fd
,从而将我们的成本降低到 3+x。更好,但我们可以做得更好。
输入EPOLLET
。先前的 fd
状态可以是武装或未武装(即 - 下一个事件是否会触发 epoll
)。此外,fd 当前可能(在 read
的入口点)准备好数据,也可能不准备好数据。四个州。让我们把它们分散开来。
就绪(是否布防):第一次调用read
returns数据。 1个系统调用。这条路径不会改变武装状态,就绪状态取决于我们是否读取所有内容。
Not ready (whether armed or not): 第一次调用read
returns EAGAIN
,从而使fd。我们在 wait_event
中进入睡眠状态,而无需执行另一个系统调用。一旦我们醒来,我们就处于非武装模式(因为我们刚刚醒来)。因此,我们不需要调用 epoll_ctl
来禁用对 fd 的监听。我们称 read
其中 returns 数据。我们让函数准备好或不准备好,但不准备。
总费用:2+x。
我们将不得不面对每个 fd
一次虚假唤醒,因为 fd
开始武装。我们的代码必须处理 epoll
报告没有光纤正在侦听的 fd 的情况。在这种情况下,处理只是意味着忽略并继续前进。 FD 不会再被虚假报告。