【UNIX网络编程】五种I/O模型,阻塞非阻塞同步异步问题详解

来源:互联网 发布:小程序 数据显示 编辑:程序博客网 时间:2024/06/05 22:44

IO复用

  在写简单的TCP/IP服务器-客户端程序时,客户端要同时处理两个输入:  

  • 标准输入
  • TCP套接字

      在结束的时候,因为客户端正阻塞于标准输入上的read函数,服务器TCP虽然正确的给客户TCP发送了一个FIN,但是既然客户进程正在阻塞于从标准输入读入的过程,他将看不到这个EOF,直到从套接字读时为止。
      这样的进程需要一种预先告之内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,即输入已经准备好被读取,或者描述符已经能承接更多的输出,他就通知进程。
      上面描述的能力即是IO复用。
      一般来说IO复用在一下场合应用:
      

  • 当客户处理多个描述符,包括交互式输入输出和网络套接字时,必须使用IO复用,否则会出现问题
  • 一个客户处理多个套接字时
  • TCP服务器既要处理坚挺套接字,又要处理已连接套接字的时候。
  • 一个服务器同时处理TCP和UDP时
  • 一个服务器要处理多个服务或者多个协议的时候

IO模型

  Unix下有五种IO模型
  

  • 阻塞式IO
  • 非阻塞式IO
  • I/O复用
  • 信号驱动式IO
  • 异步I/O

      下面我解释一下各个IO模型

阻塞式I/O模型

  最流行的I/O模型是阻塞式I/O模型,默认情形下,所有套接字都是阻塞的。
  我用钓鱼的例子来说明这个IO模型:
  一个人出去钓鱼,他将鱼竿放下后,就坐在旁边一直等,什么都不干,就等着鱼上钩,直到鱼上钩,再将鱼竿拿起,将鱼取下。
  这就是阻塞IO模型,当事件没有进行完毕的时候,进程就一直等待,什么事情都不做,直到事件完毕,进程再进行数据搬移,即将鱼取下。
  在这个过程中,进程是在自己等待,自己进行数据搬移的。
  下面我一数据报套接字作为例子。
  这里写图片描述
  在上图中,我们把recvfrom函数视为系统调用。
  进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才开始返回。最常见的错误是系统调用被信号中断。
  我们说进程在从调用recvfrom开始到他返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据报。

可能阻塞的套接字调用可分为以下四类

输入操作

  包括read,read,recvfrom,recvfrom和recvmsg共五个函数。
  如果某个进程对一个阻塞的TCP套接字调用这些输入函数之一,而且该套接字的接受缓冲区没有数据可读,该进程将被投入睡眠,知道有一些数据到达。

输出操作

  包括write,writev,send,sendto和sendmsg共五个函数。
  对于一个TCP套接字,内核将从应用进程的缓冲区到该套接字的发送缓冲区赋值数据。对于阻塞的套接字,如果其发送缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。
  对于一个非阻塞的TCP套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立刻返回一个EWOULDBLOCK错误。如果其发送缓冲区中根本没有空间,输出函数调用将立刻返回一个EWOULDBLOCK错误。如果该发送缓冲区中有一些空间,返回值将是内核能够复制到该缓冲区中的字节数,这个字节数也称为不足计数。
  UDP套接字不存在真正的发送缓冲区,内核只是复制应用哦该进程数据并把它沿协议栈向下传送,逐渐加上UDP首部和IP首部。因此对于一个阻塞的UDP套接字,输出函数调用将不会因与TCP套接字一样的原因而阻塞,不过可能会因为其他原因阻塞。

接收外来连接,即accept函数

  如果对一个阻塞的套接字调用accept函数,并且没有新的连接到达,调用进程将被投入睡眠。
  如果对一个非阻塞的套接字调用accept函数,并且没有新的连接到达,accept函数调用将会立刻返回一个EWOULDBLOCK错误。

发出外出链接,即用于TCP的connect函数

  TCP连接的建立涉及一个三路握手过程,而且connect函数一直要等到客户收到对于自己的SYN的ACK为止才返回。这意味着TCP的每个connect总会阻塞其调用进程至少一个到服务器的RTT时间。
  如果对一个非阻塞的TCP套接字调用connect,并且连接不能立即建立,那么连接的建立能照样发起,不过会返回一个EINPROGRESS错误。注意这个错误不同于上面的EWOULDBLOCK错误。以及有些连接可以立即建立,通常发生在服务器和客户处于同一个主机的情况下。因此即使对于一个非阻塞的connect,我们也得预备connect成功返回的情况发生。

非阻塞式I/O模型

  进程把一个套接字设置成非阻塞是在通知内核:

当所请求的IO操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。

  继续用上面钓鱼的例子举例:
  非阻塞IO模型就像是,一个人去钓鱼,将鱼竿放下后,他就去一边看书,看书的时候什么都不干就只看书,然后过一段时间就过去看看鱼竿,是否有鱼上钩,这就是非阻塞IO的轮询的方式。
  当发现没有鱼上钩的时候,他就继续返回去看书,或者干别的事,但是一旦发现有鱼上钩了,他就将鱼钓上来,这就是数据搬移。
  这非阻塞IO中,进程依旧是自己等,自己进行数据搬移,不同的是等的方式不同了。 
  非阻塞模型如下图所示:
  这里写图片描述
  前三次调用recvfrom时没有数据可返回,因此内核转而立即返回EWOULDBLOCK错误,第四次调用recvfrom时已经有一个数据报准备好,她被复制到应用进程缓冲区,于是recvfrom成功返回。
  当一个进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询。应用进程持续轮询内核,遗产看某个操作是否就绪,这么做往往耗费大量CPU时间。

I/O复用模型

  依旧是前面的钓鱼问题。
  IO复用就相当于:
  一个人来钓鱼,但是他拿了很多个鱼竿,将这些鱼竿同时放入水中,然后看哪个鱼竿有鱼,他就去处理哪个鱼竿。
  在IO复用模型中,进程依旧是自己进行等待,自己进行数据搬移的。
  有了IO复用,我们就可以调用select或poll函数,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的IO系统调用上。
  IO复用模型如下图所示:
  这里写图片描述
  在该模型中,我们阻塞于select调用,等待数据报套接字变为可读,当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。

与IO复用密切相关的另一种IO模型是在多线程中使用阻塞式IO,这种模型与上述模型极为相似,但他没有使用select阻塞在多个文件描述符上,而是使用多个线程——每个文件描述符一个线程,这样每个线程都可以自由的调用阻塞式IO系统调用了。

信号驱动式I/O模型

  依旧是钓鱼的例子:
  信号驱动式IO就相当于:
  一个人去钓鱼,他将鱼竿放下后,将一个铃铛放在鱼竿上,然后他就去做自己的事情,一旦听到铃铛响,他才会过来检查鱼竿并将鱼钓上来。如果他没有听到铃铛响,是绝对不会过来的。
  在这个模型中,依旧是进程自己进行等待,自己进程数据搬移。
  但是叫醒等待的方式不再是主动的了,而是被动的。
  模型如下:
  这里写图片描述
  在上图中:
  我们首先开启套接字的信号驱动式IO功能,并通过sigaction系统调用安装一个信号处理函数,该系统调用将立即返回,我们的进程继续工作,也就是说他没有被阻塞,当数据报准备好读取时,内核就为该进程产生一个SIGIO信号,我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让他读取数据报。
  无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据包已准备好被读取。

异步I/O模型

  其工作机制是:告知内核启动某个操作,并让内核在整个操作完成后通知我们,包括将数据从内核复制到我们自己的缓冲区。
  这种模型与前一届介绍的信号驱动模型的主要区别是:

  信号驱动IO是由内核通知我们何时可以启动一个IO操作,而异步IO模型是由内核通知我们IO操作何时完成。
  还以钓鱼的例子来讲吧:
  异步IO就是,一个人想要鱼,但是他自己不去,他让另一个人B去,他不管B是如何钓鱼的,他就仅仅只关心,什么时候钓鱼完毕,他只要这个鱼。

各种IO模型的比较

  我们在上面讲述了五种IO模型,可以看出前四种模型的主要区别在于第一阶段,即等待方式。他们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用,相反,异步IO模型在这两个阶段都要处理。

同步与异步的对比

  POSIX将两个术语定义如下:
  

  • 同步I/O操作导致请求进程阻塞,知道I/O操作完成
  • 异步I/O操作不导致请求进程阻塞
0 0
原创粉丝点击