unix网络编程第2版(卷1)_第6章_同步_异步

第6章 I/O复用:select和poll函数

6.1概述

在5.12节中,我们看到TCP客户同时处理两个输入:标准输入和TCP套接口。我们遇到的问题是客户阻塞于(标准输入上的)fgets调用,而服务器进程又被杀死。服务器TCP虽正确地给客户TCP发了一个FIN,但客户进程正阻塞于从标准输入读入,它直到从套接口读时才能看到此文件结束符(可能已经过了很长时间)。我们需要这样的能力:如果一个或多个I/O条件满足(例如,输入已准备好被读,或者描述字可以承接更多的输出)时,我们就被通知到。这个能力被称为I/O复用,是由函数select和poll支持的,我们也对较新的Posix.lg的变种(称为pselect)作介绍。
I/O复用典型地用在下列网络应用场合:

  • 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用,这在前一段中已做了描述。
  • 一个客户同时处理多个套接口是可能的,但很少出现。在15.5节一个Web客户的上下文中,我们给出使用select的例子。
  • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用,如6.8节所述。
  • 如果一个服务器既要处理TCP,又要处理UDP,一般也要使用I/O复用,8.15节中我们也要给出这样一个例子。
  • 如果一个服务器要处理多个服务或者多个协议(例如,我们将在12.5节描述的inetd守护进程),一般要使用I/O复用。

I/O复用并非只限于网络编程,许多正式应用程序也需要使用这项技术。

6.2 I/O模型

在介绍函数select和poll之前,我们需要回过头来看看

  • 阻塞 I/O
  • 非阻塞 I/O
  • I/O 复用(select和poll)
  • 信号驱动 I/O(SIGIO)
  • 异步 I/O(Posix.1 的 aio_系列函数)

你在第一次阅读时,可能想略读到本节,到后面的章节中详细介绍不同的I/O模型时才回来看。

正如本节我们所给出的所有例子所述,一个输入操作一般有两个不同的阶段:

  1. 等待数据准备好。
  2. 从内核到进程拷贝数据。

对于一个套接口上的输入操作,第一步一般是等待数据到达网络,当分组到达时,它被拷贝到内核中的某个缓冲区,第二步是将数据从内核缓冲区拷贝到应用缓冲区。

阻塞I/O模型

最流行的I/O模型是阻塞I/O模型,本书中到目前为止的所有例子都使用此模型。缺省时,所有套接口都是阻塞的。以数据报套接口作为例子,我们有示例图6.1中的情形。

此例中,我们用UDP而不是TCP,因为对于UDP来说,数据准备好的概念要简单些:整个数据报是否已接收,而对于TCP则要复杂得多,需考虑诸如套接口的低潮限度(lowwater mark)这样的许多附加变量。

在本节的例子中,我们将行数recvfrom视为系统调用,因为我们正考虑应用进程与内核的区别。不论函数recvfrom如何实现(在源自Berkeley的内核中作为系统调用,在系统V内核中作为调用系统调用getmsg的函数)。一般都有一个从一个应用进程中运行到内核中运行的切换,一段时间后再跟一个返回到应用的进程的切换。

图6.1 阻塞I/O模型

在图6.1中,进程调用recvfrom,此系统调用直到数据报到达且拷贝到应用缓冲区或是出错才返回。最常见的错误是系统调用被信号中断,如5.9节所述。我们所说进程阻塞的整段时间是指从调用recvfrom开始到它返回的这段时间,当进程返回成功指示时,应用进程开始处理数据报。

非阻塞I/O模型

当我们把一个套接口设置成非阻塞方式时,即通知内核:当请求的I/O操作非得让进程睡眠不能完成时,不要让进程睡眠,而应返回一个错误。我们将在第15章节详细介绍非阻塞I/O,但为了说明我们所考虑的例子,在图6.2中作一个小结性描述。

前三次调用recvfrom时仍无数据返回,因此内核立即被返回一个 EWOULDBLOCK 错误,第四次调用recvfrom时,数据报已准备好,被拷贝到应用缓冲区,recvfrom返回成功指示,接着就是我们处理数据。

当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,我们称此过程为轮询(polling)。应用进程连续不断地查询内核,看看某操作是否准备好,这对CPU时间是极大的浪费,但这种模式指示偶尔才遇到,一般只是专门提供某种功能的系统中才有。

图6.2 非阻塞I/O模型

I/O复用模型

有了I/O复用,我们就可以调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正的I/O系统调用。图6.3是I/O复用模型的一个小结。

我们阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,我们调用recvfrom将数据报拷贝到应用缓冲区中。

将图6.3与图6.1进行比较,似乎没有显示什么优越性,实际上,因使用了系统调用select,要求两次系统调用不是一次,好像变得还有点差。但是,在本章的后面我们将看到,使用select的好处在于我们可以等待多个描述字准备好。

图6.3 I/O复用模型

信号驱动I/O模型

我们也可以用信号,让内核在描述字准备好时用信号SIGIO通知我们,我们将此方法称为信号驱动I/O,图6.4对此作了一个小结。

图6.4 信号驱动I/O模型

首先,我们允许套接口进行信号驱动I/O(我们将在22.2节对此进行讨论),并通过系统调用sigaction安装一个信号处理程序。此系统调用立即返回,进程继续工作,它是非阻塞的。当数据报准备好被读时,就为该进程生成一个SIGIO信号。我们随即可以在信号处理程序中调用recvfrom来读数据报,并通知主循环数据已准备好被处理(这正是我们在22.3节中所要做的事情)。也可以通知主循环,让它来读取数据报。

无论我们如何处理SIGIO信号,这种模式的好处是当等待数据报到达时,可以不阻塞。主循环可以继续执行,只是等待信号处理程序的通知;或者数据已准备好被处理,或者数据报已准备好被读。

异步I/O模型

异步I/O是Posix.1的1993版本中的新内容("实时"扩展)。我们让内核启动操作,并在整个操作完成后(包括数据从内核拷贝到我们自己的缓冲区)通知我们。因为这种模型还没有广泛使用,本书不做讨论。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步是由内核通知我们I/O操作何时完成。图6.5给出一个例子。

图6.5 异步I/O模型

我们调用函数aio_read(Posix 异步I/O函数以 aio_ 或 lio_ 开头),给内核传递描述字,缓冲区指针、缓冲区大小(与read相同的三个参数)、文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。此系统调用立即返回,我们的进程不阻塞与等待I/O操作的完成。在此例子中,我们假设要求内核在操作完成时生成一个信号,此信号知道数据已拷贝到应用缓冲区才产生,这一点是与信号驱动I/O模型不同的。

正如本书说述,很少有系统支持Posix.1的异步I/O模型。例如,我们还不能确定系统是否支持套接口上的这种模型。这儿我们用它,只是作为一个与信号驱动I/O模型进行比较的例子。

各种I/O模型的比较

图6.6示出了上述五种不同I/O模型的比较。它表明:前四种模型的主要区别都在第一阶段,因为前四种魔性的第二阶段基本相同:在数据从内核拷贝到调用者的缓冲区时,进程阻塞于recvfrom调用。然后,异步I/O模型处理的两个阶段都不用于前四个模型。

图6.6 五个I/O模型的比较

同步I/O与异步I/O

Posix.1定义这两个术语如下:

  • 同步I/O操作引起请求进程阻塞,知道I/O操作完成。
  • 异步I/O操作不引起请求进程阻塞。

根据上述定义,我们的前四个模型————阻塞I/O模型、I/O复用模型和信号驱动I/O模型都是同步I/O模型,因为真正的I/O操作(recvfrom)阻塞进程,只有异步I/O模型与此异步I/O的定义相匹配。

原图

posted @ 2017-12-27 15:25  陈浩然201  阅读(381)  评论(0编辑  收藏  举报