能说一说你对IO的理解吗?IO模块总结

编程


能说说IO的整个过程吗?

网络数据从接收到处理,可以分为三个阶段:

  • 网卡接收数据包:在网络编程中,我们的数据来源都是从网络中来的,那么数据包首先到达的就是我们的网卡,因此这就是我们整个过程的第一阶段。
  • 操作系统(OS)读取数据包:在网卡拿到数据以后,会由我们的OS来负责读取对应的数据到我们的OS内核缓冲区中。数据都是自下而上传递的,此时我们应用层的运行的程序还无法获取到对应的数据。
  • 将数据复制到用户空间:我们的应用想要获取对应的数据,就需要将对应的数据从OS内核缓冲区中复制到自己的用户空间内,才可以进行下一步操作。


你知道有哪些获取数据的IO模型吗?

说起IO模型,大家肯定都不陌生,或多或少都了解过一些,但是是否有思考过这些IO模型是用来干什么的呢?我自己的理解,这些IO模型都是为了获取数据,针对上面所说的IO过程,准确的来说这些IO模型都是为了从OS内核缓冲区中获取数据。(当然换一种说法,可以理解为处理连接)

下面就来看看有哪些数据模型,分别都是怎么从OS内核缓冲区中获取数据的。

阻塞IO(BIO)

阻塞IO是最经典的一种网络IO模型,在这种IO模型下,从等待网络数据到达网卡,再到OS读取数据到OS缓冲区,最后到从OS缓冲区复制到用户空间下,这个三个阶段全部都是阻塞的。从最开始发起recvfrom系统调用后,整个数据获取的过程都是阻塞的,直到系统调动返回结果,因此被称为阻塞IO。换成我们的口语表述就是:必须得一直等着,期间啥都干不了。


基于BIO模型我们进行优劣分析:

  • 优:应用程序开发非常简单,在等待数据的过程中,用户线程被挂起,基本不占用CPU资源。
  • 缺:阻塞全阶段,不够灵活,无法支撑高并发的应用场景(具体原因在下文中)。

非阻塞IO (NIO)

基于BIO的全阶段阻塞,NIO进行了优化,改成了轮询第二阶段是否完成,也就是轮询OS内核缓冲区是否已经准备好了数据,等OS内核准备好了数据,再次进行阻塞调用复制对应的数据到用户空间进行处理。

具体的操作就是,在进行recvfrom系统调用后,如果OS内核缓存区中没有数据,就立即返回一个EWOULDBLOCK错误,此时用户线程拿到返回以后就可以做一些其他的事情,不至于一直阻塞在等待数据的第二阶段,因此称之为非阻塞IO。换成我们的口语表述就是:我一直问你,数据准备好了没,好了我再来拿。


基于NIO模型我们进行优劣分析:

  • 优:每次发起系统调用后如果没数据就立即返回,用户线程不会阻塞,实时性比较好。
  • 缺:不断轮询内核,占用大量的CPU资源,效率低下。

多路复用IO

那么,我们再基于NIO模型,进行优化。如何避免该模型下的不断轮询等待问题呢?在Linux系统中,对应的系统调用引入了select/poll系统调用,由该系统调用,可以监控多个文件描述符,一旦某个文件描述符就绪(数据准备好),内核能够将就绪状态返回给应用程序,然后应用程序根据就绪状态发起对应的IO系统调用即可。

换句话来说,就是我们的用户进程不用去像NIO模型那样去轮询内核缓冲区了,交给对应的poll/select系统调用去做,等准备好了之后,返回了对应的数据就绪可读条件,我们应用线程再去复制对应的数据到用户空间中进行操作。我们应用程序的N多数据请求,都可以交给一个select/poll去处理,因此被称之为多路复用IO。换成我们的口语表述就是:你(select/poll)帮我看着下,有了数据通知下我,我再去拿,我等着你。


基于该模型我们进行优劣分析:

  • 优:相对于BIO模型,这个模型最大的优点就是一个selector线程就可以同时处理成千上万个连接(fd),系统就不必为每个连接都创建一个线程,也不必去维护它们的状态,大大的减小了系统的开销。
  • 缺:本质上,select/poll的调用也是属于阻塞式的,因此整个数据获取阶段也是阻塞的,唯一的不同就是可以一次性管理N多个连接(fd)。

补充一点关于select/poll/epoll的对比:

1、select/poll最大的缺陷就是单个进程能打开的FD(文件描述符)是有限的,默认是1024个。而epoll通过修改/etc/security/limits.conf的配置,可以解决这个问题。

2、select/poll采用的管理方式,是不断的去轮询就绪的FD,这种实现就会导致一旦FD太多,轮询的效率会变低,导致IO效率降低。而epoll采用的热点探测(实现的一个伪AIO,让就绪的FD回调),只会对就绪的socket进行操作。

3、epoll使用了mmap来避免不必要的内存复制。

4、epoll的API更加简单易用。

信号量驱动IO

这个信号量驱动的IO模型,其实和多路复用模型实现很像,不过是将select/poll的系统调用换成了一个信号量,然后数据准备好,回调通知应用程序去进行对应系统调用。不过需要注意的就是在递交信号量以后,该应用进程是可以继续执行的。

换成我们的口语表述就是:准备好了数据发个信号给我,我再去拿,我先去做其他事情了。

异步IO

而异步IO,是目前最理想的一种网络IO模型。该模型下,应用发送系统调用后,立刻就返回了,直到数据全部准备好(复制到用户空间),最后递交在aio_read中,交给应用程序。这个模型与信号量IO的区别在于:信号量IO是告诉你数据准备好了(OS缓冲区中有数据了),而AIO是告诉你,我操作完成了(已经从OS缓冲区复制到用户缓冲区了),也就是说用户进程连复制数据的阻塞都没有了。

在我看来,信号量IO与AIO都有一点控制反转的味道,因为之前都是用户进程主动去轮询或调用,而这两网络IO模型,是反过来由OS通知应用进程。

换成我们的口语表述就是:你准备好了数据,再主动交给我。


基于该模型我们进行优劣分析:

  • 优:全阶段都是异步的,用户进程只需要接收内核的操作完成事件或者注册一个IO回调函数。
  • 缺:依赖于OS底层实现,而目前Linux下的AIO模型并不完善,因此我们使用的最多的还是多路复用IO模型。


Java中的网络IO操作是什么样的呢?

说完了网络IO模型,我们再谈谈Java里是怎么去处理网络IO的呢?首先我们可以基于BIO网络模型,使用对应的Socket-API去创建一个简单的网络IO程序(代码我就不贴了),但是我们发现,基于BIO的代码实现,需要为每一个socket都创建对应的处理线程,如果有一瞬间有1w个请求,那我们就需要创建1w个线程去处理,这显然是不行的,至于为什么不行,请参见我对线程模块的总结文章。

那我们如果通过BIO的基本实现走不通,可不可以引入线程池作为一个缓冲和资源控制呢?在我们的互联网前期的确出现这种处理方式,但是这依旧无法避免BIO的本质——全阶段阻塞。

让我们想象一下,如果使用线程池去作为缓冲。假如我们线程允许创建100个线程,队列大小为2000,此时有1w个请求过来,瞬间线程打满,如果1w个请求中都是极其复杂的操作,会造成处理线程一定时间的阻塞,那么没有多余线程去处理请求,只能将多出来的请求放入队列中;由于线程池中的线程一直阻塞,慢慢队列也被打满,最终多余的请求被丢弃掉,这是无法接受的。

那我们多路复用模型的实现为什么可以呢?它也是全阶段阻塞的呀。这就是因为请求(fd)会被select/poll管理,直到请求数据准备好,返回对应的可操作条件才会让处理线程去处理对应的请求,而在请求数据没准备好之前,只需要select/poll去轮询状态就好了,这样就不需要为每一个请求都创建处理线程,也不必维护对应的线程状态,你准备好了,我再去做对应的事情就好了(你可读了,我就用线程去读,你可写了我就用线程去写);这样就大大的降低了服务端OS的开销,从而可以支持大规模的并发,另外就是Reactor模式与该IO模型的天生一对。


谈谈你认识的NIO?

在我看来,所谓的NIO这个单词,是可以分成两块来谈的,一块是NIO网络模型,也就是上文中轮询的那个模型,是存在缺陷的。另外就是Java中的New IO包,就是对多路复用模型进行了再次封装后提供的代码层面的API。

这个New IO包可以好好聊一聊,因为这个各个组件例如Selector、Channel、Buffer等等,还有他们之间的关系,还有他们有哪些Channel,分别是用来干嘛的,有什么区别,是怎么注册到Selector中的。

如果你还了解Netty,可以根据这里的New IO包说一下,这种实现有哪些缺点,然后引出Netty。再谈谈Netty的一些基本概念,例如Reactor设计模式是如何与多路复用网络模型搭配使用的,然后说说两个线程组,进一步说说对应的父子通道,Channel与Handler是如何解耦的,等等内容。本文已经很长了,所以就不额外扩展了,仅仅总结一下基本的IO概念,关于Netty的内容后面会有更新。


参考书籍:

《Netty权威指南》

《Netty、Redis、Zookeeper高并发实战》

以上是 能说一说你对IO的理解吗?IO模块总结 的全部内容, 来源链接: utcz.com/z/515191.html

回到顶部