NSPR 中的非阻塞 IO

简介

以前,NetScape Portable Runtime (NSPR) 中的所有 I/O 都是阻塞的(或同步的)。调用 io 函数的线程会被阻塞,直到 io 操作完成。阻塞 io 模型鼓励使用多线程作为编程模型。通常会创建一个线程来处理可能导致阻塞的多个并发 I/O 操作之一。

非阻塞 io 模型中,文件描述符可以标记为非阻塞。非阻塞文件描述符上的 io 函数要么立即成功,要么立即失败并返回 <tt>PR_WOULD_BLOCK_ERROR</tt>。单个线程足以同时处理多个非阻塞文件描述符。通常,此中心线程会对一组非阻塞文件描述符调用 <tt>PR_Poll()</tt>。(注意:<tt>PR_Poll()</tt> 也适用于阻塞文件描述符,尽管在阻塞 io 模型中不太有用。)当 <tt>PR_Poll()</tt> 报告文件描述符已准备好进行某些 io 操作时,中心线程会对该文件描述符调用该 io 函数。

创建非阻塞套接字

只有套接字可以设置为非阻塞。常规文件始终以阻塞模式运行。这不是一个严重的限制,因为可以假设磁盘 I/O 永远不会阻塞。从根本上说,这种限制是由于非阻塞 I/O 和 <tt>select()</tt> 仅在某些平台(例如 Winsock)上对套接字可用。

在 NSPR 中,由 <tt>PR_NewTCPSocket()</tt> 或 <tt>PR_NewUDPSocket()</tt> 返回的新套接字始终以阻塞模式创建。可以使用 <tt>PR_SetSockOpt()</tt> 将新套接字设为非阻塞,如下例所示(为简洁起见,省略了错误检查)


<tt>PRFileDesc *sock;</tt>
<tt>PRIntn optval = 1;</tt>

<tt>sock = PR_NewTCPSocket();</tt>

/*
* Make the socket nonblocking
*/

PR_SetSockOpt(sock, PR_SockOpt_Nonblocking, &optval, sizeof(optval));

编程限制

由于在 NSPR 中使用了 NT 异步 I/O,因此存在一些限制。在 NSPR 中,NT 上的阻塞套接字与 I/O 完成端口相关联。一旦与 I/O 完成端口相关联,我们就无法将套接字与 I/O 完成端口分离。我见过一些使用与 I/O 完成端口相关联的非阻塞套接字的奇怪问题。因此,第一个限制是

新套接字的阻塞/非阻塞 io 模式在第一次对该套接字调用潜在阻塞 io 函数时确定。一旦套接字的 io 模式确定,就不能更改。

潜在阻塞 io 函数包括 <tt>PR_Connect()</tt>、<tt>PR_Accept()</tt>、<tt>PR_AcceptRead()</tt>、<tt>PR_Read()</tt>、<tt>PR_Write()</tt>、<tt>PR_Writev()</tt>、<tt>PR_Recv()</tt>、<tt>PR_Send()</tt>、<tt>PR_RecvFrom()</tt>、<tt>PR_SendTo()</tt> 和 <tt>PR_TransmitFile()</tt>,不包括 <tt>PR_Bind()</tt> 和 <tt>PR_Listen()</tt>。

在阻塞模式下,任何这些潜在阻塞函数都需要使用 NT I/O 完成端口。因此,在这一点上,我们必须确定是否将套接字与 I/O 完成端口相关联,并且此决定以后无法更改。

由于使用了 NT 异步 I/O 和已用套接字的回收,因此存在第二个限制

由 <tt>PR_Accept()</tt> 或 <tt>PR_AcceptRead()</tt> 返回的新套接字继承侦听套接字的阻塞/非阻塞 io 模式,并且这不能更改。

在阻塞侦听套接字上由 <tt>PR_Accept()</tt> 或 <tt>PR_AcceptRead()</tt> 返回的套接字可能是以前在 <tt>PR_TransmitFile()</tt> 调用中使用的已回收套接字。由于 <tt>PR_TransmitFile()</tt> 仅以阻塞模式运行,因此此已回收套接字只能以阻塞模式重用,因此存在上述限制。

由于这些限制仅适用于 NT,因此建议您在开发周期的早期在 NT 上测试使用非阻塞 io 的跨平台代码。这些限制在调试 NSPR 库中由断言强制执行。

与阻塞 IO 的区别

  • 在非阻塞模式下,io 函数的超时参数会被忽略。

  • <tt>PR_AcceptRead()</tt> 和 <tt>PR_TransmitFile()</tt> 仅适用于阻塞套接字。它们在非阻塞模式下没有意义。

  • <tt>PR_Write()</tt>、<tt>PR_Send()</tt>、<tt>PR_Writev()</tt> 在阻塞模式下会阻塞,直到整个缓冲区发送完毕。在非阻塞模式下,它们不能阻塞,因此它们可能会在仅发送部分缓冲区时返回。

PR_Poll() 或 PR_Select()?

<tt>PR_Select()</tt> 已弃用,现在声明在 <tt>private/pprio.h</tt> 中。请改用 <tt>PR_Poll()</tt>。

<tt>PR_Select()</tt> 的当前实现只是调用 <tt>PR_Poll()</tt>,因此性能肯定会更差。此外,本机文件描述符(套接字句柄)不能添加到 <tt>PR_fd_set</tt> 中,即函数 <tt>PR_FD_NSET</tt>、<tt>PR_FD_NCLR</tt>、<tt>PR_FD_NISSET</tt> 不起作用。

PR_Available()

当 <tt>PR_Available()</tt> 返回 0 时,它可能表示两种情况之一

  • 该套接字上没有可供读取的数据。即 <tt>PR_Recv()</tt> 会阻塞(阻塞套接字)或失败并返回 <tt>PR_WOULD_BLOCK_ERROR</tt>(非阻塞套接字)。

  • 该套接字上的 TCP 连接已关闭(流结束)。

可以通过 <tt>PR_Poll()</tt> 区分这两种情况。如果 <tt>PR_Poll()</tt> 报告套接字可读(即 <tt>PR_POLL_READ</tt> 在 <tt>out_flags</tt> 中设置),并且 <tt>PR_Available()</tt> 返回 0,则表示套接字连接已关闭。

当前状态

已在所有受支持的平台上实现。