目录
一、五种I/O模型
1. 阻塞式I/O
2. 非阻塞式I/O
3. I/O复用(多路转接)
4. 信号驱动式I/O
5. 异步I/O
二、五种I/O模型的比较
三、I/O复用典型使用在下列网络应用场合
一、五种I/O模型
- 阻塞式I/O
- 非阻塞式I/O
- I/O复用(多路转接)
- 信号驱动式I/O
- 异步I/O
I/O我们并不陌生,简单的说就是输入输出;对于一个输入操作通常包括两个不同的阶段:
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达,然后被复制到内核的某个缓冲区;第二步就是把数据从内核缓冲区复制到应用进程的缓冲区。
注:我们以UDP数据报为例,来介绍这五个模型,主要是因为,数据准备好读取的概念比较简单,要么整个数据报已经收到,要么还没有。对于TCP而言,它是面向字节流的,存在水位线的概念,导致理解起来比较复杂。
接下来我们都会先以一个简单的生活例子来解释这五种I/O模型,然后再对图例进行解释;
1. 阻塞式I/O
最流行的I/O模型是阻塞式I/O模型,我们之前的博客中基本都采用的是阻塞式I/O(因为默认情况下所有的套接字都是阻塞的)。
生活中的例子:
对于阻塞式I/O模型,你可以想象一下钓鱼的例子,假设张三就是在这种模型下进行钓鱼,张三带着一根鱼竿去钓鱼,他在完成所有准备工作后,一心一意的在岸边等待着鱼儿上钩,就像是阻塞了一样,直到鱼儿上钩后或者鱼竿坏了才会动作;
模型解释:
对于上图给出的模型,进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发送错误才返回。最常见的错误是系统调用被信号中断。(进程就好比是张三,调用recvfrom不就是用鱼竿钓鱼嘛,数据报就好比是鱼儿,当数据报准备好并复制会后就好比鱼儿吃钩了)
2. 非阻塞式I/O
对于非阻塞式I/O模型,就是进程把一个套接字设置成非阻塞,本质上就是在通知内核:当所有请求的I/O操作非得把本进程投入睡眠才能完成时,请不要把本进程投入睡眠,而是返回一个错误。
生活中的例子:
非阻塞式I/O模型,也是钓鱼的例子,这次是李四在钓鱼。李四拿着一个鱼竿,他觉得张三钓鱼太累了(阻塞了),他将所有工作都完成后,定期的检测水面是否有动静,如果有鱼儿吃钩就立马挥动鱼竿将鱼钓上来,否则就去干其他的事情;
模型解释:
前三次调用recvfrom时没有数据可以返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已经有一个数据报准备好了,它被复制到应用进程的缓冲区,于是recvfrom成功返回。进而开始处理数据。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询。应用进程持续轮询内核,以查看某个操作是否就绪。这样做往往耗费大量的CPU时间,不过像这样的模型偶尔也会遇到,通常是在专门提供某一种功能的系统中才有。
3. I/O复用(多路转接)
有了I/O复用(多路转接),我们就可以调用select或poll或epoll,阻塞在这三个系统调用的某一个之上,而不是阻塞在真正的I/O系统调用上。
生活的例子:
同样的还是钓鱼的例子,这次是王五钓鱼。王五钓鱼的方式与前两位有所不同。王五带来100个鱼竿来钓鱼,将鱼竿全部插在岸边,只要有一个鱼竿有鱼儿上够了,就立马钓上来;这样一来,整体的效率就上来了。
模型解释:
我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读的数据报复制到应用进程缓冲区中。相比前两个,调用recvfrom就一定不会阻塞,前两个调用recvfrom是将等的时间和读取的时间一起做了,但是I/O复用就讲这两件事分开了。但是感觉I/O复用好像略显劣势,但是使用select最大的亮点就在于它可以等待多个描述符就绪。
4. 信号驱动式I/O
我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式I/O模型。
生活中的例子:
赵六在这种方式下进行钓鱼(一根鱼竿),他将所有工作都做好后,在鱼竿上挂了个铃铛,如果铃铛响了就挥动鱼竿将鱼钓上来,否则就根本不管鱼竿。
模型解释:
我们首先开启套接字的信号驱动功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它并没有被阻塞。当数据报准备好读取时,内核就为改进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom函数读取数据报,并通知主循环数据报已经准备好待处理,也可以立即通知主循环,让他读取数据报。
无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据报已准备好被处理,也可以是数据报已准备好被读取。(后续博客有相关程序案例)
5. 异步I/O
异步I/O的工作机制:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与信号驱动I/O模型的主要区别在于:信号驱动式I/O是有内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
生活中的例子:
没错还是钓鱼,这次是田七钓鱼。田七是一个有钱的老板,他给了自己的司机一个桶、一个电话、一个鱼竿,让司机去钓鱼,当鱼桶装满的时候再打电话告诉田七来拿鱼,而田七自己则开车去做其他事情去了。
模型解释:
我们调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待I/O完成期间,我们的进程不被阻塞。本例子中我们假设要求内核在操作完成时产生某个信号。该信号直到数据已复制到应用进程缓冲区才产生,这一点不同于信号驱动式I/O模型。(后续博客有相关程序案例,这里大致了解即可)
二、五种I/O模型的比较
如下图所示,前4种模型主要区别在于第一阶段,因为他们第二阶段都是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型。
同步I/O和异步I/O的比较:
- 同步I/O:导致请求进程阻塞,直到I/O操作完成;
- 异步I/O:不导致请求进程阻塞;
简单的讲:就是是否参与了I/O操作;
前四种I/O模型——阻塞式I/O模型、非阻塞式I/O模型、I/O复用(多路转接)和信号驱动式I/O模型都是同步I/O,因为其中真正的I/O操作(recvfrom)将进程阻塞。只有异步I/O模型是异步I/O。
三、I/O复用典型使用在下列网络应用场合
- 当客户处理多个描述符时,必须使用I/O复用;
- 如果一个TCP服务器既要处理监听套接字,又要处理已连接的套接字,一般就要使用I/O复用;
- 如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。
- 如果一个服务器要处理多个协议或多个服务,一般就需要使用I/O复用。