Epoll、kqueue、用户指定指针:如何在多线程环境中安全地释放它?

Epoll, kqueue, user specified pointer: how to safely deallocate it in a multithreaded envinronment?

我们可以在 Unices 系统中用于异步 I/O 警报的设施,例如 Linux 上的 epoll,BSD 系统上的 kqueue 和 Solaris /dev/poll 或 I/O 端口, 都让用户指定一个指针关联到用户想要接收 I/O 警报的文件描述符。

通常在这个指针中,用户指定指向一个结构的指针,该结构将抽象出一个文件描述符(例如 "Stream" 结构,或类似的东西),并且用户每次都会分配一个新结构一个新的文件描述符已打开。

例如struct stream { int fd; int flags; callback_t on_read_fn; /* ... */ };

现在,我的问题是:如何安全地释放用户在多线程环境中分配的这个结构?

我问这个,因为 epoll/kqueue/etc 的性质: 您通常有一个线程 "downloads" 来自内核的事件向量,包含具有一些 I/O 就绪状态的文件描述符,以及与该文件描述符关联的用户指针。

现在,让我们考虑我有 2 个线程:T1,它下载这些事件并处理它们,例如调用 stream->on_read_fn(); 等,T2 只运行用户代码、用户事件和类似的东西。

如果 T2 想要关闭一个文件描述符,只需执行 close(stream->fd); 并且 T1 将不会再收到任何关于该 fd 的 I/O 警报,因此释放 stream 结构是安全的那里。

但是,如果 T1 线程已经在它正在处理的事件向量中下载了完全相同的文件描述符,但它还没有处理该文件描述符呢?

如果T1在T2之前调度,就OK,但是如果T2在T1之前调度,它会关闭文件描述符并释放stream结构,所以线程T1,什么时候处理那个文件描述符,将有一个用户关联的指针,指向一个已经释放的结构!当然这样会很崩溃。

我的观点是,T2 永远不会 知道线程 T1 是否为该特定文件描述符下载了一些 I/O 警报,T2 都无法预测 T1 是否会下载一些 I/O 警报或根本没有!

这非常棘手,让我头晕目眩。有什么想法吗? 在这种情况下何时可以安全地释放用户指定的指针?

注意:我的一个朋友建议在调用 close(2) 之前从 epoll/kqueue 队列中删除文件描述符。没错,这就是我现在所做的,但这并不能解决问题,因为 T2 可以从 epoll/kqueue 队列中删除文件描述符,但这不能保证 I/O该文件描述符的事件尚未从内核 "downloaded" 中获取,将很快由线程 T1 处理。

我遇到了完全相似的问题,这就是为什么在新的 linux 内核提案中,有人(不记得名字了)建议为 FD 实现 DISABLED 状态,这样你就可以跳过处理,如果它有已被另一个线程释放。

就我个人而言,我从多线程 epool 调用转移到 FD 上的单线程 epool(),然后将事件调度到多线程。内部的对象本身是引用计数的,稍后由垃圾收集器收集。与多线程 epool 解决方案相比,诚实地工作得很好并且没有明显的退化...

* 已编辑 *

此外,我研究了另一种方法,通过创建一个 std::set 受互斥锁保护的线程来关闭 FD,而不是处理 epool,只要 FD 需要,由消费者线程填充被关闭。这也很好用。

我宁愿避免在 2 个线程之间共享相同的数据结构。

过去,使用 "one-shot" 技巧,它似乎可以在许多系统上移植。对于一次性行为,一旦事件发出信号,它就会暂时 "taken out" 队列中,即没有其他线程会收到任何 fd 变为可读或可写的通知。

完成事件处理后,您需要将其添加回 epoll/kqueue(如 Linux 文档所说,"re-arm" fd)。

  • 在 Linux 上:

    添加到 epoll : epoll_ctl()/EPOLL_CTL_ADD , 标记 EPOLLET|EPOLLONESHOT

    重新武装:epoll_ctl()/EPOLL_CTL_MOD 使用相同的事件标志。

  • 在 BSD/OSX 上使用 kqueue

    添加到 kqueue:EV_SET(...EV_ADD|EV_ONESHOT...);

    重新武装:EV_SET(...EV_ADD|EV_ONESHOT...);

  • 在 Solaris 上

    只需使用 port_associate() 即可添加和重新布防。

我在我的程序中解决了这个问题,方法是不释放结构,而是将其标记为 "dead" 并将其添加到列表中,以便以后可以重复使用。这样指针始终保持有效,尽管它可能已被重复使用。