【问题标题】:Coding pattern for dependant overlapped io in windowswindows中依赖重叠io的编码模式
【发布时间】:2014-02-10 07:23:18
【问题描述】:

我是一名 linux 程序员,最近参与了将基于 epoll 的客户端移植到 Windows 中,该客户端带有两个用 c 编写的文件描述符。
如您所知,在 linux 中使用 epoll 或 select(我知道 windows 支持 select,但它根本没有效率)您可以阻塞文件描述符,直到文件描述符 准备就绪,您可以知道它何时准备好写入和何时读取。

我查看了 Windows IOCP,在微软世界中重叠 io 听起来不错。 但在所有示例中,它都用于多客户端服务器,每个客户端的套接字都独立于其他套接字。

使用完成端口,可以为每个客户端创建一个completionKey结构,并在结构中放入一个变量,并在调用时读取 WSArecv 和 wirt 当 WSAsend 和其他变量指示套接字值并从 GetQueuedCompletionStatus 检索它们以知道要做什么时,如果对套接字进行了写入,则进行读取,反之亦然。

但就我而言,文件描述符 (fd) 确实是重叠的。从一个 fd 读取,对另一个 fd 进行读写,这使得很难知道什么 GetQueuedCompletionStatus 结果中的每个 fd 确实发生了操作,因为每个 fd 都关联一个 completionKey。为了清楚起见,请考虑:

有两个句柄,分别称为 fd1 和 fd2,completionKey1 持有 f1 的句柄和状态,fd2 的 completionKey2 和 completionKey 变量用于 从 GetQueuedCompletionStatus 检索完成。

    GetQueuedCompletionStatus(port_handle, &completionKey.bufflen, (PULONG_PTR)&completionKey,(LPOVERLAPPED *)&ovl,INFINITE);

   switch (completionKey.status)
    {
        case READ:
            if(completionKey->handle == fd1)
            {
                fd1_read_is_done(completionKey.buffer,completionKey.bufflen);
                completionKey->status = WRITE;
                do_fd1_write(completionKey);
                completionKey2->status = WRITE;
                completionKey2->buffer = "somedata";
                do_fd2_write(completionKey2);
            }
            else if(completionKey->handle == fd2)
            {
                fd2_read_is_done(completionKey.buffer,completionKey.bufflen);
                completionKey->status = WRITE;
                do_fd2_write(completionKey);
                completionKey1->status = WRITE;
                completionKey1->buffer = "somedata";
                do_fd1_write(completionKey1);
            }
            break;
        case WRITE_EVENT:
            if(completionKey->handle == fd1)
            {
                fd1_write_is_done(completionKey.bufflen);
                completionKey->status = READ;
                do_fd1_read(completionKey);
                completionKey2->status = READ;
                do_fd2_read(completionKey2);
            }
            else if(completionKey->handle == fd2)
            {
                fd2_write_is_done(completionKey.bufflen);
                completionKey->status = READ;
                do_fd2_read(completionKey);
                completionKey1->status = READ;
                do_fd1_read(completionKey1);
            }
            break;
    }

在上面的代码中,会出现一些改变completionKeys 将覆盖挂起的读取或写入以及结果的情况 completionKey->status 将是错误的(例如,它将报告读取而不是写入),最糟糕的是缓冲区将被覆盖。如果我使用锁定 completionKeys,会导致死锁的情况。

查看 WSAsend 或 WSArecv 后,注意到可以为每个发送或接收设置重叠参数。 但这会导致两个主要问题。根据 WSAOVERLAPPED 结构:

    typedef struct _WSAOVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    };
    PVOID  Pointer;
  };
  HANDLE    hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

首先,其中没有放置状态和适当缓冲区的位置,并且大部分都是保留的。

第二个如果可以解决第一个问题,我需要检查是否没有可用的重叠剩余并且它们都用于待处理的操作, 为每次读写分配一个新的,因为客户端会很忙,它可能会发生很多,此外,管理那些重叠的池是一件令人头疼的事情。 所以是我遗漏了什么还是微软搞砸了这个?

因为我不需要多线程,还有其他方法可以解决我的问题吗?
提前致谢
编辑
正如我所猜测的,我在使用重叠结构时提到的第一个问题已经有了答案,我只需要创建另一个包含所有缓冲区和状态等的结构,并将 OVERLAPPED 作为第一个文件。 现在你解决我的其他问题;)

【问题讨论】:

  • 出于好奇,Linux端对应的问题是怎么处理的?也就是说,当您在套接字 A 上读取请求并且(因此)想要写入套接字 B,但套接字 B 还没有准备好写入时?
  • 实际上我有一个队列,我只是写到队列中,然后尝试出列并在任何可能的时候写入。
  • 好的,那么每个套接字只需要两个 OVERLAPPED 结构,然后呢?一个用于单个挂起的读取操作,一个用于单个挂起的写操作?
  • 我试图在你的回答下解释。在我看来,仅靠排队是无法完成的。排队正好可以写。对于阅读,我需要盲目地从开关块中读出,并且需要 GetQueuedCompletionStatus 不是 INFINITE。而且看起来有点乱
  • 考虑到所有两个重叠的结构都可能在套接字上挂起

标签: c windows iocp overlapped-io


【解决方案1】:

你真的在这里问了两个不同的问题。我无法回答第一个问题,因为我从未使用过 IO 完成端口,但从我所阅读的所有内容来看,除了专家之外,每个人都最好避免使用它们。 (对于我认为您所描述的问题,我将指出一个明显的解决方案:与其在另一个写入仍在等待时实际将数据写入另一个套接字,不如将数据放入队列中并写入稍后再处理。您仍然需要在给定的套接字上同时处理两个操作 - 一个读取和一个写入 - 但这应该不是问题。)

但是,使用OVERLAPPED(或WSAOVERLAPPED)结构来跟踪重叠请求的状态很容易。您所要做的就是将OVERLAPPED 结构作为第一个元素嵌入到更大的结构中:

typedef struct _MyOverlapped
{
  WSAOVERLAPPED overlapped;
  ... your data goes here ...
} MyOverlapped, lpMyOverlapped;

然后将发送到完成例程的LPWSAOVERLAPPED 转换为lpMyOverlapped 以访问您的上下文数据。

或者,如果您使用的是完成例程,则保证WSAOVERLAPPEDhEvent 成员不会被使用,因此您可以将其设置为指向您选择的结构的指针。

我不明白您为什么认为管理重叠结构池会成为问题。每个活动缓冲区只有一个重叠结构,因此每次分配缓冲区时,都要分配相应的重叠结构。

【讨论】:

  • 另外一个更主观的说明:在任何情况下,您都可能需要考虑使用队列。我不确定是否有任何保证套接字提供程序将能够处理同一个套接字上无限数量的未决异步请求。此外,如果连接断开或发生其他错误,如果您只收到 一个 错误而不是每个待处理写入的单独错误,它可能会使事情变得更容易。队列应该易于实现;当给定的写请求完成时,处理队列中的下一项。
  • 谢谢哈利。您对如何创建适当的重叠结构是正确的。在上面的伪代码中,当读取完成时,我将缓冲区写入队列。关键是在 iocp 中,您只需读取或写入,对于每次读取或写入,您都需要一个未在其他待处理 io 中使用的重叠。考虑对 fd1 的写入刚刚完成。然后我需要从 fd1 和 fd2 中读取。 fd1 不需要新的 ovrlap,我可以使用刚刚获得的那个。但对于 fd2,我需要创建新的。如果我每次都开始分配新的 ovrlap,并在完成时释放它,那将是昂贵的。
  • 如果我创建一个 ovrlap 数组并查看是否有一个空闲的,如果没有,则无法完成读取,并且它可以结束 GetQueuedCompletionStatus 永远阻塞的情况并且排队无济于事。 (因为句柄的行为是相互依赖的。如果没有数据来自一个,另一个不会做任何事情)一些工作会突然出现。首先将超时设置为 GetQueuedCompletionStatus 并在发生这种情况时尝试在该开关块之外再次进行读写。但听起来很乱。我想知道是否有一种干净的方法
  • 如果您在单个套接字上挂起多个读取操作,则无法判断它们中的哪些将接收任何新数据,因此这不太可能有意义。 (在 Linux 端有一个类似的声明;如果我的理解正确,那么您就有竞争条件。)如果您在给定的套接字上只有一个读取操作挂起,那么每个套接字只需要两个重叠的结构(一个用于读取,一个用于写入),因此不需要为重叠结构设置单独的池,只需将它们与该套接字的其余上下文数据放在一起。
  • 关于您目前正在处理的问题:您是否确保每个挂起的写操作都有一个单独的缓冲区分配给它,并且该缓冲区在操作完成之前保持有效?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-12
  • 1970-01-01
  • 2012-01-26
  • 1970-01-01
  • 2015-07-08
  • 1970-01-01
相关资源
最近更新 更多