Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

这篇具有很好参考价值的文章主要介绍了Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

系列文章目录和关于我

零丶背景

最近有很多想学的,像netty的使用、原理源码,但是苦于自己对于操作系统和nio了解不多,有点无从下手,遂学习之。

一丶网络io的过程

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

上图粗略描述了网络io的过程,了解其中的拷贝过程有利于我们理解非阻塞io,以及IO多路复用的必要性。

  1. 数据从网卡到内核缓冲区
    网卡通过DMA的方式将网络帧copy到内核空间

    并不是拷贝到内核空间就完事了,因为还需要根据协议对数据进行处理。

    所以网卡使用硬中断通知cpu,cpu响应后会使用网卡注册函数进行收包,然后协议层处理网络帧。

  2. 数据从内核缓冲区到用户空间

    根据协议处理好的数据,还需要拷贝到用户空间才能被运行在内核态的应用程序使用==>cpu进行数据拷贝。随后内核唤醒用户进程,相当于我们的java程序从阻塞io中被唤醒,继续执行下一行代码的执行。

二丶Socket通信过程与其中的阻塞点

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

这其中有几个阻塞的过程

  • accept 系统调用:等待客户端建立tcp连接

    这个问题不大,没有连接那么阻塞服务端线程,可以节约cpu资源。

  • read系统调用:等待请求数据来到用户空间

    数据从网卡到用户空间的过程,线程时阻塞的

  • Servlet#service 处理请求是一个同步过程

    tomcat根据http协议构造request,并和response作为参数,找到对应Servlet调用service方法,Servlet#service方法执行结束,返回内容才能通过write系统调用回应数据。

    这导致在业务处理上需要使用线程池来让服务端可以处理多个并发请求。

  • write系统调用:响应数据写回

    write系统调用将servlet处理后的响应数据,写回到文件描述符中。

三丶NIO解决了什么问题

1.单线程监测若干个文件描述符是否可以执行IO操作

这就是常说的IO多路复用,那为什么需要IO多路复用?

尽量使用较少的系统资源处理更多的连接,如果当前单台服务器接收了1w个请求,服务端当如何处理?

1.1 传统BIO模型

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

上面是一段java BIO模型并发处理多请求的实例代码,它有以下不足

  • 大量的线程占用很大的内存空间
  • 线程切换会带来很大的开销
  • process方法中需要需要调用read系统调用,阻塞直到可读,并没有真正进行读写操作。

1.2. 非阻塞IO

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

上面是非阻塞IO的一个实例

socketChannel.configureBlocking(false)可以让后续的read在通道数据没有就绪的时候直接返回-1,而不是让线程阻塞。这个特性让调度线程池中的线程减少了阻塞,从而节省了线程资源。

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

但是这种方式也不是没有任何缺点,多次系统意味着多次系统调用,每次系统调用都需要,用户态<=>内核态的来回切换,需要cpu保存进程的上下文,调用结束还需要恢复进程的上下文。

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

1.3 IO多路复用

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

如上是Java IO多路复用的简陋例子。操作系统提供了多路复用的机制,将连接上来的客户端都进行注册,然后不断循环扫描各个客户端连接,监听客户端的请求。但是,多路复用轮询扫描各个客户端连接的过程在操作系统内核中进行极大的加快了多路复用的效率,减少了用户态和内核态的切换

2.减少堆内内存<=>堆外内存的拷贝开销

使用NIO Channel读写时需要需要先读到堆外内存,然后拷贝到堆内内存,如果直接使用堆外内存则可以减少堆外到堆内的拷贝过程。

下图是将Channel数据读取到Buffer,调用IOUtil#read的源码

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

下图是将Buffer数据写入到Channel,调用IOUtil#write的源码

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

2.1 为什么需要再堆外内存和堆内内存来回捯饬?

写入Buffer数据到文件描述符,or读取文件描述符数据到Buffer都是需要进行系统调用的,执行系统调用依赖于执行native方法,而执行native方法的线程被认为是处于SafePoint,处于SafePoint就有可能发生 GC 重排列对象内存的情况。

并且这个写入和读取是针对地址的(如下图,最终的native调用需要传入地址)如果写入或者读取buffer由于gc移动,那么地址会改变,但是native方法调用可不管这个,就导致读写出现错误。因此需要依赖于堆外内存。

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

2.2 为什么Socket基于Inpustream,OutputStream没有这个问题

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

以SokcetInputStream的读为例,读最终调用socktRead0这个native方法,入参fd是当前Socket对应的文件描述符,byte数组就是数据最终读入的目的地。

下图是native 方法socketRead0的实现

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

可以看到,其实是先将socket fd内容读取到c语言声明的数组,然后拷贝到Java byte[],这个c语言声明的数组其实作用类似于直接内存!

3.减少内核空间和用户空间的拷贝开销

上面说了直接内存的作用:减少堆外堆内的拷贝开销。无论堆外堆内,都是用户空间的拷贝。

3.1 DMA控制器替CPU打工

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

上图是读取磁盘文件的时序图,可以看到如果没有DMA技术,蓝色部分需要CPU来完成,将浪费宝贵的资源。

再DMA读取到足够数据后,会发送中断信号给CPU,让CPU将内核缓冲区数据,拷贝到用户缓冲区,随后CPU再来调度Java程序,Java程序才能操作到用户缓冲区的数据。

3.2 零拷贝

3.2.1 传统文件传输

如下图是我们使用IO流,读取磁盘文件,通过Socket API 发送的流程,其中需要read,和 write 系统调用,每次系统调用都意味着用户态与内核态的上下文切换

并且还有四次数据拷贝,其中两次由DMA负责打工,两次由CPU负责拷贝。

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

如何优化:

  • 如果Java程序不需要对磁盘数据内容进行再加工(业务操作)那么不需要拷贝到用户空间,从而减少拷贝次数
  • 由于用户空间没有操作网卡和磁盘的权限,操作这些设备需要由操作系统内核完成,那么如果操作系统提供新的系统调用函数,岂不是就可以减少用户态与内核态的上下文切换
3.2.2 mmap + write

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

  • 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核共享这个缓冲区;
  • 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
  • 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的

所以mmap优化了什么?

mmap并没有减少系统调用带来的内核态用户态切换开销,只是应用程序和内核共享缓冲区,从而让cpu可以直接将内核缓冲区的数据,拷贝到socket缓冲区,不需要拷贝到用户缓冲区,再从用户缓冲区拷贝到socket缓冲区。

3.2.3 sendfile

linux 提供sendfile系统调用,只需这一个系统调用就可以从一个文件描述符拷贝数据到另外一个文件描述符

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

sendfile可以减少write,read导致的系统调用,从而优化效率。

如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术,那么还可以进一步优化。

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

  1. 通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
  2. 缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝。

这便是所谓的零拷贝,减少内存层面拷贝数据的次数,以及系统调用内核态用户态的切换,从而优化性能。

3.3 NIO中的零拷贝

3.3.1 FileChannel#map

NIO中的FileChannel.map()方法使用了mmap系统调用实现内存映射方式

将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能对文件内容进行更改,但是如果其后要通过SocketChannel发送,还是需要CPU进行数据的拷贝。

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

如上是MappedByteBuffer的获取方式,其实底层是通过反射调用DirectByteBuffer的构造方法实现的,其中的cleaner是直接内存的回收器,传入的unmapper会被回调,从而调用native方法实现资源释放。

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

这种方式适合读取大文件,同时也能对文件内容进行更改。

3.3.2 FileChannel#transferTo,transerFrom

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

在操作系统层面是调用的一个sendFile系统调用。通过这个系统调用,可以在内核层直接完成文件内容的拷贝。

4.FileChannel#force强制刷盘

由于CPU的运行速度非常快,所以CPU在执行指令时,通常只能与缓存进行交互,而不适合直接操作像磁盘、网卡这样的硬件。也因此,在进行文件写入时,操作系统也是先写入到page cache中,缓存起来,然后再往硬件写入。

缓存有利也有弊,使用page cache页缓存,应用程序将数据都写入到了page cache中,但是却没有真正写入磁盘。如果这个时候出现断电,那么将出现缓存数据丢失。

FileChannel#force会进行fsync系统调用

Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)

fsync可以实现将page cache缓存内容进行落盘,从而保证不丢失(redis aof可以设置持久化机制,通常设置每秒落盘一次,这里落盘也是fsync系统调用)。为了性能考虑,应用程序不可能每写入一点数据就调用fsync,fsync也是有性能损耗的。

四丶IO多路复用 select/poll/epoll

上面我们聊到了IO多路复用解决了什么问题,以及NIO Selector的基本使用,但是没有探究在操作系统层面是如何实现的,下面来学习一下。

1.select系统调用

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout)
  • nfds: 最大的文件描述符+1,代表监听这一组描述符(为什么要+1?因为除了当前最大描述符之外,还有可能有新的fd连接上来)
  • fd_set: 是一个位图集合, 对于同一个文件描述符,可以监听不同的事件
  • readfds:文件描述符“可读”事件
  • writefds:文件描述符“可写”事件
  • exceptfds:文件描述符“异常”事件,一般内核用的,实际编程很少使用
  • timeout:超时时间:0是立即返回,-1是一直阻塞,如果大于0,则达到设置值的微秒数即返回
  • 返回值: 所监听的所有监听集合中满足条件的总数(满足条件的读、写、异常事件的总数),出错时返回-1,并设置errno。如果超时时间触发,则返回0

select 其实就是把NIO中用户态要遍历的fd数组拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据依旧需要通过系统调用,切换到内核态进行。

可以看到select依赖了很多位图参数,系统调用完后需要用户程序遍历一次位图才能直到哪一个fd具备了io事件,并且这个位图大小最大为1024,导致select用起来需要很多位操作并且最多只能支持1024路IO。

2.poll系统调用

int poll(struct pollfd *fds, nfds_t nfds/*最大监听的文件描述符个数*/, int timeout/*最大监听的文件描述符个数*/);

其中pollfd为:

struct pollfd {
      int   fd;         /* file descriptor */
      short events;     /* requested events */
      short revents;    /* returned events */
};

poll可以看作升级版select,它突破了1024个文件描述符的限制,并且poll函数的监听和返回是分开的,简化了代码实现。

虽然poll不需要遍历所有的文件描述符了,只需要遍历加入数组中的描述符,范围缩小了很多,但缺点仍然是需要遍历,当加入数组描述符很多,但是存在事件的fd很少,这个遍历操作还是有点不划算的。

3.epoll系统调用

在linux环境下,java nio中的selector就是基于epoll实现的。

3.1 epoll_create

int epoll_create(int size)
    //返回一个fd
    //传入大小作为参考值

epoll_create返回一个特殊的文件描述符,它代表红黑树的根节点。size则是树的大小,它代表你将监听多少个文件描述符。epoll_create将按照传入的大小,构造出一棵大小为size的红黑树。

3.2 epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epfd 是epoll_create的返回值,也就说红黑树的根节点
// op 表示操作,比如增加,修改,删除
//fd 是需要增加,修改,删除的文件描述符
// struct epoll_event *event 是一个结构体,如下
struct epoll_event {
               uint32_t     events;      /* Epoll events 读事件or写事件,or 异常事件*/
               epoll_data_t data;        /* User data variable */
           };
 typedef union epoll_data {
               void        *ptr;
               int          fd;//代表一个文件描述符,初始化的时候传入需要监听的文件描述符,当监听返回时,此处会传出一个有事件发生的文件描述符,因此,无需我们遍历得到结果了
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

用来操作epoll句柄,可以使用该函数往红黑树里增加文件描述符,修改文件描述符,和删除文件描述符。

3.3 epoll_wait

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
//epfd 是epoll_create的返回值,也就说红黑树的根节点
// struct epoll_event *events 是一个数组,返回的所有触发了事件的文件描述符集合
//maxevents代表这个数组的大小
//timeout 0代表立即返回,-1代表永久阻塞,如果大于0,则代表超时等待毫秒数

3.4 水平触发,边缘触发

epoll有两种触发方式,分别为水平触发边沿触发

  • 水平触发

    只要有数据处于就绪状态,那么可读事件就会一直触发。

    举个例子,假设客户端一次性发来了4K数据 ,但是服务器recv函数定义的buffer大小仅为1024字节,那么一次肯定是不能将所有数据都读取完的,这时候就会继续触发可读事件,直到所有数据都处理完成。

    epoll默认的触发方式就是水平触发。

  • 边缘触发

    只有数据发送过来的时候会触发一次,即使数据没有读取完,也不会继续触发。

  • 触发方式的设置:

    水平触发和边沿触发在内核里 使用两个bit mask区分,分别为:

    • EPOLLLT 水平 触发
    • EPOLLET 边沿触发

    需要在注册事件的时候将其与需要注册的事件做一个位或运算即可:

    ev.events = EPOLLIN;    //LT
    ev.events = EPOLLIN | EPOLLET;   //ET
    

4.总结

select函数需要一次性传入所有需要监控的连接(在内核中是FD),并在内核中对这些FD进行持续的扫描。当发现其中有FD不老实时,就会通知应用程序有客户端事件发生了, 上层应用接到通知后,就只能自己再去遍历所有的FD,寻找有事件发生的连接,然后进行业务处理。
但是select受限于操作系统,扫描的FD个数是受限的。

于是出现了Poll函数,解决了slelect文件描述符受限的问题。但是,上层应用程序依然要自己去遍历所有客户端,寻找哪个客户端上有事件发 生。高并发场景下,性能依然严重受限。
于是又出现了epoll机制。

epoll机制会直接返回有事件发生的FD。这样就省掉了上层应用频繁扫描所有客户端的消耗,进一步解决多路复用的高并发问题。文章来源地址https://www.toymoban.com/news/detail-479135.html

到了这里,关于Java NIO原理 (Selector、Channel、Buffer、零拷贝、IO多路复用)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java 网络编程之NIO(selector)

                        本编文章意在循环渐进,可看最后一个就可以了                Selector selector = Selector.open();                首先channel必须是非阻塞的情况下                 channel.register(选择器,操作的类型,绑定的组件);返回的是选择键              1)Ch

    2023年04月11日
    浏览(32)
  • Java NIO (三)NIO Channel类

            前面提到,Java NIO中一个socket连接使用一个Channel来表示。从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。然而,远不止如此,Java NIO的通道可以更加细化。例如,不同的网络传输协议,在Java中都有不同的NIO Chann

    2024年01月18日
    浏览(38)
  • Java-NIO篇章(4)——Selector选择器详解

    选择器(Selector)是什么呢?选择器和通道的关系又是什么?这里详细说明,假设不用选择器,那么一个客户端请求数据传输那就需要建立一个连接,为了避免线程阻塞,那么每个客户端开辟一个线程。而学过JVM的都知道,默认每开一个线程需要栈空间内存1MB大小。如果这时

    2024年01月21日
    浏览(26)
  • Java-NIO篇章(3)——Channel通道类详解

    Java NIO中,一个socket连接使用一个Channel(通道)来表示。对应到不同的网络传输协议类型,在Java中都有不同的NIO Channel(通道) 相对应。其中最为重要的四种Channel(通道)实现: FileChannel、 SocketChannel、 ServerSocketChannel、 DatagramChannel : FileChannel 文件通道,用于文件的数据读

    2024年01月20日
    浏览(33)
  • java nio零拷贝

      零拷贝是一种计算机执行IO操作的优化技术,其核心目标是减少数据拷贝次数,从而提高系统性能。它主要体现在以下几个方面: 1. **定义与原理**:零拷贝字面上的意思包括“零”和“拷贝”。其中,“拷贝”是指数据从一个存储区域转移到另一个存储区域;“零”表示次

    2024年02月20日
    浏览(24)
  • 从Java BIO到NIO再到多路复用,看这篇就够了

    目录 从一次优化说起 IO模型分类 分类 举例 概念详解 阻塞和非阻塞 同步与异步 Java支持版本 实战 c10k问题 上代码 BIO服务端 NIO服务端​​​​​​​ 多路复用 概念 阶段一:selectpoll 阶段二epoll Java selector 后记         近期优化了一个老的网关系统,在dubbo调用接口rt100

    2024年02月08日
    浏览(33)
  • java-IO&NIO

    1.1. 阻塞 IO 模型   最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内 核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程

    2024年02月12日
    浏览(30)
  • Java 中的 IO 和 NIO

    Java IO(Input/Output)流是用于处理输入和输出数据的机制。它提供了一种标准化的方式来读取和写入数据,可以与文件、网络、标准输入输出等进行交互。 Java IO流主要分为两个流模型:字节流(Byte Stream)和字符流(Character Stream)。 字节流(Byte Stream) InputStream:字节输入流

    2024年02月10日
    浏览(30)
  • NIO核心三:Selector

    选择器提供一种选择执行已经就绪的任务的能力。selector选择器可以让单线程处理多个通道。如果程序打开了多个连接通道,每个连接的流量都比较低,可以使用Selector对通道进行管理。 1.创建Selector 2.必须将通道设置为非阻塞模式才能注册到选择器上 3.把通道注册到选择器上

    2024年03月11日
    浏览(28)
  • Java NIO(Java Non-Blocking IO:非阻塞式IO)(2)

    1.NIO非阻塞网络编程相关的(Selector、SelectionKey、ServerScoketChannel和SocketChannel)关系梳理图: 说明: ①.当客户端连接时,会通过服务器端ServerSocketChannel得到/生成对应的SocketChannel; ②.通过register(Selector sel,int ops)方法将SocketChannel注册到Selector上(一个Selector上可以注册多个SocketChannel); ③

    2024年02月02日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包