如何兼顾 FILE_SKIP_COMPLETION_PORT_ON_SUCCESS、IOCP 和清理

How to juggle FILE_SKIP_COMPLETION_PORT_ON_SUCCESS, IOCP, and cleanup

如果 FILE_SKIP_COMPLETION_PORT_ON_SUCCESS 设置在绑定到 I/O 完成端口的文件句柄上,则 OVERLAPPED 结构需要在其 I/O 完成时释放同步地。 否则,它需要保持活动状态,直到工作人员处理来自 I/O 完成端口的通知。

这一切听起来不错,直到您意识到这只有在您自己管理文件句柄时才有效。
但是如果其他人给了你文件句柄,你怎么知道什么时候应该释放 OVERLAPPED 结构呢?有什么办法可以事后发现吗?
否则,这是否基本上意味着您无法在无法保证...的完成通知状态的任何文件句柄上正确执行重叠 I/O?

Is there any way to discover this after the fact?

是的,存在 - 需要使用 ZwQueryInformationFile with FileIoCompletionNotificationInformation FILE_IO_COMPLETION_NOTIFICATION_INFORMATION 定义在 wdm.h

so 需要查询的代码:

FILE_IO_COMPLETION_NOTIFICATION_INFORMATION ficni;
ZwQueryInformationFile(hFile, &iosb, &ficni, sizeof(ficni), FileIoCompletionNotificationInformation);

设置和查询的演示代码

HANDLE hFile;
IO_STATUS_BLOCK iosb;
STATIC_OBJECT_ATTRIBUTES(oa, "\systemroot\notepad.exe");
if (0 <= ZwOpenFile(&hFile, FILE_GENERIC_READ, &oa, &iosb, FILE_SHARE_VALID_FLAGS, 0))
{
    FILE_IO_COMPLETION_NOTIFICATION_INFORMATION ficni = { FILE_SKIP_COMPLETION_PORT_ON_SUCCESS };
    if (0 <= ZwSetInformationFile(hFile, &iosb, &ficni, sizeof(ficni), FileIoCompletionNotificationInformation))
    {
        ficni.Flags = 0x12345678;
        if (
            0 > ZwQueryInformationFile(hFile, &iosb, &ficni, sizeof(ficni), FileIoCompletionNotificationInformation)
            ||
            !(ficni.Flags & FILE_SKIP_COMPLETION_PORT_ON_SUCCESS)
            )
        {
            __debugbreak();
        }
    }

    ZwClose(hFile);
}

也让从wdm.h复制粘贴(不要说这是"undocumented")

//
// Don't queue an entry to an associated completion port if returning success
// synchronously.
//
#define FILE_SKIP_COMPLETION_PORT_ON_SUCCESS    0x1

//
// Don't set the file handle event on IO completion.
//
#define FILE_SKIP_SET_EVENT_ON_HANDLE           0x2

//
// Don't set user supplied event on successful fast-path IO completion.
//
#define FILE_SKIP_SET_USER_EVENT_ON_FAST_IO     0x4

typedef  struct _FILE_IO_COMPLETION_NOTIFICATION_INFORMATION {
    ULONG Flags;
} FILE_IO_COMPLETION_NOTIFICATION_INFORMATION, *PFILE_IO_COMPLETION_NOTIFICATION_INFORMATION;

我有疑问 - wdm.h 中声明的原因是什么?

我不确定你的场景是否合理。

您阐明的场景 - 在任意文件句柄上成功执行 I/O,甚至不知道它是否异步 - 具有挑战性,我认为非常不寻常,而且几乎可以肯定 API 旨在使用,但也许(如您所建议的)并非完全不可信。

(虽然我不认为你可以避免要求调用者和你的代码之间进行一些合作,因为在 IOCP 的情况下,调用者必须能够分辨 I/O 一个出队的数据包属于谁. 你可以通过让调用者分配 OVERLAPPED 结构来做到这一点,正如 RbMm 所建议的那样,但要求他们提供要使用的完成键可能更简单。)

如果您提供冗余事件句柄,例如,当 I/O 实际上是同步的或使用 IOCP 时,我不确定 Windows 的行为如何。但我想这在实践中不会成为问题,所以只要您不太担心未来的验证,您可能就没问题。


无论如何,处理您的问题所询问的特定问题并不难。基本上,您只需要防止结构被释放两次即可。

  • 在每次调用之前,分配一个唯一的完成键并将其添加到链表或其他合适的全局结构中。 (该结构必须能够进行原子查找和删除操作,或受临界区或类似的保护。)

  • 如果调用立即成功,即不报告 I/O 正在挂起,则将其视为从 IOCP 队列中接收到排队的数据包。通常,您可以使用由 IOCP 线程和 I/O 线程调用的通用函数,或者调用 PostQueuedCompletionStatus 以手动将数据包插入 IOCP 队列。

  • 当接收到数据包时(或调用立即成功时)首先对全局结构执行完成键的查找和删除。如果查找失败,您知道您已经收到了 I/O 的成功通知,并且不需要执行任何操作。

  • 如果查找和删除成功,适当处理I/O并释放OVERLAPPED结构。

毫无疑问,有多种方法可以优化相同的基本方法。

附录:如果调用者正在处理 IOCP 数据包,并为您提供要使用的完成密钥,您将无法在每个请求上使用唯一的完成密钥。在这种情况下,您可以改用指向 OVERLAPPED 结构的指针。

不使用指针的原因(在一般情况下)是您可能会收到一个包含来自一个 I/O 请求的完成密钥的数据包以及来自另一个请求的 OVERLAPPED 结构,因为 OVERLAPPED在处理重复通知之前,可能会同时释放和重新分配结构。在这种情况下这无关紧要,因为无论如何您的所有请求都将使用相同的完成密钥。

附录^2:如果您对句柄一无所知,您还需要为每个 OVERLAPPED 结构提供一个事件对象,并等待它们以防 I/O 完成通知以这种方式到达。现在对我来说已经太晚了,无法弄清楚这会造成什么后果,但这可能意味着在某些情况下,您会收到 三个 相同 I/O 的通知手术。您也许可以避免这种情况,但如果没有,这种方法仍然有效。