为了限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者随意访问外围设备,CPU 为指令划分了访问等级。而在操作系统中,这将分为内核态和用户态两个等级
内核态:CPU可以访问内存所有数据,包括外围设备,例如硬盘,网卡。CPU 也可以将自己从一个程序切换到另一个程序
用户态:只能受限的访问内存,且不允许访问外围设备。占用CPU的能力被剥夺,CPU 可以被抢占
程序从内核态转换为用户态,或从用户态转换为内核态都需要一定的 CPU 资源
同内核态 / 用户态一样,内存虚拟地址空间也被分为两部分,一部分由内核使用,一部分由用户进程使用。
读操作分为内核准备数据和将数据从内核拷贝到用户空间两个阶段,如图
同样,写操作也分为从用户空间拷贝数据到内核,再从内核写数据到设备两个阶段
同步和异步:描述的是用户线程与内核的交互方式
阻塞和非阻塞:描述的是用户线程调用内核 IO 操作的方式
一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作。
同步 IO 和异步 IO 的区别就在于第二个步骤是否阻塞,如果实际的 IO 读写阻塞请求进程,那么就是同步 IO。
阻塞 IO 和非阻塞 IO 的区别在于第一步,发起 IO 请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞 IO,如果不阻塞,那么就是非阻塞 IO
最简单的 IO 模型,用户线程在读写时被阻塞
应用程序为了执行读写操作,会调用相应的一个系统调用,将系统控制权交给内核(用户态到内核态切换),然后就进行等待(就是被阻塞)。用户线程这时不能做任何事情。
内核开始执行这个系统调用,执行完毕后复制数据到用户态内存,向用户线程返回响应。用户线程得到响应后,就不再阻塞,并进行后面的工作。
在同步阻塞的基础上,用户线程读写时,不被阻塞,而是立即返回,之后用户线程不断发起IO请求。
数据未到达时系统返回一状态值,数据到达后才真正读取数据,为了拿到数据,需不断轮询,无谓地消耗了大量的CPU
一般很少直接使用这种模型,而是在其他 IO 模型中使用非阻塞 IO 这一特性
多路复用有一个选择器(Selector),将需要处理的 Channel (其实就是 socket)注册到选择器上。
然后轮询 Selector(调用 select 并阻塞线程)。当有 Channel 准备完成,可以进行操作(读,写,连接)时,select 会返回。
这时用户线程需要迭代所有 SelectionKey(对应 Channel 的网络事件) 并做相应的业务处理。
另一种多路复用是 epoll,将 socket 注册到 epool(由 epoll_create创建),将注册需要处理的网络事件。
然后轮询(调用epoll_wait,阻塞线程)。当可以进行操作时,epoll_wait 返回,且返回所有准备好的网络事件,用户线程只需要处理已经准备好的网络事件
当数十万并发连接存在时,epoll 和 select 的区别比较明显。在这种情况下,可能每一毫秒只有数百个活跃的连接,
同时其余数十万连接在这一毫秒是非活跃的,Selector 将会浪费大量时间在查询哪些非活跃的连接上。而 epoll 可以节省时间。
另外,每次调用 select,需要把 fd 集合从用户态拷贝到内核态,而 select 支持的文件描述符数量太小
从单个连接的处理流程看,多路复用有点像同步阻塞 IO,但多路复用使得用户可以在一个线程内同时处理多个 socket 的 IO 请求。
调用发出后,系统立刻返回,实际处理这个调用的函数在完成后,通过状态、通知和回调来通知调用者的输入输出操作。异步IO的工作机制是:告知内核启动某个操作,并让内核在整个操作完成后通知
jdk 提供了几种 I/O 模型
reactor 模式建立在多路复用的基础上,这个模式有两个主要角色:事件分离器(dispatcher),事件处理器(handler)
事件分离器:处理读写,连接等行为,通过由线程池实现
事件处理器:同一个线程实现,实现事件循环,不断调用多路分离函数 select (或 epoll_wait)。当有某个事件被激活时,调用关联的事件处理器处理
reactor 处理流程如下
proactor 模式建立在异步 IO 上。
用户线程将AsynchronousOperation(读/写等)、Proactor 以及操作完成时的 CompletionHandler 注册到 AsynchronousOperationProcessor。
当用户线程调用异步API后,用户线程继续执行自己的任务,AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步读写。
当异步 IO操作完成时,将调用 CompletionHandler
处理流程如下:
图片摘自 http://www.cnblogs.com/fanzhidongyzby/p/4098546.html
主动和被动
以主动写为例:
Reactor将handle放到select(),等待可写就绪,然后调用write()写入数据;写完处理后续逻辑;
Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;
可以看出,Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时再进行操作;
Proactor直接调用异步读写操作,调用完后立刻返回;
实现
Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应;
Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响)
优点
Reactor实现相对简单,对于耗时短的处理场景处理高效;
操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性;
事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁;
事务分离:将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来,
Proactor性能更高,能够处理耗时长的并发场景;
缺点
Reactor处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理;
Proactor实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现;
适用场景
Reactor:同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序;
Proactor:异步接收和同时处理多个服务请求的事件驱动程序;
---------------------
按照posix标准,系统io分为同步io和异步io两种,其中同步io常用的是bio nio (阻塞IO和非阻塞IO)。异步io有aio。
(既只有同步有阻塞和非阻塞之分)
从程序的角度来看,bio在读和写的时候,会阻塞,只有当程序将流写入操作系统或者读到流后,阻塞才会结束,线程接着run下去。
非阻塞同步IO用:re actor 模式 异步IO用pro actor模式
而nio和aio属于非阻塞方式,他们都是基于事件驱动思想,但是nio采用的是reactor 模式,而aio采用的是proactor模式。
Reactor 模式使用event loop 阻塞等在io上,一但io可以读或写,通过分发器,遍历事件注册队列,将事件分发到指定注册的处理器。由应用的处理器来再将流读取到缓冲区或写入操作系统,完成io操作。
Proctor 模式下读和写的方法是异步的,只需调用读和写即可。当有流可读取的时候,操作系统会将流传入read方法缓存区,并通知应用程序。对于写,当操作系统将writer 写入完毕时,操作系统会主动通知应用程序。
proactor模式的Aio,流的读取和写入由操作系统完成,省去了遍历事件通知队列selector 的代价。
Windows上的iocp实现了aio,linux目前只有基于epoll模拟实现的aio。
参考:https://www.jianshu.com/p/bae386af45ca 《Reactor 与Proactor》
https://www.cnblogs.com/randomsort/p/9618365.html 《Java I/O 模型 》
https://blog.csdn.net/wanbf123/article/details/78062802 《Reactor和Proactor对比以及优缺点(netty的底层原理reactor模型)》
https://www.cnblogs.com/bitkevin/p/5724410.html