如何在多线程程序中安全地删除与 epoll 一起使用的套接字文件描述符?

How to safely delete a socket file descriptor used with epoll in a multithreaded program?

我正在尝试解决 epoll 竞争条件问题,其中 epoll 事件循环是 运行,我希望它停止监视套接字文件描述符 (FD),但调用后我真的不知道epoll_ctl(..., EPOLL_CTL_DEL, ...) 如果 epoll 实例实际上删除了它,或者它正在处理与 FD 关联的事件,或者它只是从 epoll_wait().

唤醒线程

我用谷歌搜索了一下,找到了一个添加了 EPOLL_CTL_DISABLE 操作 (here) 的特殊内核补丁。可悲的是,它需要 EPOLLONESHOT,我认为这不是很有效,因为重新装备不是一个微不足道的操作(它需要在 epoll 的红黑树中进行搜索)。

因此我想知道我自己的解决方案。显然,我可以停止事件循环的线程,并最终在我处理完套接字文件描述符后重新创建它,并且我可以通过简单地执行 pthread_join() 来知道线程何时消失,但我们不要太天真了, pthread_join() 将使内核释放线程的资源,然后 pthread_create() 将再次分配相同的资源,这不像 EPOLLONESHOT 情况那样理想,并且肯定会带来一些性能损失。

现在我想知道别的事情。如果我能以某种方式在 epoll_wait() 将唤醒的套接字上手动分派一个事件,我可以在它唤醒后部署任何阻塞同步方法以确保它不会继续进行。这样,它将进入下一个 epoll_wait() 迭代,在那里它将意识到套接字已被删除并且不会发生竞争条件。为了说明,如下所示(真的可以使用任何东西代替障碍):

/* Obviously, never call the function from
within the event loop, or a deadlock will emerge */
void safe_socket_delete(int sfd) {
  atomic_store(&epoll->should_wait, 1);
  /* This is the function I'm looking for.
  I need to use it to be sure that the event
  loop will go through the should_wait if */
  dispatch_event(sfd, some_event);
  pthread_barrier_wait(&epoll->barrier);
  // we do whatever we want with the socket now
  pthread_barrier_wait(&epoll->barrier);
}

void event_loop() {
  while(1) {
    int events = epoll_wait(epoll->fd, ...);
    if(atomic_load(&epoll->should_wait) == 1) {
      pthread_barrier_wait(&epoll->barrier);
      pthread_barrier_wait(&epoll->barrier);
      continue;
    }
    // handle the events
  }
}

有这样的功能吗?如果没有,我该如何有效地处理问题?

也许我可以用不同的方式处理这个问题。不是在现有套接字上调度事件来唤醒 epoll_wait(),也许我可以使用 eventfd() 创建一个新的事件 FD,然后使其可读或可写,在需要时将其添加到 epoll,并且多田。事后删除它。我会多想一点,也许会尝试这个想法。当然,它带有 2 个树搜索,但在需要时进行 2 个搜索比在不需要时进行搜索更好(总是需要使用 EPOLLONESHOT 重新装备)。

巨大的优化将是始终拥有一个带有 EPOLLET 的此类事件文件描述符,以便我可以在其上模拟事件并让 epoll_wait() 唤醒。优点:没有树搜索,缺点:每个 epoll 实例多一个文件描述符。

调度事件有多种方式。既然你用的是Linux,为什么不用eventfdhttps://man7.org/linux/man-pages/man2/eventfd.2.html。创建 eventfd 并使用 epoll 注册它。当你写入它时,你将唤醒 epoll,你可以像处理任何其他你需要处理的文件描述符一样处理它。