【总结】JavaIO

IO

背景

操作系统的核心资源(CPU、 内存、网络、I0、 驱动)均由内核进行管理,为了避免用户直接操作内核,保证内核的安全,操作系统将内存寻址空间划分为两部分:内核空间、用户空间。
针对I/O操作,以读取为例来说,数据需要由磁盘拷贝到内核缓冲区,再由内核缓冲区拷贝到用户缓冲区。这两次拷贝均需要一定的时间, 所以操作系统支持不同的策略来实现这种拷贝,以提高性能。

【总结】JavaIO

Unix I/O模型

简介

Linux的内核将所有外部设备都看做一个文件来操作, 对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor (fd, 文件描述符)。而对一个socket的读写也会有相应的描述符,称之为socket fd (socket描述符) 。描述符就是一个数字,它指向内核中的一-个结构体(文件路径,数据区等–些属性)

根据UNIX网络编程对I/O模型的分类,UNIX提供 了5种I/O模型:阻塞I/O模型、非阻塞式I/O模型、I/O复用模型、信号驱动I/O模型。异步I/O模型 。

01 /阻塞I/O模型

最常用的I/O模型就是阻塞I/O模型,缺省情况下,所有文件操作都是阻塞的。我们以套接字接口为例讲解此模型:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间会一直等待,进程再从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞l/O模型。(即在那两次copy期间都会被阻塞)

【总结】JavaIO

02 /非阻塞I/O模型

recvfrom从应用层到达内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,-般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。

【总结】JavaIO

前半阶段不阻塞,后半部分阻塞(前半部分虽然不阻塞,但是一直在轮询,cpu干不了别的)总体上性能相比阻塞的没有很大的提升

03 / I/O复用模型

Linux提供select/poll,进程通过将一个或 多个fd传递给select或pol系统调用,阻塞在select操作上(但是已经传递过后阻塞,并监控),这样==select/poll可以帮我们侦测多个fd是否处于就绪状态。==select/poll是顺序扫描fd是否 就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。
Linux还提供了一个epol系统调用,epoll使 用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数callback。/O复用模型的优势是,我们可以等待多个描述符就绪。

不是扫描而是时间

【总结】JavaIO

第一阶段阻塞,第二阶段也阻塞,但是第一阶段可以处理多个文件

04 /信号驱动I/O模型

首先开启套接口信号驱动I/O功能,并 通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一-个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。

【总结】JavaIO

第一阶段不阻塞,第二阶段阻塞

05 /异步I/O模型

告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时开始-个/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成。

【总结】JavaIO

第一和第二阶段都不阻塞。但并不是每个操作系统都支持它

06 /五种I/O模型的比较

【总结】JavaIO

I/O复用,虽然阻塞,但是在第一个阶段可以监控多个文件(一个进程监控多个远程调用)

1、只有最后一种是异步的,其余都是同步的。

2、只有最后一个都不阻塞,其余都有不同的阻塞(正因为这样才是同步)

要明白什么时候阻塞和同/异步

07 / I/O多路复用技术

I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,/O多路复用的最大优势是系统开销小,系统不需要创建新的额外的进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
目前支持I/O多路复用的系统调用有select、pselect、 poll、 epoll, 在Linux网络 编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select的一 些固有缺陷导致了它的应用受到了很大的限制,最终Linux不得不在新的内核版本中寻找select的替代方案,最终选择了epoll。

为了克服select的缺点,epol做了很多重大改进,主要有如下两点:

1.支持一个进程打开 的socket描述符不受限制
select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。
对于那些需要支持上万个TCP连接的大型服务器来说显然太少了。epolI并 没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024。例如,在1GB内存的机器上大约是10万个句柄左右。

2.I/O效 率不会随着FD数目的增加而线性下降
传统的select/poll另-个致命弱点就是当你拥有一个很大的socket集合,由于网络延时或者链路空闲,任- -时刻只有少部分的socket是活跃"的,但是select/polI每次调用都会线性扫描全部的集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对“活跃的socket进行操作。这是因为在内核实现中.
epoll是根据每个fd上面的callback函数实现的,那么,只有“活跃的socket才会主动的去调用callback函数,其他空闲状态socket则不会。

Java I/O模型

01/IO

【总结】JavaIO

02 / NIO

新的输入/输出(NIO) 库是在JDK 1.4中引入的。NIO弥补了原来同步阻塞I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据。NIO包含三个核心的组件: Buffer (缓冲区)、Channel (通道,读写都是通过它,是双向的)、Selector (多路复用器,一个线程监视多个文件)

2.1 Buffer

Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的/O中,可以将数据直接写入或者将数据直接读到Stream对象中。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

  • Buffer是抽象类, 它有如下子类:
    ByteBuffer、CharBuffer. ShortBuffer、 IntBuffer、 LongBuffer、 FloatBuffer、 DoubleBuffer
  • 只能通过静态方法实例化Buffer:
    public static ByteBuffer allocate(int capacity); // 分配堆内存
    public static ByteBuffer allocateDirect(int capacity); // 分配直接内存

区别在于前者分配的是堆内存,后者是直接内存(操作系统的内存)

  • Buffer中 的四个成员变量:
  1. 容量(capacity) : Buffer可以存储的最大数据量,该值不可改变;
  2. 界限(imit) : Buffer中可以读/写数据的边界,limit之后的数据不能访问
  3. 位置(position) :下一个可以被读写的数据的位置(索引) ;
  4. 标记(mark) : Buffer允许将位置直接定位到该标记处,这是一个可选属性;
    并且,上述变量满足如下的关系: 0 <= mark <= position <= limit <= capacity。

【总结】JavaIO

【总结】JavaIO

每次并不清除数据而是覆盖数据,并将指针都还原

2.2 Channel

Channel是一个通道, 可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。==通道与流的不同之处在于通道是双向的,流只是在一个方向上移动而且通道可以用于读、写或者同时用于读写。==因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的APl。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

【总结】JavaIO

2.3 Selector

Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll(0代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。

Selector是抽象类,可以通过调用此类的open()静态方法来创建Selector实例。Selector可 以同时监控多个SelectableChannel的I0状况,是非阻塞IO的核心。一个Selector实例有 三个SelectionKey集合:
1.所有的SelectionKey集合:代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回。

2.被选择的SelectionKey集合:代表了所有可通过select()方法获取的、需要进行IO处理的Channel,这个集合可以通过selectedKeys(返回。

3.被取消的SelectionKey集合: 代表了所有被取消注册关系的Channel,在下一次执行select)方法时, 这些Channel对应的SelectionKey会被彻底删除,程序通常无须直接访问该集合。

Selector还提供了一系列和select()相关的方法:

  1. int select():监控所有注册的Channel,当它们中间有需要处理的I0操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合中,该访法返回这些Channel的数量。
  2. int select (long timeout):可以设 置超时时长的select()操作。
  3. int selectNow():执行一个立即返回的select)操作,相对于无参数的select)方法而言,该方法不会阻塞线程。
  4. Selector wakeup():使一个还未返 回的select()方法立刻返回。

03 / NIO2

【总结】JavaIO

使用AsynchronousServerSocketChannel只要三步:
1.调用AsynchronousServerSocketChannel的静态方法open(创建AsynchronousServerSocketChannel实例。
2.调用AsynchronousServerSocketChannel实例的bind(方法让它在指定IP地址、端口监听。
3.调用AsynchronousServerSocketChannel实 例的accept()方法接受连接请求。

程序调用accept()方法之后,当前线程不会阻塞,而程序也不知道accept()方法什么时候会接收到客户端
的请求。为了解决这个异步问题,AIO提供 了两个版本的accept()方法。

  1. Future< AsynchronousSocketChannel> accept():
    接受客户端的请求。如果程序需要获得连接成功后返回的AsynchronousSocketChannel,则应该调用该方法返回的Future对象的get(方法。但get()方 法会阻塞线程,因此这种方式依然会阻塞当前线程。
  2. void accept(A attachment, CompletionHandler<AsynchronousSocketChannel, ? super A>handler):接受来自客户端的请求,连接成功或连接失败都会触发CompletionHandler对象里相应的方法。其中AsynchronousSocketChannel就代表连接成功后返回的AsynchronousSocketChannel。

04 / Netty

在进行磁盘IO操作时,建议使用NIO。在进行网络IO操作时,则建议使用Netty框架,原因如下:

  1. NIO的类库和API繁杂, 使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、 SocketChannel、ByteBuffer等类;
  2. 需要具备其他额外的技能 做铺垫,例如熟悉Java多 线程编程。这是因为NIO编程设计到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序;
  3. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大;
    等类;
  4. 需要具备其他额外的技能 做铺垫,例如熟悉Java多 线程编程。这是因为NIO编程设计到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序;
  5. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大;
  6. JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK 1.6版本的update18修复了该问题,但是直到JDK 1.7版本该问题仍旧存在,只不过该BUG发生的概率降低了一些而已,它并没有得到根本性的解决。
上一篇:NIO和IO


下一篇:NIO源码解析-Buffer简介