五种I/O模型和Java NIO源码分析

 最近在学习Java网络编程和Netty相关的知识,了解到Netty是NIO模式的网络框架,可是提供了不一样的Channel来支持不一样模式的网络通讯处理,包括同步、异步、阻塞和非阻塞。学习要从基础开始,因此咱们就要先了解一下相关的基础概念和Java原生的NIO。这里,就将最近我学习的知识总结一下,以供你们了解。linux

 为了节约你的时间,本文主要内容以下:编程

  • 异步,阻塞的概念
  • 操做系统I/O的类型
  • Java NIO的底层实现

异步,同步,阻塞,非阻塞

同步和异步关注的是消息通讯机制,所谓同步就是调用者进行调用后,在没有获得结果以前,该调用一直不会返回,可是一旦调用返回,就获得了返回值,同步就是指调用者主动等待调用结果;而异步则相反,执行调用以后直接返回,因此可能没有返回值,等到有返回值时,由被调用者经过状态,通知来通知调用者.异步就是指被调用者来通知调用者调用结果就绪*.*因此,两者在消息通讯机制上有所不一样,一个是调用者检查调用结果是否就绪,一个是被调用者通知调用者结果就绪数组

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指在调用结果返回以前,当前线程会被挂起,调用线程只有在获得结果以后才会继续执行.非阻塞调用是指在不能马上获得结构以前,调用线程不会被挂起,仍是能够执行其余事情.bash

 两组概念相互组合就有四种状况,分别是同步阻塞,同步非阻塞,异步阻塞,异步非阻塞.咱们来举个例子来分别类比上诉四种状况.网络

 好比你要从网上下载一个1G的文件,按下下载按钮以后,若是你一直在电脑旁边,等待下载结束,这种状况就是同步阻塞;若是你不须要一直呆在电脑旁边,你能够去看一会书,可是你仍是隔一段时间来查看一下下载进度,这种状况就是同步非阻塞;若是你一直在电脑旁边,可是下载器在下载结束以后会响起音乐来提醒你,这就是异步阻塞;可是若是你不呆在电脑旁边,去看书,下载器下载结束后响起音乐来提醒你,那么这种状况就是异步非阻塞.app

Unix的I/O类型

 知道上述两组概念以后,咱们来看一下Unix下可用的5种I/O模型:框架

  • 阻塞I/O(bloking IO)
  • 非阻塞I/O(nonblocking IO)
  • 多路复用I/O(IO multiplexing)
  • 信号驱动I/O(signal driven IO)
  • 异步I/O(asynchronous IO)

 前4种都是同步,只有最后一种是异步I/O.须要注意的是***Java NIO依赖于Unix系统的多路复用I/O,对于I/O操做来讲,它是同步I/O,可是对于编程模型来讲,它是异步网络调用***.下面咱们就以系统read的调用来介绍不一样的I/O类型.异步

 当一个read发生时,它会经历两个阶段:socket

  • 1 等待数据准备
  • 2 将数据从内核内存空间拷贝到进程内存空间中

 不一样的I/O类型,在这两个阶段中有不一样的行为.可是因为这块内容比较多,并且多为表述性的知识,因此这里咱们只给出几张图片来解释,感受兴趣的同窗能够去具体了解一下。 async

阻塞I/O

非阻塞I/O

多路复用I/O

信号驱动

异步I/O

Java NIO的底层实现

 咱们都知道Netty经过JNI的方式提供了Native Socket Transport,为何Netty要提供本身的Native版本的NIO呢?明明Java NIO底层也是基于epoll调用(最新的版本)的.这里,咱们先不明说,你们想想可能的状况.下列的源码都来自于OpenJDK-8u40-b25版本.

open方法

 若是咱们顺着Selector.open()方法一个类一个类的找下去,很容易就发现Selector的初始化是由DefaultSelectorProvider根据不一样操做系统平台生成的不一样的SelectorProvider,对于Linux系统,它会生成EPollSelectorProvider实例,而这个实例会生成EPollSelectorImpl做为最终的Selector实现.

class EPollSelectorImpl extends SelectorImpl
{
    .....
    // The poll object
    EPollArrayWrapper pollWrapper;
    .....
    EPollSelectorImpl(SelectorProvider sp) throws IOException {
        .....
        pollWrapper = new EPollArrayWrapper();
        pollWrapper.initInterrupt(fd0, fd1);
        .....
    }
    .....
}
复制代码

EpollArrayWapper将Linux的epoll相关系统调用封装成了native方法供EpollSelectorImpl使用.

private native int epollCreate();
    private native void epollCtl(int epfd, int opcode, int fd, int events);
    private native int epollWait(long pollAddress, int numfds, long timeout,
                                 int epfd) throws IOException;
复制代码

 上述三个native方法就对应Linux下epoll相关的三个系统调用

//建立一个epoll句柄,size是这个监听的数目的最大值.
int epoll_create(int size);
//事件注册函数,告诉内核epoll监听什么类型的事件,参数是感兴趣的事件类型,回调和监听的fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待事件的产生,相似于select调用,events参数用来从内核获得事件的集合
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
复制代码

 因此,咱们会发如今EpollArrayWapper的构造函数中调用了epollCreate方法,建立了一个epoll的句柄.这样,Selector对象就算创造完毕了.

register方法

 与open相似,ServerSocketChannelregister函数底层是调用了SelectorImpl类的register方法,这个SelectorImpl就是EPollSelectorImpl的父类.

protected final SelectionKey register(AbstractSelectableChannel ch,
                                      int ops,
                                      Object attachment)
{
    if (!(ch instanceof SelChImpl))
        throw new IllegalSelectorException();
    //生成SelectorKey来存储到hashmap中,一共以后获取
    SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
    //attach用户想要存储的对象
    k.attach(attachment);
    //调用子类的implRegister方法
    synchronized (publicKeys) {
        implRegister(k);
    }
    //设置关注的option
    k.interestOps(ops);
    return k;
}
复制代码

EpollSelectorImpl的相应的方法实现以下,它调用了EPollArrayWrapperadd方法,记录下Channel所对应的fd值,而后将ski添加到keys变量中.在EPollArrayWrapper中有一个byte数组eventLow记录全部的channel的fd值.

protected void implRegister(SelectionKeyImpl ski) {
        if (closed)
            throw new ClosedSelectorException();
        SelChImpl ch = ski.channel;
        //获取Channel所对应的fd,由于在linux下socket会被看成一个文件,也会有fd
        int fd = Integer.valueOf(ch.getFDVal());
        fdToKey.put(fd, ski);
        //调用pollWrapper的add方法,将channel的fd添加到监控列表中
        pollWrapper.add(fd);
        //保存到HashSet中,keys是SelectorImpl的成员变量
        keys.add(ski);
    }
复制代码

 咱们会发现,调用register方法并无涉及到EpollArrayWrapper中的native方法epollCtl的调用,这是由于他们将这个方法的调用推迟到Select方法中去了.

Select方法

 和register方法相似,SelectorImpl中的select方法最终调用了其子类EpollSelectorImpldoSelect方法

protected int doSelect(long timeout) throws IOException {
    .....
    try {
        ....
        //调用了poll方法,底层调用了native的epollCtl和epollWait方法
        pollWrapper.poll(timeout);
    } finally {
        ....
    }
    ....
    //更新selectedKeys,为以后的selectedKeys函数作准备
    int numKeysUpdated = updateSelectedKeys();
    ....
    return numKeysUpdated;
}
复制代码

 由上述的代码,能够看到,EPollSelectorImpl先调用EPollArrayWapperpoll方法,而后在更新SelectedKeys.其中poll方法会先调用epollCtl来注册先前在register方法中保存的Channel的fd和感兴趣的事件类型,而后epollWait方法等待感兴趣事件的生成,致使线程阻塞.

int poll(long timeout) throws IOException {
    updateRegistrations(); ////先调用epollCtl,更新关注的事件类型
    ////致使阻塞,等待事件产生
    updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
    .....
    return updated;
}
复制代码

 等待关注的事件产生以后(或在等待时间超过预先设置的最大时间),epollWait函数就会返回.select函数从阻塞状态恢复.

selectedKeys方法

 咱们先来看SelectorImpl中的selectedKeys方法.

//是经过Util.ungrowableSet生成的,不能添加,只能减小
private Set<SelectionKey> publicSelectedKeys;
public Set<SelectionKey> selectedKeys() {
    ....
    return publicSelectedKeys;
}
复制代码

 很奇怪啊,怎麽直接就返回publicSelectedKeys了,难道在select函数的执行过程当中有修改过这个变量吗?

publicSelectedKeys这个对象实际上是selectedKeys变量的一份副本,你能够在SelectorImpl的构造函数中找到它们俩的关系,咱们再回头看一下selectupdateSelectedKeys方法.

private int updateSelectedKeys() {
    //更新了的keys的个数,或在说是产生的事件的个数
    int entries = pollWrapper.updated; 
    int numKeysUpdated = 0;
    for (int i=0; i<entries; i++) {
        //对应的channel的fd
        int nextFD = pollWrapper.getDescriptor(i);
        //经过fd找到对应的SelectionKey
        SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
        if (ski != null) {
            int rOps = pollWrapper.getEventOps(i);
            //更新selectedKey变量,并通知响应的channel来作响应的处理
            if (selectedKeys.contains(ski)) {
                if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
                    numKeysUpdated++;
                }
            } else {
                ski.channel.translateAndSetReadyOps(rOps, ski);
                if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
                    selectedKeys.add(ski);
                    numKeysUpdated++;
                }
            }
        }
    }
    return numKeysUpdated;
}
复制代码

后记

 看到这里,详细你们都已经了解到了NIO的底层实现了吧.这里我想在说两个问题.

 一是为何Netty本身又重新实现了一边native相关的NIO底层方法? 听听Netty的创始人是怎麽说的吧连接。由于Java的版本使用的epoll的level-triggered模式,而Netty则但愿使用edge-triggered模式,并且Java版本没有将epoll的部分配置项暴露出来,好比说TCP_CORK和SO_REUSEPORT。

 二是看这么多源码,花费这么多时间有什么做用呢?我感受若是从非功利的角度来看,那么就是纯粹的但愿了解的更多,有时候看完源码或在理解了底层原理以后,都会用一种恍然大悟的感受,好比说AQS的原理.若是从目的性的角度来看,那么就是你知道底层原理以后,你的把握性就更强了,若是出了问题,你能够更快的找出来,而且解决.除此以外,你还能够按照具体的现实状况,以源码为模板在本身造轮子,实现一个更加符合你当前需求的版本.

 后续若是有时间,我但愿好好了解一下epoll的操做系统级别的实现原理.

相关文章
相关标签/搜索