对比五种I/O模型

一、关于 I/O模型的引出

我们都知道,为了操作系统安全性等的考虑,进程是无法直接操作I/O设备的,其必须通过系统调用请求内核来协助完成I/O动作,而内核会为每个I/O设备维护一个buffer。

在整个请求过程中,数据输入至内核buffer需要时间,而从内核buffer复制数据至用户进程也需要时间。因此根据在这两段时间内等待方式的不同,I/O动作可以分为以下五种模式:

  • 1)阻塞I/O(blocking I/O)

  • 2)非阻塞I/O (nonblocking I/O)

  • 3) I/O复用(select 和poll) (I/O multiplexing)

  • 4)信号驱动I/O (signal driven I/O (SIGIO))

  • 5)异步I/O (asynchronous I/O (the POSIX aio_functions))

二、关于 I/O模型的划分

阻塞:调用的进程一直处于等待状态,直到操作完成。

非阻塞:在内核的数据还未准备好时,会立即返回,进程可以去干其他事情。

从同步异步,以及阻塞、非阻塞两个维度来划分来看:

三、I/O 模型分述

阻塞I/O(blocking I/O)模型

应用进程通过recvfrom函数系统调用,系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。如图,我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据报。

在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程。

socket 编程方式解释:

简介:进程会一直阻塞,直到数据拷贝完成

当调用recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。

当使用socket()函数和WSASocket()函数创建套接字时,默认的套接字都是阻塞的。这意味着当调用Windows Sockets API不能立即完成时,线程处于等待状态,直到操作完成。

并不是所有Windows Sockets API以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会立即返回。将可能阻塞套接字的Windows Sockets API调用分为以下四种:

  • 1.输入操作: recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。如果此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。

  • 2.输出操作: send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。

  • 3.接受连接:accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。

  • 4.外出连接:connect()和WSAConnect()函数。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少到服务器的一次往返时间。

使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。

阻塞模式套接字的不足表现为,在大量建立好的套接字线程之间进行通信时比较困难。当使用"生产者-消费者"模型开发网络程序时,为每个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当希望同时处理大量套接字时,将无从下手,其扩展性很差

Socket编程理解图:

说明1:当上层应用进程调用recv系统调用时,如果对等方没有发送数据(Linux内核缓冲区中没有数据),上层应用进程将阻塞[默认:被Linux内核阻塞)

说明2:当对等方发送了数据,Linux内核recv端缓冲区数据到达,内核会把数据copy给用户空间。然后上层应用进程解除阻塞,执行下一步操作。

非阻塞I/O (nonblocking I/O)模型

书上解释:

我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试 数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。

如下图是《UNIX网络编程卷1》插图

书上解释:

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

当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom 时,我们称此过程为轮询(polling)。由于应用进程像这样连续不断地查询内核,看看某操作是否准备好,这对CPU时间是极大的浪费,所以这种模型只是偶尔才会遇到。

socket 编程方式解释:

简介:非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中,进程是阻塞的;

当使用socket()函数和WSASocket()函数创建套接字时,默认都是阻塞的。在创建套接字之后,通过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl().

套接字设置为非阻塞模式后,在调用Windows Sockets API函数时,调用函数会立即返回。大多数情况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK错误代码。说明请求的操作在调用期间内没有时间完成。通常,应用程序需要重复调用该函数,直到获得成功返回代码。

需要说明的是并非所有的Windows Sockets API在非阻塞模式下调用,都会返回WSAEWOULDBLOCK错误。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代码。当然,在调用WSAStartup()函数时更不会返回该错误代码,因为该函数是应用程序第一调用的函数,当然不会返回这样的错误代码。

要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数之外,还可以使用WSAAsyncselect()和WSAEventselect()函数。当调用该函数时,套接字会自动地设置为非阻塞方式。

由于使用非阻塞套接字在调用函数时,会经常返回WSAEWOULDBLOCK错误。所以在任何时候,都应仔细检查返回代码并作好对“失败”的准备。应用程序连续不断地调用这个函数,直到它返回成功指示为止。上面的程序清单中,在While循环体内不断地调用recv()函数,以读入1024个字节的数据。这种做法很浪费系统资源。

要完成这样的操作,有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。同样,这种方法也不好。因为该做法对系统造成的开销是很大的,并且应用程序至少要调用recv()函数两次,才能实际地读入数据。较好的做法是,使用套接字的“I/O模型”来判断非阻塞套接字是否可读可写。

非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,需要编写更多的代码,以便在每个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。因此,非阻塞套接字便显得有些难于使用。

但是,非阻塞套接字在控制建立的多个连接,在数据的收发量不均,时间不定时,明显具有优势。这种套接字在使用上存在一定难度,但只要排除了这些困难,它在功能上还是非常强大的。通常情况下,可考虑使用套接字的“I/O模型”,它有助于应用程序通过异步方式,同时对一个或多个套接字的通信加以管理。

Socket编程理解图:

说明1: 上层应用程序进程将套接字设置成非阻塞模式。

说明2: 上层应用程序进程轮询调用recv函数,接受数据。若缓冲区没有数据,上层程序进程不会阻塞,recv返回值为-1,错误码是EWOULDBLOCK。

说明3:上层应用程序进程不断轮询有没有数据到来。造成上层应用忙等待。大量消耗CPU。因此非阻塞模式很少直接用。应用范围小,一般和select IO复用配合使用

I/O复用(select 和poll) (I/O multiplexing)模型

书上解释:

I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

如下图是《UNIX网络编程卷1》插图

只要有数据就绪,select调用返回,应用程序调用recvfrom将数据从内核区拷贝至用户区。

仔细看实例图,发现select模型似乎有些disadvantage,即前后进行了两次系统调用,比上一个模型多了一次。然而,select模型也有其明显的优势:每次select阻塞结束返回后,可以获得多个准备就绪的套接字(即一个select可以对多个套接字进行管理,类似于同时监控多个套接字事件是否就绪)。

和阻塞IO模型相比,selectI/O复用模型相当于提前阻塞了。等到有数据到来时,再调用recv就不会因为要等数据就绪而发生阻塞。

socket 编程方式解释:

简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听;

I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

从上图可以看到在I/O复用模型中,I/O执行的两个阶段都是用户进程都是阻塞的,但是两个阶段是独立的,在一次完整的I/O操作中,该用户进程是发起了两次系统调用。

Socket编程理解图:

说明1: 上层应用程序进程调用select(该机制由Linux内核支持,避免了应用程序进程忙等待),进行轮询文件描述符的状态变化。

说明2:当select管理的文件描述符没有数据(或者状态没有变化时),上层应用程序进程也会阻塞。

说明3:好处是:select机制可以管理多个文件描述符

说明4:select可以看成一个管理者,用select来管理多个IO。

一旦检测到的一个I/O或者多个IO,有我们感兴事件发生时,select函数将返回,返回值为检测到的事件个数。进而可以利用select相关API函数,操作具体事件。

说明5:select函数可以设置等待时间,避免了上层应用程序进程长期僵死。

说明6: 和阻塞IO模型相比,selectI/O复用模型相当于提前阻塞了。等到有数据到来时,再调用recv就不会发生阻塞。

信号驱动I/O (signal driven I/O (SIGIO))模型

书上解释:

我们可以用信号,让内核在数据就绪时用信号SIGIO通知我们,将此方法称为信号驱动I/O

首先,我们允许套接字进行信号驱动I/O,并通过系统调用 sigaction 安装一个信号处理程序。此系统调用立即返回,进程继续工作,它是非阻塞的。当数据报准备好被读时,就为该进程生成一个SIGIO信号。我们随即可以在信号处理程序中调用 recvfrom 来取读数据报。

如下图是《UNIX网络编程卷1》插图

socket 编程方式解释:

简介:两次调用,两次返回;

首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

自画理解图:

该模型也叫作基于事件驱动的I/O模型,可以看到该模型中,只有在I/O执行的第二阶段阻塞了用户进程,而在第一阶段是没有阻塞的。

乍看起来感觉和非阻塞模型很相似,其实不同之处就在于,该模型在I/O执行的第一阶段,当数据准备完成之后,会主动的通知用户进程数据已经准备完成,即对用户进程做一个回调。该通知分为两种,一为水平触发,即如果用户进程不响应则会一直发送通知,二为边缘触发,即只通知一次。

Socket编程理解图:

说明1: 上层应用程序进程建立SIGIO信号处理程序。当缓冲区有数据到来,内核会发送信号告诉上层应用程序进程。

说明2:上层应用程序进程接收到信号后,调用recv函数,因缓冲区有数据,recv函数一般不会阻塞。

说明3:这种用于模型用的比较少,属于典型的“拉模式(上层应用被动的去Linux内核空间中拉数据)”。即:上层应用程序进程,需要调用recv函数把数据拉进来,会有时间延迟,我们无法避免在延迟时,会又有新的信号的产生。

异步I/O (asynchronous I/O (the POSIX aio_functions))模型

书上解释:

如下图是《UNIX网络编程卷1》插图

调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。

socket 编程方式解释:

简介:数据拷贝的时候进程无需阻塞。

当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作

自画理解图:

在该模型中,当用户进程发起系统调用后,立刻就可以开始去做其它的事情,然后直到I/O执行的两个阶段都完成之后,内核会给用户进程发送通知,告诉用户进程操作已经完成了。

Socket编程理解图:

说明1:上层应用程序进程调用aio_read函数,同时提交一个应用层的缓冲区buf;调用完毕后,不会阻塞。上层应用程序进程可以继续其他任务。

说明2:当TCP/IP协议缓冲区有数据时,Linux主动的把内核数据copy到用户空间。然后再给上层应用程序进程发送信号;告诉应用程序进程数据到来,需要处理!

说明3:典型的“推模式”

说明4: 效率最高的一种模式,上层应用程序进程有异步处理的能力(在Linux内核的支持下,处理其他任务的同时,也可支持IO通讯)。

异步I/O是指什么?

上层应用程序进程,在进行其他任务的同时时,也可以接收数据\(接受异步通信事件\)。而且上层应用程序并不需要调用recv函数

四、五种模型总结

同步IO引起进程阻塞,直至IO操作完成。

异步IO不会引起进程阻塞。

IO复用是先通过select调用阻塞。

理解图

资料:

高性能IO模型浅析

Last updated