浅析IO,NIO和五种IO模型

浅析IO,NIO和五种IO模型


在这里插入图片描述

为了便于cpu的管理,合理分配cpu资源,inter x86架构的cpu一共有四个级别,0-3级,0级特权最高,3级特权最低。最重要的工作要交给最高的特权级完成。

一:几个基本的概念

1.用户态:对于操作系统来说,JVM只是一个用户进程,是属于应用程序,处于用户态空间中,特权级是3,不能直接操作底层的硬件。

2.系统调用:这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。系统调用的是操作系统级别的api,比如java IO的读取数据过程(使用缓冲区),用户程序发起读操作,导致“ syscall read ”系统调用,就会把数据搬入到 一个buffer中;用户发起写操作,导致 “syscall write ”系统调用,将会把一个 buffer 中的数据 搬出去(发送到网络中 or 写入到磁盘文件)。可以说系统调用时处于用户态和内核态中间的一个过程

3.内核态:内核态的特权级是0,可以直接操作系统的资源。例如用户发起IO请求,系统调用会调用内核代码来,特权级会从3切换到0,等到执行完成之后才会从内核态切换到用户态。

4.局部性原理:操作系统访问磁盘的时候,由于局部性原理,操作系统不会每一次只去读取一个字节,这样代价太大,而是会借助硬件(直接存储器,DMA)一次性存取一片数据,因此就需要一个内核缓冲区。现将数据从磁盘读取到内核缓冲区,再将数据从内核缓冲区搬运到用户缓冲区。

操作系统与Java基于流的I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区)。I/O类喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据,java.io的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io类则喜欢一铲子一铲子地加工数据。
—— 引自《JAVA NIO》

二:同步,异步,阻塞,非阻塞

1.同步:

多个任务组成一个事件流,其中的一个任务的执行会导致整个事件流的暂时中止,这些任务没有办法并发的完成。例如在java中的synchronized对一个变量加锁,有多个线程来访问这个变量,一个线程访问的时候其他线程就只能挂起。

2.异步:

多个任务组成一个事件流,其中的一个任务的执行不会导致整个事件流的暂时中止。可以采用多线程实现异步。

3.阻塞:

当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足;

4.非阻塞:

当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。

注意:

同步异步,阻塞非阻塞不是同一种概念,同步异步的概念是,一个任务的执行会不会导致整个事件流的中止;阻塞非阻塞的概念是,发送一个请求,条件不满足无法完成请求的时候,会不会得到一个标志信息告知条件不满足。

三:阻塞IO,非阻塞IO,同步IO,异步IO

一般来说,一个IO请求包含两个步骤(以读请求为例):

  1. 内核查看数据是否就绪
  2. 进行数据拷贝(数据从磁盘读到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区)
1.阻塞IO:

对于阻塞IO来说,如果数据没有就绪就会一直等待,用户线程就处于阻塞状态,用户线程交出CPU,当数据就绪之后内核将数据拷贝到用户缓冲区,用户线程才会解除block状态。

2.非阻塞式IO:

非阻塞式IO,如果数据没有就绪,就会返回一个标志信息告知用户线程,数据没有准备好,但数据就绪之后便将数据拷贝到用户线程。非阻塞式IO不会移交CPU,会一直轮询,看数据是否准备好。

3.同步IO:

事实上,同步IO和异步IO模型是针对用户线程和内核的交互来说的:

对于同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;

4.异步IO:

而异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。

这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。

同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。

注意:

注意同步IO和异步IO与阻塞IO和非阻塞IO是不同的两组概念。

阻塞IO和非阻塞IO是反映在当用户请求IO操作时,如果数据没有就绪,是用户线程一直等待数据就绪,还是会收到一个标志信息这一点上面的。也就是说,阻塞IO和非阻塞IO是反映在IO操作的第一个阶段,在查看数据是否就绪时是如何处理的。

四:五种IO模型

1.阻塞式IO模型:

用户发起IO请求之后,内核会去查看数据是否就绪,如果数据没有就绪,用户线程就会一直等着,就出处于阻塞状态,移交出cpu.当数据就绪之后,内核会将数据拷贝到用户缓冲区。

例子:如何数据没有就绪就会一直阻塞在read()方法

data = socket.read(); 
2.非阻塞式IO模型:

用户发送一个IO请求之后,不会等待,立即会得到一个结果,如果数据没有就绪,会返回一个标志信息,用户就会知道数据没有准备好,用户线程会一直轮询。数据一旦就绪,并且再次收到了用户线程的请求,内核就会将数据拷贝到用户缓冲区。

但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。

例子:

while(true){
    data = socket.read();
    if(data!= error){
        处理数据
        break;
    }
}
3.多路复用IO模型

JavaNIO中用的就是多路复用的IO模型。

多路复用模型中,只有一个线程去不断轮询多个socket的状态,socket中有读写的时候才去调用IO资源。

在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。

另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的(非阻塞),这个效率要比用户线程要高的多。

4.信号驱动IO模型

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

5.异步IO模型

异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。

也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。

注意:

异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。

五:Java中NIO实现

Java NIO 由以下几个核心部分组成:

Channels
Buffers
Selectors
Channel 和 Buffer

基本上,所有的 IO 在NIO 中都从一个Channel 开始。Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。这里有个图示
在这里插入图片描述Channel和Buffer有好几种类型。下面是JAVA NIO中的一些主要Channel的实现:

FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel

这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。

以下是Java NIO里关键的Buffer实现:

ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer

这些Buffer覆盖了你能通过IO发送的基本数据类型:byte, short, int, long, float, double 和 char。Java NIO 还有个 MappedByteBuffer,用于表示内存映射文件

Selector

Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。

这是在一个单线程中使用一个Selector处理3个Channel的图示:
在这里插入图片描述要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值