一、同步、异步、阻塞与非阻塞
从内核角度看I/O操作分为两步:用户层API调用;内核层完成系统调用(发起I/O请求)。所以同步、异步针对的是用户的API的调用;阻塞、非阻塞针对的是IO请求。
同步指的是函数完成之前会一直等待;阻塞指的是系统调用的时候进程会被设置为sleep状态,直到等待的数据发生
同步与异步:
实际上同步与异步是针对应用程序与内核的交互而言的。同步过程中进程触发IO操作并等待或者轮询的去查看IO操作是否完成。异步过程中进程触发IO操作以后,直接返回,做自己的事情,IO交给内核来处理,完成后内核通知进程IO完成。
阻塞与非阻塞:
若要做一件事情,能不能立即得到返回应答,如果不能立即获得返回,需要等待,就是阻塞;否则就是非阻塞。
二、Unix提供的I/O模型
分为五种:阻塞IO、非阻塞IO、IO复用、信号驱动IO、异步IO
首先我们以一个示例对这五种IO模型进行说明。
事情:演唱会
角色1:售票处
角色2:小明
角色3:黄牛
角色4:快递员
阻塞IO:
小明到售票处进行购票,票还未到发售时间,在售票处等待三天,直到买到票返回
非阻塞IO:
小明到售票处进行购票,票还未到发售时间,小明回家,过三个小时再来问一次,如此反复,直到买到票回家
IO复用:
小明给黄牛打电话,帮小明留意买张票,票买到后黄牛打电话告诉小明,小明来取票回家;票没有出来之前,小明就直接做自己的事情
信号驱动IO:
小明想看演唱会,直接给售票处打电话说明,有票打电话通知一声,小明去取票;票没有出来之前,小明就直接做自己的事情
异步IO:
小明想看演唱会,直接给售票处打电话说明,有票直接让快递员送一张票
阻塞IO:
BIO:称为同步阻塞IO。一个用户连接请求创建一个线程进行处理,它的缺点是用户请求数量和服务端的线程数有很大关系,服务端的线程是并发的瓶颈,限制了服务器用户的数量。
分为两个阶段:
(1)等待数据就绪,网络I/O就是等待远端数据陆续到达,磁盘I/O就是等待磁盘数据从磁盘上读取到内核态中;
(2)数据拷贝,将内核空间的数据拷贝一份到用户态中。
非阻塞IO:
非阻塞IO分为三个阶段:
(1)socket设置为NONBLOCK就是告诉内核,当I/O请求无法完成时,不要将线程设置为sleep状态,而是返回一个错误码(EWOULDBLOCK),这样线程就不会阻塞了;
(2)I/O操作函数会一直测试数据是否准备好,若没有准备好,继续测试,直到准备好;整个I/O请求过程中,虽然用户线程每次发起请求都会立即返回,但为了等到数据,仍要不断轮询,重复请求,浪费大量CPU资源;
(3)数据准备好了,将其从用户空间拷贝到内核空间。
IO复用:
I/O多路复用会用到select函数和poll函数,这两个函数会使线程阻塞,与阻塞I/O不同的是:I/O多路复用可以同时对多个I/O操作进行阻塞,还可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或者可写,才真正调用I/O函数。
从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视Channel,以及调用select函数的额外操作,增加了额外工作。但是,使用 select以后最大的优势是用户可以在一个线程内同时处理多个Channel的I/O请求。用户可以注册多个Channel,然后不断地调用select读取被激活的Channel,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
调用select/poll该方法由一个用户态线程负责轮询多个Channel,直到某个阶段1的数据就绪,再通知实际的用户线程执行阶段2的拷贝。 通过一个专职的用户态线程执行非阻塞I/O轮询,模拟实现了阶段一的异步化。
信号驱动IO
首先我们允许socket进行信号驱动I/O,并安装一个信号处理函数,线程继续运行并不阻塞。当数据准备好时,线程会收到一个SIGIO 信号,可以在信号处理函数中调用I/O操作函数处理数据。
异步IO
调用aio_read 函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。所以异步I/O模式下,阶段1和阶段2全部由内核完成,完成不需要用户线程的参与。