如何在单线程事件循环中同时等待epoll和io_uring的就绪事件
解决方案
核心思路
你不需要同时阻塞在两个系统调用上,只需要将其中一个事件源的通知挂载到另一个的监听队列,始终只阻塞在单个系统调用即可,两种成熟实现方案如下:
方案1:将io_uring事件挂载到epoll监听(对原有epoll逻辑改动最小)
实现步骤
- 调用
eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)创建非阻塞的事件通知fd - 调用
io_uring_register_eventfd(uring, event_fd)将该fd注册到io_uring,只要io_uring产生完成队列事件(CQE),内核就会向eventfd写入计数,触发可读事件 - 将eventfd添加到原有epoll的监听集合,监听
EPOLLIN事件
事件循环代码示例
while(keep_running) { // 仅阻塞在epoll_wait,任意事件源有事件都会立即唤醒 int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for (int i = 0; i < nfds; i++) { if (events[i].data.fd == event_fd) { // 清空eventfd计数,避免重复触发 uint64_t cnt; while (read(event_fd, &cnt, sizeof(cnt)) > 0); // 批量拉取所有io_uring完成事件处理 struct io_uring_cqe *cqes[MAX_CQES]; int cqe_cnt = io_uring_peek_batch_cqe(uring, cqes, MAX_CQES); for (int j = 0; j < cqe_cnt; j++) { // 处理io_uring任务逻辑 ... io_uring_cqe_seen(uring, cqes[j]); } } else { // 原有epoll事件(如数据库驱动socket事件)处理逻辑 ... } } }
优化建议
可以通过io_uring_register_eventfd_enabled接口动态开关eventfd通知,当没有io_uring任务提交时关闭通知,减少不必要的触发开销。
方案2:将epoll挂载到io_uring监听(适合后续向io_uring迁移的场景)
实现步骤
- 向io_uring提交一个多轮(multishot)poll请求,监听epoll fd的
EPOLLIN事件,设置专属标记位区分该事件是epoll触发 - 仅阻塞在io_uring的等待接口,任意事件源产生事件都会返回CQE
事件循环代码示例
// 初始化阶段提交epoll的multishot poll请求 #define EPOLL_EVENT_MAGIC 0xdeadbeef struct io_uring_sqe *sqe = io_uring_get_sqe(uring); io_uring_prep_poll_multishot(sqe, epoll_fd, EPOLLIN); io_uring_sqe_set_data64(sqe, EPOLL_EVENT_MAGIC); io_uring_submit(uring); while(keep_running) { struct io_uring_cqe *cqe; int ret = io_uring_wait_cqe(uring, &cqe); if (ret < 0) continue; if (io_uring_cqe_get_data64(cqe) == EPOLL_EVENT_MAGIC) { // epoll有事件触发,非阻塞拉取所有epoll事件处理 int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, 0); for (int i = 0; i < nfds; i++) { // 原有epoll事件处理逻辑 ... } io_uring_cqe_seen(uring, cqe); } else { // 处理io_uring自身任务完成逻辑 ... io_uring_cqe_seen(uring, cqe); } }
注意事项
- multishot poll需要内核版本5.15及以上支持,低版本内核可以改用普通poll请求,每次事件触发后重新提交即可
- 该方案对io_uring侧的事件处理延迟更低,适合io_uring任务占比更高的场景
内容的提问来源于stack exchange,提问作者Mary Chang
相关产品推荐
相关产品推荐

