美文网首页
nio的演进历程

nio的演进历程

作者: hello_kd | 来源:发表于2020-02-29 11:26 被阅读0次

    本文说的NIO,是指操作系统层面的NIO,no blocking io,并不是指Java api层面的nio。操作系统并不是一开始就支持nio的,而是从传统的bio blocking io模型演变而来的,文本主要简单的介绍下nio的演变历程

    首先,一开始,操作系统只支持bio,基于bio的网络编程格式一般如下,注意,本文的例子都是用伪代码的,但是并不影响我们对io模型的理解,其中fd指的是文件描述符,也可以指的是网络socket

    int fd = socket()
    bind(fd,8888)
    listen(fd)
    for(;;) {
      //阻塞
      int fd1 = accept(fd);
      new Thread(() -> {
        //阻塞
         recv(fd1)
      })
    }
    

    阻塞IO,主要有两个方面是阻塞的,一个是接受客户端的连接accept,另一个读取连接的数据recv。这种IO模型的优点是,编程简单容易理解,理论上可以开启不限制的线程数,但是缺点也很明显,就是开启过多的线程数,会带来频繁的线程上下文切换,使CPU一直浪费在线程切换上,资源浪费。

    为了解决线程太多的问题,操作系统支持了非阻塞IO,也就是接受连接和读取数据都是非阻塞的,此时编程格式变为

    int fd = socket()
    bind(fd,8888)
    listen(fd)
    int[] fdArr = new int[]{};
    
    for(;;) {
        int fd1 = accept()
        if (fd1 > 0) {
            fdArr.add(fd1)
        }
       for(int fd : fdarr) {
             read(fd)
       }
    }
    

    由于接受连接和读取数据都是非阻塞的,因此应用程序需要不断的判断socket是否有连接到来,是否有数据可以读取。这种的优点是,可以用单线程来处理所有客户端的请求,不会像bio那样会产生大量的线程,但是缺点也同样明显,就是程序每次循环都需要调用n次(取决于文件描述符fd的个数)的系统调用,但是实际上有可能每次遍历都没有有效的数据,系统调用需要从用户态转到核心态,一样浪费大量的CPU资源。

    为了解决上述的问题,操作系统出现了多路复用器select,利用select,编程格式如下

    int fd = socket()
    bind(fd,8888)
    listen(fd)
    int[] fdArr = new int[]{};
    fdArr.add(fd)
    for(;;) {
        int[] selectArr = select(fdArr)
        if (selectArr.length > 0) {
            for (int fd : selectArr) {
                if (fd.accept()) {
                    fdArr.add(fd.accept)
                } else if (fd.isReadable()) {
                   read(fd)
                }
            }
        }
    }
    

    利用select编程,应用程序不必每次循环都调用了N次的系统调用read和一次accept。而是将批量的文件描述符传递给select系统调用,由系统帮助应用程序挑选出那些准备好IO事件的文件描述符,这样,应用程序最终获取到肯定是有数据可以处理的socket。而不是像之前那样,对每个socket都调了一次read,看有没有数据,减少了大量无用的系统调用。
    但是这种也有一个缺点,就是每次调用select都需要将所有的文件描述符都传一次给内核,频繁的进行数据拷贝。

    因此,操作系统又进一步提供了性能更高的epoll系统调用,来减少数据拷贝。也就是通过在内核开辟一块空间,用来存储需要操作系统监听的文件描述符,而不是每次调用系统调用都拷贝一次数据,此时的编程模式如下

    int fd = socket()
    bind(fd,8888)]
    listen()
    //开辟内存空间,用于存储fd
    int epfd = epoll_create(5)
    //将fd添加到epoll_create开辟的内存空间epfd中
    epoll_ctl(ADD, fd, epfd, accept)
    for(;;) {
        int[] selectfds = epoll_wait(epfd)
        for(int fd : selectfds) {
           if (fd.isAccept()) {
              epoll_ctl(ADD, fd, epfd, read)
          } else if (fd.isReadable) {
              read(fd)
          }
        }
    }
    

    在epoll的模式下,首先应用程序需要系统调用epoll_create()开辟一块内存空间,用于存储需要让操作系统监听的fd,然后将监听的fd添加到epfd中,接着在一个死循环中,调用epoll_wait(),注意这个系统调用会阻塞(可以设置超时时间),一直阻塞到epfd中有处于ready状态的fd,然后再通过epoll_wait()返回的fd列表,对列表进行循环处理,若是获得一个新连接,再将其添加到epfd中进行监听,若是有数据到达的fd,再通过系统调用read,主动获取数据。
    通过epoll模型,可以发现,不必要的系统调用和数据拷贝都不存在了,极大提高了处理网络IO的效率,但是也有一个缺点,就是对应用程序来说,编程模型变得更复杂一些。

    总结,bio到nio大致经过主要的4个发展历程,从阻塞到非阻塞,有一个需要注意的是,不管是bio还是nio,读取数据都是应用程序主动发起系统调用的,因此上述的这几种IO模型都是同步的,当然了,现在有一些操作系统是支持异步的,有兴趣的可以自己去查相关资料

    相关文章

      网友评论

          本文标题:nio的演进历程

          本文链接:https://www.haomeiwen.com/subject/ifqmfhtx.html