我们可以在 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 的性质:你通常有一个线程从内核“下载”一个事件向量,包含具有一些 I/O 准备的文件描述符,以及一个与之关联的用户指针文件描述符。
现在,让我们考虑一下我有 2 个线程:T1 下载这些事件并处理它们,例如调用stream->on_read_fn();
等,T2 简单地运行用户代码、用户事件和类似的东西。
如果 T2 想要关闭一个文件描述符,只需执行此操作close(stream->fd);
,T1 就不会再收到该 fd 的任何 I/O 警报,因此在stream
那里释放结构是安全的。
但是,如果 T1 线程已经在它正在处理的事件向量中下载了相同的文件描述符,但它还没有处理那个文件描述符呢?
如果 T1 被安排在 T2 之前,它会没问题,但如果 T2 被安排在 T1 之前,它将关闭文件描述符并释放stream
结构,因此线程 T1 在处理该文件描述符时将有一个用户关联指针,指向一个已经释放的结构!当然,这会严重崩溃。
我的观点是,如果线程 T1 为该特定文件描述符下载了一些 I/O 警报,T2永远不会知道,T2 也无法预测 T1 是否会下载一些 I/O 警报或根本不下载!
这非常棘手,它让我头晕目眩。有什么想法吗?在这种情况下,何时可以安全地释放用户指定的指针?
注意:我的一个朋友建议在调用之前从 epoll/kqueue 队列中删除文件描述符close(2)
。没错,这就是我现在所做的,但这并不能解决问题,因为 T2 可以从 epoll/kqueue 队列中删除文件描述符,但这并不能保证该文件的 I/O 事件描述符尚未从内核“下载”,将很快由线程 T1 处理。