五种模型出自:RFC标准。可参考: 《UNIX网络编程-卷一》 6.2
很多程序员是从高级语言的网络编程/文件操作了解到nio,继而了解到五种io模型的;
这五种io模型不止用于网络io
- “阻塞与****系统调用”是怎么回事?我知道了线程.sleep()可以阻塞线程,指定sleep位可以标记状态,,但是还没见到阻塞的系统调用,,
- 是说发动调用后阻塞?调用系统调用自动阻塞?
- 说到底系统调用是怎么用啊,原以为是包含头文件,结果头文件是封装好的入口,,
linux网络编程一共有几种 IO 模型?NIO 和多路复用的区别?
一共有五种IO模型
- 阻塞IO模型 BIO
- 非阻塞IO模型 NIO
- IO多路复用模型(select,poll,epoll…)
- 信号驱动模型(SIGIO)
- 异步IO(AIO,POSIX的aio_系列函数) Future-Listener机制 )
IO操作可分为两阶段看待:
1)进程向发起IO请求,等待数据准备(Waiting for the data to be ready),系统调用后进入内核态,内核操作数据到内核缓冲区
2)实际的IO操作,将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
I/O多路复用是阻塞在select,epoll这样 的系统调用,没有阻塞在真正的I/O系统调用
如recvfrom进程受阻于select,等待可能多个套接口中的任一个变为可读
IO多路复用使用两个系统调用(select和 recvfrom) ,blocking IO只调用了一个系统调用 (recvfrom)
多路复用模型中,每一个socket,设置为 non-blocking, 阻塞是被select()阻塞,而不是被 socket阻塞的
select/epoll 核心是可以同时处理多个 connection,而不是更快的处理单个connection,所以连接数不高的话,性能不一定比多线程+阻塞IO好
前四种IO模型都是同步IO操作,他们的区别在于第一阶段,而第二阶段是一样的,即在内核数据copy到用户空间时都是阻塞的。
而异步 I/O模型的进程在这两个阶段都是运行的。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,实际IO读写阶段会阻塞进程,而在异步IO中,是由操作系统帮忙做完IO工作再直接返回结果。
- 同步是在等待什么?阻塞是通知进程后等他主动发起吗?进程是在空转等着通知,还是干着其他事情,轮询着等通知吗?
几个核心点:
- 此处阻塞非阻塞说的是线程的状态,同步和异步说的是消息的通知机制
- 同步需要主动读写数据,异步是不需要主动读写数据
- 同步IO和异步IO是针对用户应用程序和内核的交互
##NIO(非阻塞IO模型)
NIO,即Non-Blocking IO,是非阻塞IO模型。
NIO存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源。可以考虑IO复用模型去解决这个问题。
不阻塞了,但是轮询doge
阻塞会让出cpu,轮询会一直占着cpu
非阻塞IO的流程如下:
- 应用进程发起IO系统调用,内核态进行IO操作
- 应用进程轮询向操作系统内核,发起recvfrom读取数据。
- 操作系统内核数据没有准备好,立即返回EWOULDBLOCK错误码。
- 应用程序进程轮询调用,继续向操作系统内核发起recvfrom读取数据。
- 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
- 完成调用,返回成功提示。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
常见的RPC框架,如Thrift,Dubbo
这种框架内部一般维护了请求的协议和请求号,可以维护一个以请求号为key,结果的result为future的map,结合NIO+长连接,获取非常不错的性能。
IO多路复用模型
多路指多个TCP连接(即 socket或者channel),复用指复用一个或几个线程。
解决轮询的方法:先阻塞进程,等到内核数据准备好了,主动通知应用进程再去进行系统调用。
- 阻塞等于挂起吗?是说发起select()调用后,进程被阻塞,进入内核态等select调用返回?既然说阻塞与select()调用,那应该是这个意思。
IO复用模型核心思路:系统给我们提供一类函数(如select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。
- 多个Socket怎么说?等一批一起处理?后面追加的怎么办?
最简单的Reactor模式:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。
Java的Selector对于Linux系统来说,有一个致命限制:同一个channel的select不能被并发的调用。因此,如果有多个I/O线程,必须保证:一个socket只能属于一个IoThread,而一个IoThread可以管理多个socket。
另外连接的处理和读写的处理通常可以选择分开,这样对于海量连接的注册和读写就可以分发。虽然read()和write()是比较高效无阻塞的函数,但毕竟会占用CPU,如果面对更高的并发则无能为力。
对于Redis来说,由于服务端是全局串行的,能够保证同一连接的所有请求与返回顺序一致。这样可以使用单线程+队列,把请求数据缓冲。然后pipeline发送,返回future,然后channel可读时,直接在队列中把future取回来,done()就可以了。
常见的RPC框架,如Thrift,Dubbo
这种框架内部一般维护了请求的协议和请求号,可以维护一个以请求号为key,结果的result为future的map,结合NIO+长连接,获取非常不错的性能。
IO多路复用之select
应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。
NIO中,需要轮询多次轮询系统调用直到可以读取数据,然而借助select的IO多路复用模型,只需要发起一次询问就够了,大大优化了性能。
select监视文件3类描述符: writefds、readfds、和 exceptfds。
调用后select函数会阻塞住,等有数据 可读、可写、出异常 或者 超时 就会返回。
但是,select有几个缺点:
- 单个进程监听的IO最大连接数(FD,文件描述符)有限,默认是1024 (可修改宏定义) static final int MAX_FD = 1024
- select函数返回后,是通过遍历fdset,找到就绪的描述符。(仅知道有I/O事件发生,却不知是哪几个,所以遍历所有来查找(不能随机访问的数据结构的亚子))随着数量增加而性能下降
- 每次调用 select(),需要把 fd 集合从用户态拷贝到 内核态,并进行遍历(消息传递都是从内核到用户空间
poll
因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题(用链表存储)。但是select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。
IO多路复用之epoll
为了解决select/poll存在的问题,多路复用模型epoll诞生,它采用事件监听回调机制来实现,流程图如下:
epoll先通过epoll_ctl()来注册一个fd,一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。
epoll()在2.6内核中提出的,对比select和poll,epoll更加灵 活,没有描述符限制,用户态拷贝到内核态只需要使用事件通知
优点:
-
没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
-
效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降。通过callback机制通知,内核和用户空间mmap同一块内存实现
JAVA中的I/O
1.4之前BIO
大型服务一般采用 C或者C++, 因为可以直接操作系统提供的异步IO,AIO.
NIO @Since(“1.4”)
NIO2.0 @Since(“1.7”),提供 AIO的功能,支持文件和网络套接字的异步IO.
NIO高级
Proactor与Reactor
一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。
涉及到事件分发器的两种模式称为:Reactor和Proactor。Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。
Reactor相当于去医院,先用事件分发器向操作系统挂号,等叫到号了(被通知can read 或者 can write)应用线程再去实际IO
Proactor相当于网上办理银行业务,向操作系统告知自己要把数据调动到什么地方,然后等操作系统内核线程完成了再告诉事件分发器
有点类似有DMA的硬件设备的IO,这里的NIO是面向线程的
在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
而在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称为overlapped技术),事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
在Proactor中实现读:
- 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
- 事件分发器等待操作完成事件。
- 在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。
- 事件分发器呼唤处理器。
- 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。
附录
相关系统调用
linux
- recvfro
- epoll_create() 在Linux内核里面申请一个文件系统 B+树,返回epoll对象,也是一个fd
- epoll_ctl() 操作epoll对象,在这个对象里面修改 添加删除对应的链接fd, 绑定一个callback函数
- epoll_wait() 判断并完成对应的IO操作
windows
- IOCP