Java 开发

图解 IO

言七墨 · 11月22日 · 2020年 · · · · · · · · 549次已读

一、图解普通IOmmap、零拷贝

1、如果不使用零拷贝技术,普通的IO操作在OS层面是如何执行的?

  • read:使用read读取数据的时候,会有一次用户态到内核态的切换,也就是说从用户角度切换到了内核角度去执行,这个时候基于DMA引擎把磁盘上的数据拷贝到内核缓冲区里去;接着会从内核态切换到用户态,基于CPU把内核缓冲区里的数据拷贝到用户缓冲区里去
  • write:接着调用了Socket的输出流的write方法,此时会从用户态切换到内核态,同时基于CPU把用户缓冲区的数据拷贝到Socket缓冲区里区,接着会有一个异步化的过程,基于DMA引擎从Socket缓冲区里把数据拷贝到网络协议引擎里发送出去
  • readwrite都完成之后,从内核态切换回用户态
  • 性能:从本地磁盘读取数据,到通过网络发送出去,用户态和内核态之间要发生4次切换,这是其一;其二,数据从磁盘拿出来过后,一共要经过4次拷贝;所以说,这4次切换和4次拷贝,让普通IO的操作性能很低

2、mmap(内存映射技术)为什么可以提升IO性能?

  • 把一个磁盘文件映射到内存里来,然后把映射到内存里来的数据通过Socket发送出去
  • 有一种mmap技术,也就是内存映射技术,直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于DMA引擎拷贝的,同时用户缓冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了
  • 光是这一点,就可以避免一次拷贝,但是这个过程中还是有用户态切换到内核态去进行映射拷贝,接着再从内核态切换到用户态,建立用户缓冲区和内核缓冲区的映射
  • 接着直接把内核缓冲区里的数据拷贝到Socket缓冲区里去,然后再拷贝到网络协议引擎里,发送出去就可以了,最后再切换回用户态
  • 减少一次拷贝,但是并不减少切换次数,一共是4次切换,3次拷贝
  • 案例:RocketMQ底层主要是基于mmap技术来提升磁盘文件七墨博客读写的

3、零拷贝技术是如何提升IO性能的?

  • linux提供了sendfile,也就是零拷贝技术
  • 代码层面,如果说基于零拷贝技术来读取磁盘文件,同时把读取到的数据通过Socket发送出去的话,如Kafka源码中,通过transferFromtransferTo两个方法,从磁盘上读取文件,把数据通过网络发送出去
  • 零拷贝技术,先从用户态切换到内核态,在内核态的状态下,把磁盘上的数据拷贝到内核缓冲区,同时从内核缓冲区拷贝一些offsetlengthSocket缓冲区中;接着从内核态切换到用户态,从内核缓冲区直接将数据拷贝到网络协议引擎里去
  • 同时从Socket缓冲区里拷贝一些offsetlength到网络协议引擎里去,但是这个offsetlength的量很七墨博客少,几乎可以忽略
  • 只需要2次切换、2次拷贝
  • 案例:kafkatomcat都使用了零拷贝的技术

二、图解 selectpollepoll

IO多路复用被广泛使用的模型有select、poll 和 epoll,IO多路复用实现的主旨思想是不再由程序自己监视客户端的连接,取而代之由内核替应用程序监视文件。其中,Selectorselect、poll 和 epoll的包装类。

select

流程(这里拿读文件描述符举例,rset是读文件描述符集合)

  1. 创建Socket服务端,然后设置Socket可以监听的fd的个数
  2. rset从用户态全量拷贝到内核态
  3. 由内核态判断哪个fd有数据
  4. 没有fd就绪时,内核会一直阻塞判断
  5. fd就绪时,内核就将有数据的fd对应的rset上的那一位置位,标明有数据来了
  6. 此时,select函数不再阻塞,而是返回
  7. 然后结合rset遍历fd_set集合,判断哪个fd被置位了
  8. 读取被置位的fd中的数据
  9. 最后进行相应的逻辑处理

select简介和弊端

  • 使用select模型处理IO请求和同步阻塞模型没有太大的区别,甚至还要监听Socket,以及调用select函数的额外操作,效率更差,但是select模型可以在一个线程内同时处理多个SocketIO请求,即如果处理的连接数不是很高的话,使用selectweb server不一定比multi-threading + blocking IOweb server性能更好,可能延迟还更大,但select的优势并不是单个连接能处理得更快,而是在于能处理更多的连接
  • 每次调用select函数,都需要将fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,拷贝的开销就会很大
  • select采用的是轮询模型,每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,遍历的开销就会很大
  • 为了减少数据拷贝以及轮询fd_set集合带来的性能损耗,内核对被监控的fd_set集合大小做了限制,通过宏FD_SETSIZE控制,一般32位平台为102464位平台为2048。单纯改变进程打开的文件描述符个数并不能改变select监听的文件个数。
  • 摆脱不了用户态到内核态切换的性能开销
  • rset不可重用
  • 遍历fd_set集合判断哪个fd被置位时,需要以O(n)的时间复杂度去遍历

poll

流程(这里拿读文件描述符举例)

  1. 创建Socket服务端,然后设置Socket可以监听的fd的个数
  2. fd存储到pollfd中,如果是读事件,就将当前pollfdevents设为POLLIN
  3. pollfds(本身是个数组)从用户态拷贝到内核态(select使用的是bitmap类型的rset
  4. poll是个阻塞函数,同select一样,阻塞的去轮询pollfds数组,看看有没有元素被置位
  5. 当一个或多个fd就绪的时候,对应的pollfdrevents元素会被置位为POLLINselect置位的是bitmap,如果bitmap被置位,则无法被重用)
  6. 此时,poll函数不再阻塞,而是返回
  7. 然后遍历pollfds数组,判断哪个fd被置位了
  8. 将被置位的pollfdrevents恢复为 0,即恢复为默认值(这里可以保证pollfd能被重复使用)
  9. 读取被置位的fd中的数据
  10. 最后进行相应的逻辑处理

poll简介和弊端

  • poll本质上和select没有区别,只是它没有最大连接数的限制,原因是它是基于数组(或链表)来存储的,它将用户传入的需要监视的fd拷贝到内核空间,然后查询每个fd对应的状态,如果是就绪状态则将其加入到等待队列中,并继续遍历,如果遍历完所有fd,没有发现就绪的fd,则挂起当前进程,直到有就绪的fd或者主动超时,被唤醒后它又要再次遍历fd
  • poll只解决了select监视文件描述符数据的限制,但每次调用还是需要将fd从用户空间拷贝到内核空间,然后在内核空间轮询所有的描述符
  • 由于使用的数据结构是pollfd,可以监听的fd的个数远远不止bitmap1024的大小
  • 每次置位的都是pollfdrevents字段,所以pollfds数组是可以重用的
  • 摆脱不了用户态到内核态切换的性能开销
  • 遍历pollfds数组判断哪个fd被置位时,需要以O(n)的时间复杂度去遍历

epoll

流程

  1. 创建Socket服务端,然后设置Socket可以监听的fd的个数
  2. 通过epoll_create()函数创建epfd(可以理解为白板
  3. 通过epoll_ctl()函数注册fd,相当于往白板上写fd-events
  4. 在执行epoll_wait()函数时,用户态和内核态共享了一块内存空间,epfd就在这块内存空间中
  5. 没有fd就绪时,epoll_wait函数和select、poll一样都是阻塞状态
  6. 一旦有fd就绪(触发EPOLLIN事件)时,则将就绪的fd重排到数组的最前面,然后内核就会采用类似callback回调机制来激活就绪的fdepoll_wait函数就七墨博客可以收到通知
  7. epoll_wait函数进行返回,这里是有返回值的,返回值是指一共有多少个fd触发了EPOLLIN事件(比如,这里有5fd触发了EPOLLIN事件)
  8. 最后只需要遍历对应数组的前5个元素即可(所以这里的时间复杂度是O(1)

epoll简介

  • epollLinuxIO多路复用select、poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用fd集合来传递结果,而不用迫使开发者每次等待事件之前都必须重新准备要被监听的fd集合,另一点原因是获取事件的时候,它无须遍历整个被监听的fd集合,只需要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了(采用回调的机制,不是轮询的方式,不会随着fd数据的增加导致效率的下降,只有活跃可用的fd才会调用callback函数)
  • 虽然表面上看epoll非常好,但是对于连接数少并且连接都十分活跃的情况下,selectpoll的性能可能比epoll好,因为epoll是建立在大量的函数回调的基础之上
  • epoll除了提供select、poll那种IO事件的水平触发Level Triggered,简称LTLT是默认模式,只要这个fd还有数据可读,每次epoll_wait都会返回https://qimok.cn它的事件,提醒用户程序去操作)外,还提供了边缘触发Edge Triggered,简称ETET是高速模式,它只会提醒一次,直到下次再有数据流入之前都不会再提醒了,无论fd中是否还有数据可读)。在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值或者EAGAIN错误。
  • 摆脱了用户态到内核态切换的性能开销
  • 只需要O(1)的事件复杂度即可

epoll使用案例

  • Redis
  • Nginx
  • Linux下,Java NIO

三、图解BIONIOAIO

BIO网络通信原理言七墨

NIO网络通信原理

NIO网络通信原理(基于多线程优化)

AIO网络通信原理

  • 异步非阻塞:在基于AIO文件的读写API去读写磁盘文件,自己发起一个读写请求之后,交给操作系统去处理,自己就不需要管了,直到操作系统完成之后,会来回调自己的一个接口,通知自己,这个数据读好了或这个数据写完了
  • read:读取数据的时候,提供给操作系统一个空buffer,然后自己就可以去干别的事了,即把读数据的事,交给操作系统去干了,操作系统内核将读到的数据放入buffer中,完事后,来回调你的一个接口,将buffer再交给你
  • write:把放了数据的buffer交给操作系统的内核去处理,自己就可以去干别的事了,操作系统内核完成了数据的写之后,来回调你的一个接口,告诉你,你交给我的数据,我都给你写回到客户端了 

BIONIOAIO的区别

  • BIO(阻塞):同步阻塞,读写卡在那。一个客户端、一个连接、一个线程,线程太多,缺点是不能支持大规模的客户端
  • NIO(轮询):同步非阻塞,发起一个读写请求,就可以去干别的事了,每隔一段事件去看看干完没有,总体来说,还是同步的。一个客户端、一个channel、一个selector多路复用轮询多个channel,有请求来的时候,再创建一个线程专门处理这个请求,对这个请求读写完成之后,线程就释放。即一个线程加少量的线程去支撑大量的客户端
  • AIO(回调):异步非阻塞,发起一个读写请求,就不管了,操作系统读写完成之后,来回调你的一个接口,通知你,这个事干完了。一个请求、一个线程,当请求来的时候,NIO是同步读写数据,AIO是异步的,即工作线程不用卡在这里读/写

四、Netty的架构原理图

1 条回应
  1. Morcat2020-11-28 · 0:19

    写的真的不错!!有学到!