IO模型
用户线程读取数据,当调用channel.read 或 stream.read后,会由用户空间切换至操作系统的内核空间来真正读取数据,而读取数据又分为两个阶段,分别为:
- 等待数据阶段
- 复制数据阶段
阻塞IO(同步)
如上图,用户线程发起了一个read调用,真正读取数据,要切换到内核空间,由操作系统去读数据。当网络中没有数据发送过来时,read调用就会阻塞,表现为用户线程在等待
。等数据从网络传输过来后,操作系统将数据从网卡复制到内存,等数据真正复制完成,那么就从内核空间切换回用户空间,此时read方法调用完成。
非阻塞IO(同步)
如上图,用户线程发起read调用,切换到内核空间。如果发现数据还不存在,会直接返回。但是,如果发现正在复制数据,还是会等待数据复制完成后返回。因此,非阻塞IO发生在等待数据阶段。复制数据阶段,IO还是阻塞的。
多路复用(同步)
用户线程,发起 selector的select()方法调用,此时,线程阻塞。检测有没有事件发生,如果有事件发生,内核告诉用户线程,select()方法不再阻塞。接下来就可以根据发生的事件类型,如发生读事件,那么用户线程就会发起read调用,去内核读取数据。
异步IO
- 同步:只有一个线程做,从发起系统调用,到结果返回,都由一个线程来做。中间,可能会阻塞,线程做不了其他事情。
- 异步:不是一个线程完成,至少有两个线程。如一个线程发起系统调用后,就不管了。交给操作系统去执行,有结果返回时,另外一个线程,将结果返回。返回的结果,会触发原来线程的回调函数,让第一个线程回来继续执行。
异步IO是非阻塞的,不存在阻塞的情况。
零拷贝
传统IO问题
传统IO将一个文件通过socket写出
File f = new File("mydata/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");
byte[] buf = new byte[(int)f.length()];
file.read(buf); //把文件读到数组中
Socket socket = ...;
socket.getOutputStream().write(buf);
- java本身并不具备IO读写能力,因此read方法调用后,要从java程序用户态,切换至内核态(
第一次:用户态到内核态切换
),去操作系统的内核读数据。将数据读入内核缓冲区中(第一次数据拷贝
)。这个期间,用户线程会阻塞,操作系统使用DMA(Direct Memory Access)来实现数据读取,且不使用CPU。 - 从内核态切换到用户态(
第二次:内核态到用户态
),将内核缓冲区的数据,复制到用户缓冲区(第二次数据拷贝
)。这期间会用到CPU无法使用DMA。 - 调用write方法后,将用户缓冲区的数据拷贝到socket缓冲区(
第三次数据拷贝
)。 - 接下来向网卡写数据,这项能力java又不具备,因此,要从用户态切换到内核态(
第三次:用户态到内核态切换
),使用DMA将socket缓冲区的数据拷贝到网卡(第四次数据拷贝
),不会使用CPU。
数据拷贝进行了4次,用户态与内核态切换进行了3次。
NIO优化后
- 通过DirectByteBuf
- ByteBuffer.allocate(10): HeapByteBuffer使用的还是Java内存
- ByteBuffer.allocateDirect(10):DirectByteBuffer 使用的是操作系统内存(特点是:操作系统可以访问,用户也可以访问)
Java 使用了 DirectByteBuf 将对外内存映射到JVM内存来直接访问。读写过程中,减少了一次数据拷贝。但是内核态到用户态切换没有减少。
-
linux 2.1 优化
Java中,channel调用transferTo和TransferFrom后,数据拷贝如图:
数据拷贝只在内核态完成,发生了三次数据拷贝。 -
linux 2.4
Java中,channel调用transferTo和TransferFrom后,数据拷贝如图:
数据拷贝只发生了两次,而内核缓冲区到socket缓冲区,只是拷贝了数据的length,offset等内容,速度极快。
真正的零拷贝,其实不是没有数据拷贝。只是没有将数据拷贝到JVM内存。