关于nio:Netty网络编程NIO与零拷贝

41次阅读

共计 4119 个字符,预计需要花费 11 分钟才能阅读完成。

1. 什么是 DMA

2. 什么是用户态和内核态

3. 一般 BIO 的拷贝流程剖析

4.mmap 零碎函数

5.sendFile 零碎函数(零拷贝)

6.java 堆外内存如何回收

1. 什么是 DMA

DMA(Direct Memory Access 间接存储器拜访), 咱们先从一张图来理解一下 DMA 是一个什么安装。

假如在什么 没有 DMA 的状况下 ,如果 CPU 想从内存里读取数据并发送到网卡中,在读的过程中,咱们能够晓得:
1.1)CPU 的速度最快
1.2) 当 CPU 在 内存中读取数据 的时候,读取的速度 瓶颈在于内存的读写速度
1.3) 当 CPU 实现读取,将数据写入网卡的时候 ,写入的速度 瓶颈在于网卡的速度
1.4)CPU 在读写的时候,是无奈做其它事件的。

这个时候咱们就能够得出结论:

1.5)cpu 的速度 取决于这一系列操作环上 最慢 的那一个。
1.6)cpu 利用率极低 ,大部分工夫都在 期待 IO

此时如果有了DMA,那么咱们的读写就会变得和如图一样:

CPU 只须要把 读写工作委托给 DMA,帮助 CPU 搬运数据,这些操作都由 DMA 主动执行 ,而 不须要依赖于 CPU 的大量中断负载,此时 cpu 就能够去做其它的事件了。

2. 什么是用户态和内核态
其实最初咱们的服务器程序,都是要在 linux 上运行的,Linux 依据命令的重要水平 ,也分为 不同的权限 。Linux 操作系统就将权限分成了 2 个等级,别离就是 用户态 和内核态

用户态:
用户态的过程可能拜访的资源就有极大的限度,有些指令操作无关痛痒,随便执行都没事,大部分都是属于用户态的。

内核态:
运行在内核态的过程能够“随心所欲”,能够间接调用操作系统内核函数。

比方:咱们 调用 malloc 函数申请动态内存 ,此时 cpu 就要 从用户态切换到内核态,调用操作系统底层函数来申请空间。

3. 一般 BIO 的拷贝流程剖析

咱们来看一下 一般 IO 的拷贝流程

咱们来看这一段代码:

咱们先从服务器上读取了一个文件,而后通过连贯和流传输到申请客户端上,咱们能够看到大抵的申请流程是这样的:

当程序或者操作者对 CPU 收回指令,这些指令和数据暂存在内存里,在 CPU 闲暇时传送给 CPU,CPU 解决后把后果输入到输出设备上

3.1)用户态程序接到申请,要从磁盘上读取文件,切换到内核态 ,这里是第 1 次用户态内核态切换
3.2)当要读取的文件通过 DMA 复制到内核缓冲区的时候,咱们还要把这些数据传送给 CPU,CPU 之后再把这些数据送到输出设备上,这里是第 1 次 cpu 拷贝
3.3)当内核态程序数据读取结束, 切换回用户态 ,这里是第 2 次内核态用户态切换
3.4)当程序创立一个缓冲区, 并将数据写入 socket 缓冲区,这里是第 3 次用户态内核态切换
3.5)此时 cpu 要把数据拷贝到 socket 缓冲区,这里是第 2 次 cpu 拷贝
3.6)实现所有操作之后,应用程序从内核态切换回用户态,继续执行后续操作(程序到此为止)。这里是第 4 次用户态内核态切换

此时咱们能够看出,传统的 IO 拷贝流程,经验了 4 次用户态和内核态的切换,进行了 2 次 cpu 复制,性能消耗微小,咱们有没有更节俭资源的做法呢?

4.mmap 零碎函数

linux 的底层内核函数 mmap 函数对底层进行了一个优化:

4.1)用户态程序接到申请,要从磁盘上读取文件,切换到内核态,这里是 第 1 次用户态内核态切换
4.2)当要读取的文件通过 DMA 复制到内核缓冲区实现,此时内核缓冲区,用户数据缓冲区共享一块物理内存空间,这里就 无需 cpu 拷贝到用户空间中
4.3)此时读取文件结束,用户切换回用户态,这是第 2 次用户态内核态切换
4.4)申请一块缓冲区,须要调用内核函数,这是第 3 次用户态内核态切换
4.5)内核态通过 cpu 复制,将共享空间的数据内容拷贝到 socket 缓冲区中,这是 第 1 次 cpu 拷贝
4.6)实现所有操作之后,应用程序从内核态切换回用户态,继续执行后续操作(程序到此为止)。这里是第 4 次用户态内核态切换

咱们能够看出,mmap 函数少了一次 cpu 复制,对于空间的利用率进步了,不过还是须要 4 次用户态和内核态的切换

5.sendFile 零碎函数(零拷贝)

零拷贝:指的是 没有 cpu 拷贝,数据还是须要通过DMA 拷贝到内存中,再发送进来的

4.1)用户态程序接到申请,要从磁盘上读取文件,切换到内核态,这里是第 1 次用户态内核态切换。
4.2)当数据通过 DMA 复制进入内核缓冲区并且实现,咱们还是通过 cpu 复制把数据复制到 socket 缓冲区,不过这里的cpu 复制只复制很大量的内容,能够简直忽略不计。

4.3)此时数据通过 DMA 复制发送给目的地。
4.4)程序切换回用户态,这是第 2 次用户态内核态切换

咱们发现,sendFile 零碎函数,只须要两次用户态到内核态的切换,而且一次 cpu 复制都不须要,大大节约了资源。

6.java 堆外内存如何回收

介绍了零拷贝技术,其实 Netty 底层是应用堆外内存来实现零拷贝技术的 ,api:ByteBuffer.allocateDirect(), 这条命令间接在堆外内存开拓了一块空间 ,咱们都晓得 GC 是收集堆内存垃圾的,那 堆外内存又是如何收集的呢

堆外内存的劣势:
堆外内存的劣势在于 IO 上,java 在 应用 socket 发送数据的时候 ,如果 应用堆外内存 ,就能够 间接应用堆外内存往 socket 上发送数据 ,就 节俭了先把堆外数据拷贝到堆内数据的开销。

咱们先来看看 ByteBuffer.allocateDirect()的源码:

咱们能够看出,java 应用 unsafe 类来调配了一块堆外内存

那么堆外内存是如何回收的呢?咱们来看这样一行代码:

cleaner 就是用来回收堆外内存的,然而它是如何工作的呢?咱们认真钻研一下 cleaner 这个类,它是一个链表构造:

通过 create(Object,Runnable)办法创立 cleaner 对象,调用本身的 add 办法,将其退出链表中。

clean 有个重要的 clean 办法,它首先将对象从本身链表中删除:

而后执行 this.thunk 的 run 办法,thunk 就是由创立的时候传入的 Runnable 函数:

能够看出,run 办法是一个开释堆外内存的函数。

逻辑咱们曾经梳理完,然而 JVM 如何开释其占用的堆外内存呢 如何跟 Cleaner 关联起来

首先,Cleaner 继承了 PhantomReference(虚援用),对于强脆弱虚援用,在后面的博客曾经赘述过:深刻了解 JVM(八)——强脆弱虚援用

简略地再介绍一下虚援用,当 GC 某个对象的时候,如果此对象上有虚援用,会将其退出 PhantomReference 退出到 ReferenceQueue 队列。

Cleaner 继承 PhantomReference,而 PhantomReference 又继承 Reference,Reference 初始化的时候,会运行一个动态代码块:

咱们能够看出,ReferenceHandler 作为一个优先级比拟高的守护线程被启动了。

在看他的解决逻辑之前,咱们先理解一下对象的四种状态;

  • Active:激活。创立 ref 对象时就是激活状态
  • Pending:期待入援用队列。所对应的援用被 GC,就要入队。
  • Enqueued:入队状态。

    • 如果指定了 refQueue 生产 pending 挪动到 enqueued 状态。refQueue.poll 时进入生效状态
    • 如果没有指定 refQueue,间接到生效状态。
  • Inactive:生效

接下来咱们能够看业务逻辑了:

这是一个死循环,咱们再往里点:

    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            // 可能有多线程对一个援用队列操作,所以要加锁
            synchronized (lock) {
                  // 如果以后对象是 期待入援用队列 的状态
                if (pending != null) {
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    // 转化为 clean 对象
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    // 解除援用
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                     // 如果没有,期待唤醒
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    if (waitForNotify) {lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }

        // Fast path for cleaners
        // 革除内存
        if (c != null) {c.clean();
            return true;
        }

        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

咱们能够得出:
1)当对象状态是 Pending 的时候,就会进入 if,将这个对象转化为 clean 对象,并将这个援用置空
2)进行 clean 的垃圾收集
3)这个线程始终在后盾启动,如果有援用,就会唤醒该线程。

正文完
 0