我们看到TCP客户同时处理两个输入:标准输入和TCP套接字。我们遇到的问题是就在客户阻塞于(标准输入上)fgets调用,服务器进程会被杀死。服务器TCP虽然正确的给客户TCP发送了一个FIN,但是既然客户进程正阻塞于从标准输入读入的过程,它将看不到这个EOF,直到从套接字读时为止(可能额已过了很长时间)。这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(也就是说输入已准备好被读入,或者描述符已能承接更多的输出),它就通知进程。这个能力称为I/O复用,是由select和poll这两个函数支持的。


      I/O复用典型使用在下列网络应用场合:

  • 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用;
  • 如果一个TCP服务器即要处理监听套接字,又要处理已连接套接字,一般使用I/O复用;
  • 一个客户同时处理多个套接字时可能的,不过比较少见;
  • 如果一个服务器即要处理TCP,又要处理UDP,一般就要使用I/O复用。
  • 如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用。

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

 

UNIX下可用的5种I/O模型的基本区别:

1.阻塞式I/O;

2.非阻塞式I/O;

3.I/O复用(select和poll);

4.信号驱动式I/O(SIGIO);

5.异步I/O(POSIX的aio_系列函数)

 

一个输入操作通常包括两个不同的阶段:

(1)等待数据准备好;

(2)从内核向进城复制数据。

     对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。


1.阻塞式I/O模型

                UNIX网络编程——I/O复用:select和poll函数_系统调用

     我们在前面所说的I/O模型都是阻塞I/O,即调用recv系统调用,如果没有数据则阻塞等待,当数据到来则将数据从内核空间(套接口缓冲区)拷贝到用户空间(recv函数提供的buf),然后recv返回,进行数据处理。


2.非阻塞式I/O模型

       进程把一个套接字设置成非阻塞。前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。我们接着处理数据。

      当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询,应用进程持续轮询内核,以查看某个操作是否就绪,这么做往往耗费大量的CPU 时间。

         UNIX网络编程——I/O复用:select和poll函数_系统调用_02

     我们可以使用 fcntl(fd, F_SETFL, flag | O_NONBLOCK); 将套接字标志变成非阻塞,调用recv,如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里,事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询(Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:

while(1) 

非阻塞read(设备1); 

if(设备1有数据到达) 

处理数据; 

非阻塞read(设备2); 

if(设备2有数据到达) 

处理数据; 

..............................

}


如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处理。

非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了,在实际应用中非阻塞I/O模型比较少用。



3.I/O复用模型

    我们调用select或poll阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。

              UNIX网络编程——I/O复用:select和poll函数_系统调用_03

     我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。


4.信号驱动I/O模型

     我们可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们,我们称这种模型为信号驱动式I/O。


    UNIX网络编程——I/O复用:select和poll函数_系统调用_04

     我们首先开启套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为进程产生一个SIGIO信号。我们随后即可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。


5.异步I/O模型

       一般来说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与信号驱动式模型的主要区别是:信号驱动式I/O是由内核通知我们何时启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

               UNIX网络编程——I/O复用:select和poll函数_套接字_05

     我们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符,缓冲区指针,缓冲区大小(read相同的三个参数)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待I/O完成期间,我们的进程不被阻塞。


各种I/O模型的比较:

        前四种模型的主要区别在于第一阶段,因为他们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个都要处理,从而不同于其他4种模型。


同步I/O和异步I/O对比:

POSIX把这两个术语定义为:

同步I/O操作:导致请求进程阻塞,直到I/O操作完成;

异步I/O操作:不导致请求进程阻塞。

  UNIX网络编程——I/O复用:select和poll函数_复用_06

     根据上述定义,我们的前4种模型--------------阻塞式I/O模型,非阻塞式I/O模型,I/O复用模型和信号驱动式I/O模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。