美文网首页
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