5种模型的前4种模型为同步IO,只有异步IO模型是异步IO操作
首先,我们知道对于一个网络输入操作通常包括两个不同阶段:
第一阶段:等待网络数据到达网卡→读取到内核缓冲区(数据准备好)
第二阶段:从内核缓冲区复制数据到进程缓冲区(复制数据了)
这很显然是两个操作,所以我们重点关注以下2点操作:
A、等待数据读取到内核缓冲区(数据准备)
B、复制内核缓冲区数据到进程缓冲区(数据复制)
进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。
调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。
即:A阻塞,B阻塞
举例:
你准备去书店买书,但老板不在,留了电话
你打电话问老板:有没有《三体》卖?
老板查了一下说:书店里没有《三体》,我打个电话让供应商送货,你等一下
然后你就一直在书店里等着,不吃饭,不睡觉(进入A阶段,等待数据,陷入了IO阻塞)
过了一段时间,供应商把书送到书店
老板打电话给你:书已经到了,你有空吗?等你有空,我才当面把书交给你(进入B阶段,数据复制,阻塞住了)
交易完成
进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞,但会一直轮询内核缓冲区数据准备好了没。进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管时间是否已经发生,若时间没有发生,则返回-1,此时可以根据errno区分这两种情况,对于accept,recv和send,事件未发生时,errno通常被设置成eagain。
即:A不阻塞,B阻塞
举例:
你准备去书店买书,但老板不在,留了电话
你打电话问老板:有没有《三体》卖?
老板查了一下说:书店里没有《三体》,我打个电话让供应商送货,你先回家去吧,过几天再来问(进入A阶段,等待数据,但非阻塞)
想象:你回到家,该吃就吃,该喝就喝,隔了1周后才打电话问老板书到了没?(非阻塞,继续做别的事情)
但一般编程,都会设置轮询IO,即:
实际:你回到家,循环不停地打电话给老板,我的书到了没?(陷入事实上的轮询IO阻塞,相当于死循环,即使属于非阻塞模型)
过了1个小时,老板说:书已经到了,你有空吗?等你有空,我才当面把书交给你(进入B阶段,数据复制,阻塞住了)
交易完成
多个进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该复用器(select)。 复用器(select)会监听所有注册进来的IO。如果复用器(select)所监听的IO在内核缓冲区都没有可读数据,复用器(select)调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,复用器(select)调用就会返回。之后复用器(select)通知注册进程来再次发起读取IO,读取内核中准备好的数据。
linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数。
即:A不阻塞,B阻塞
举例:
对于书店老板来说,他对接的是多个客人。你,张三,李四都打电话给老板,要买书
你买《三体》,张三买《鲁迅》,李四买《史记》,然后最坏的情况,3本书老板都没有
老板说:我打个电话让供应商送货,你们先别一直打电话,书到了我再打电话让你们过来取
实际:你,张三,李四挂了电话,就去吃饭睡觉,做自己的事了(进入A阶段,等待数据,但非阻塞)
老板一直等不到供应商来送货,但自己需要一直等待着(陷入事实上的select/poll/epoll阻塞)
此时供应商送来一本书,但老板不知道属于谁,得遍历一边购书单,发现是你的书
老板打电话给你:书已经到了,你有空吗?等你有空,我才当面把书交给你(进入B阶段,数据复制,阻塞住了)
交易完成
当进程发起一个IO操作,会向内核注册一个信号处理函数 ,然后进程返回;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据
linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号。然后处理IO事件。
即:A不阻塞,B阻塞
举例:
你准备去书店买书,但老板不在,留了电话
你打电话问老板:有没有《三体》卖?
老板查了一下说:书店里没有《三体》
然后你说:我给你留个收货地址。如果书到了,你打电话给我,我会让别人在这个地址接收你的书
然后你就回家去了,该干嘛干嘛(进入A阶段,等待数据,但留了回调函数,非阻塞)
老板打电话给你:书已经到了,让你的人准备好,我才当面把书交给你的人(进入B阶段,数据复制,阻塞住了)
交易完成
当进程发起一个IO操作,进程立即返回,但也不返回结果;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。所谓的通知进程结果,是指:包含将数据从内核复制到应该进程的缓冲区,完成后通知应用进程,也就是说,已经完成了A B 2个步骤了,才通知请求进程,此时请求进程直接获取到数据,根本不会阻塞,即所谓的异步。
linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。
即:A不阻塞,B不阻塞
你准备去书店买书,但老板不在,留了电话
你打电话问老板:有没有《三体》卖?
老板查了一下说:书店里没有《三体》,你留个地址,书到了我会把书送到你家去
然后你就回家去了,该干嘛干嘛(进入A阶段,等待数据,非阻塞)
老板接收到供应商的书,把书放到你家楼下,而此时你在家睡觉(进入B阶段,数据复制,非阻塞)
老板打电话给你:书已经到了,在你家楼下,自己去拿吧
交易完成
根据上面所说的IO操作的两个阶段,可以把上面的I/O模型进行如下归类:
阻塞IO:在两个阶段上面都是阻塞的,A阶段阻塞,B阶段还是阻塞
非阻塞IO:在A不阻塞(但陷入事实上的轮询IO阻塞),B阶段还是阻塞的
IO复用:A阶段不阻塞(陷入事实上的select/poll/epoll阻塞),B阶段还是阻塞的
信号IO:A阶段不阻塞,当信号通知程序数据准备完毕,B阶段还是阻塞的
异步IO:A阶段不阻塞,B阶段不阻塞
现在,我们可以看到,前4种IO模型的B阶段:数据复制阶段都是阻塞,也就是按POSIX标准来说的同步IO,最后1种异步IO模型,才是按POSIX标准来说的异步IO。
同步一般指主动请求并等待I/O操作完毕的方式,I/O操作未完成前,会导致应用进程挂起
而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知),这可以使进程在数据读写时也不阻塞
阻塞或者非阻塞I/O主要是指I/O操作第一阶段的完成方式(进程访问的数据如果尚未就绪),即数据还未准备好的时候,应用进程的表现
如果这里进程挂起,则为阻塞I/O,否则为非阻塞I/O,主要是描述进程在数据未准备好时的状态,根据进程等待数据时的状态来判断
阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式:
说白了就是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待
而非阻塞方式下,读取或者写入函数会立即返回一个状态值