| RFC-0009:边沿触发的 async_wait | |
|---|---|
| 状态 | 已接受 |
| 涉及的领域 |
|
| 说明 | 该提案指出,如果指定了 ZX_WAIT_ASYNC_EDGE 标志,则会省略初始检查,并将信号集添加到 DispatchObject 的兴趣列表中,而无论初始信号状态如何。在这种运行模式下,其中一个信号必须从不活跃状态转换为活跃状态,才能在提供的端口上将数据包排队(可能需要信号在此过程中变为不活跃状态)。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2020-10-24 |
| 审核日期(年-月-日) | 2020-11-06 |
摘要
等待对象上断言的信号通常是电平触发的,并且在 zx_object_wait_async 开始时会进行检查,以防信号已处于活跃状态,在这种情况下,数据包会立即发送到端口。此 RFC 涉及向 zx_object_wait_async 添加一个选项 ZX_WAIT_ASYNC_EDGE,该选项不会执行初始检查,因此仅当信号在调用后从不活跃状态转换为活跃状态时才会生成数据包。
可能会出现以下情况:在对象上的信号已处于活跃状态的情况下,使用 ZX_WAIT_ASYNC_EDGE 调用 zx_object_wait_async。在这种情况下,只有在对象上的信号变为不活跃状态,然后断言该信号后,数据包才会排队到端口。实际上,这就是 ZX_WAIT_ASYNC_EDGE 的常用方式。
设计初衷
Linux 中的 epoll 轮询机制可以在两种模式下运行:电平触发和边沿触发。Fuchsia 的等待功能(尤其是 zx_object_wait_async 和 zx_port_wait)已经可以实现电平触发的轮询。但是,边沿触发的轮询需要能够等待对象上已处于活跃状态的信号(通过 I/O)变为不活跃状态,然后再次变为活跃状态,并在后续信号转换时将数据包排队到端口。这就是 ZX_WAIT_ASYNC_EDGE 的目的。
设计
幸运的是,zx_object_wait_async 的 ZX_WAIT_ASYNC_EDGE 标志的实现非常简单。
目前,如果其中一个信号集已处于活跃状态,则会直接调用观察器的 OnMatch 方法,而无需执行任何其他操作。否则,如果没有任何信号集处于活跃状态,则会通过提供的 SignalObserver 将该集添加到 DispatchObject 的兴趣列表中。
该提案指出,如果指定了 ZX_WAIT_ASYNC_EDGE 标志,则会省略初始检查,并将信号集添加到 DispatchObject 的兴趣列表中,而无论初始信号状态如何。在这种运行模式下,其中一个信号必须从不活跃状态转换为活跃状态,才能在提供的端口上将数据包排队(可能需要信号在此过程中变为不活跃状态)。
在 epoll 中使用 ZX_WAIT_ASYNC_EDGE 和 EPOLLET 边沿触发
此更改的主要用途是在 epoll 中使用 EPOLLET 标志启用边沿触发的轮询。Zircon 中的等待与 epoll 中的轮询不同,因为文件描述符是
使用 epoll_ctl/EPOLL_CTL_ADD 添加到 epoll 文件描述符的,并且会持续
监控,直到使用 epoll_ctl/EPOLL_CTL_DEL 移除为止。Zircon 等待(尤其是使用 zx_object_wait_async)始终是一次性的,并且在信号变为活跃状态后,必须通过再次调用 zx_object_wait_async 来“重新武装”文件对象。
由于 epoll 使用必须通过重复调用 epoll_wait(不一定
调用 epoll_ctl)来运行,因此必须在 epoll_wait 中的某个位置进行对 zx_object_wait_async 的重新武装调用。
对于默认的电平触发轮询,在 epoll_wait 中,一旦 zx_port_wait 返回一个已发出信号的文件对象,我们就无法在返回之前调用 zx_object_wait_async,因为该对象上的信号处于活跃状态,并且会在端口上生成重复的数据包。因此,系统会维护一个处于活跃状态的电平触发文件描述符列表,并且在进入 epoll_wait 时,会在该列表中的文件描述符上调用 zx_object_wait_async,然后再在 zx_port_wait 中等待。
对于边沿触发的轮询,在 epoll_wait 返回后,应执行非阻塞 I/O,直到返回 EWOULDBLOCK 为止。此时,文件对象上的信号将处于不活跃状态。此时,应调用 epoll_wait。但是,如果文件对象上的信号在 I/O 操作返回 EWOULDBLOCK 和调用 epoll_wait 之间变为活跃状态,则除非已调用 zx_object_wait_async,否则该事件将丢失。
因此,在边沿触发模式下,必须在 epoll_wait 返回之前调用 zx_object_wait_async 来重新武装文件对象。 这就是 ZX_WAIT_ASYNC_EDGE 的必要之处。可以在 zx_port_wait 返回和 epoll_wait 返回之间使用此标志调用 zx_object_wait_async,因为尽管信号此时处于活跃状态,但 ZX_WAIT_ASYNC_EDGE 会跳过检查信号是否处于活跃状态(此时信号处于活跃状态),因此不会立即在端口上将数据包排队。
这意味着,当 I/O 发生直到 EWOULDBLOCK 时,zx_object_wait_async 已经监控文件对象,并且覆盖范围没有间隙。
实现
向 zx_object_wait_async 添加 ZX_WAIT_ASYNC_EDGE 选项已在 fxr/438521 中实现,并且在 epoll 中的使用已在 fxr/438656 中实现。
性能
性能影响可以忽略不计,因为仅向现有代码添加了一个额外的添加方法参数以及对该参数的检查。
安全注意事项
不适用
隐私注意事项
不适用
测试
已添加其他单元测试。
文档
已在实现 CL 中向 zx_object_wait_async 添加文档。
zx_object_wait_async
缺点、替代方案和未知事项
这似乎是实现此功能的最简单方法,类似于在其他操作系统中实现边沿触发轮询的方式。
使用此标志时,应注意不要错过 I/O 事件。
信号可能会在执行 I/O 后以及调用 zx_object_wait_async 之前变为活跃状态,在这种情况下,可能会错过从无信号到有信号的转换。实际上,ZX_WAIT_ASYNC_EDGE 标志会在 zx_port_wait 返回后立即使用,表明信号在对象上处于活跃状态。这样,在对对象执行非阻塞 I/O 直到信号处于不活跃状态(通常直到返回 ZX_ERR_SHOULD_WAIT)后,可以调用 zx_port_wait 以等待其他 I/O 准备就绪。
由于边沿触发的 epoll_wait 与 EPOLLET 的模式为 (1) epoll_wait (2) 非阻塞 I/O,直到 fd 处于未就绪状态 (3) 再次 epoll_wait,因此 ZX_WAIT_ASYNC_EDGE 的替代方案是在每个 I/O 操作中执行检查,以查看文件描述符是否已通过 epoll_ctl 与 EPOLLET 添加到 epoll 文件描述符(并且已停止就绪),并重新武装等待。对于相对罕见的用例,这需要对 zxio 和 fdio 进行大量修改。
在先技术和参考文档
此更改的目的是模拟 Linux 中的 EPOLLET 标志:
https://man7.org/linux/man-pages/man7/epoll.7.html