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

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

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

linux
提供了sendfile
,也就是零拷贝技术- 代码层面,如果说基于零拷贝技术来读取磁盘文件,同时把读取到的数据通过
Socket
发送出去的话,如Kafka
源码中,通过transferFrom
和transferTo
两个方法,从磁盘上读取文件,把数据通过网络发送出去 - 零拷贝技术,先从用户态切换到内核态,在内核态的状态下,把磁盘上的数据拷贝到内核缓冲区,同时从内核缓冲区拷贝一些
offset
和length
到Socket
缓冲区中;接着从内核态切换到用户态,从内核缓冲区直接将数据拷贝到网络协议引擎里去 - 同时从
Socket
缓冲区里拷贝一些offset
和length
到网络协议引擎里去,但是这个offset
和length
的量很少,几乎可以忽略 - 只需要
2
次切换、2
次拷贝 - 案例:
kafka
、tomcat
都使用了零拷贝的技术
二、图解 select
、poll
、epoll
IO多路复用被广泛使用的模型有select、poll 和 epoll
,IO多路复用实现的主旨思想是不再由程序自己监视客户端的连接,取而代之由内核替应用程序监视文件。其中,Selector
是select、poll 和 epoll
的包装类。
select

流程(这里拿读文件描述符举例,rset
是读文件描述符集合)
- 创建
Socket
服务端,然后设置Socket
可以监听的fd
的个数 - 将
rset
从用户态全量拷贝到内核态 - 由内核态判断哪个
fd
有数据 - 没有
fd
就绪时,内核会一直阻塞判断 - 有
fd
就绪时,内核就将有数据的fd
对应的rset
上的那一位置位,标明有数据来了 - 此时,
select
函数不再阻塞,而是返回 - 然后结合
rset
遍历fd_set
集合,判断哪个fd
被置位了 - 读取被置位的
fd
中的数据 - 最后进行相应的逻辑处理
select
简介和弊端
- 使用
select
模型处理IO
请求和同步阻塞模型没有太大的区别,甚至还要监听Socket
,以及调用select
函数的额外操作,效率更差,但是select
模型可以在一个线程内同时处理多个Socket
的IO
请求,即如果处理的连接数不是很高的话,使用select
的web server
不一定比multi-threadin
的https://qimok.cn g + blocking IOweb server
性能更好,可能延迟还更大,但select
的优势并不是单个连接能处理得更快,而是在于能处理更多的连接 - 每次调用
select
函数,都需要将fd_set
集合从用户态拷贝到内核态,如果fd_set
集合很大时,拷贝的开销就会很大 select
采用的是轮询模型,每次调用select
都需要在内核遍历传递进来的所有fd_set
,如果fd_set
集合很大时,遍历的开销就会很大- 为了减少数据拷贝以及轮询
fd_set
集合带来的性能损耗,内核对被监控的fd_set
集合大小做了限制,通过宏FD_SETSIZE
控制,一般32位
平台为1024
,64位
平台为2048
。单纯改变进程打开的文件描述符个数并不能改变select
监听的文件个数。 - 摆脱不了用户态到内核态切换的性能开销
rset
不可重用- 遍历
fd_set
集合判断哪个fd
被置位时,需要以O(n)
的时间复杂度去遍历
poll

流程(这里拿读文件描述符举例)
- 创建
Socket
服务端,然后设置Socket
可以监听的fd
的个数 - 将
fd
存储到pollfd
中,如果是读事件,就将当前pollfd
的events
设为POLLIN
- 将
pollfds
(本身是个数组)从用户态拷贝到内核态(select
使用的是bitmap
类型的rset
) poll
是个阻塞函数,同select
一样,阻塞的去轮询pollfds
数组,看看有没有元素被置位- 当一个或多个
fd
就绪的时候,对应的pollfd
的revents
元素会被置位为POLLIN
(select
置位的是bitmap
,如果bitmap
被置位,则无法被重用) - 此时,
poll
函数不再阻塞,而是返回 - 然后遍历
pollfds
数组,判断哪个fd
被置位了 - 将被置位的
pollfd
的revents
恢复为 0,即恢复为默认值(这里可以保证pollfd
能被重复使用) - 读取被置位的
fd
中的数据 - 最后进行相应的逻辑处理
poll
简介和弊端
poll
本质上和select
没有区别七墨博客 ,只是它没有最大连接数的限制,原因是它是基于数组(或链表)来存储的,它将用户传入的需要监视的fd
拷贝到内核空间,然后查询每个fd
对应的状态,如果是就绪状态则将其加入到等待队列中,并继续遍历,如果遍历完所有fd
,没有发现就绪的fd
,则挂起当前进程,直到有就绪的fd
或者主动超时,被唤醒后它又要再次遍历fd
poll
只解决了se
监视文件描述符数据的限制,但每次调用还是需要将https://qimok.cn lectfd
从用户空间拷贝到内核空间,然后在内核空间轮询所有的描述符- 由于使用的数据结构是
pollfd
,可以监听的fd
的个数远远不止bitmap
1024的大小 - 每次置位的都是
pollfd
的revents
字段,所以pollfds
数组是可以重用的 - 摆脱不了用户态到内核态切换的性能开销
- 遍历
pollfds
数组判断哪个fd
被置位时,需要以O(n)
的时间复杂度去遍历
epoll

流程
- 创建
Socket
服务端,然后设置Socket
可以监听的fd
的个数 - 通过
epoll_create()
函数创建epfd
(可以理解为白板) - 通过
epoll_ctl()
函数注册fd
,相当于往白板上写fd-events
- 在执行
epoll_wait()
函数时,用户态和内核态共享了一块内存空间,epfd
就在这块内存空间中 - 没有
fd
就绪时,epoll_wait
函数和select、poll
一样都是阻塞状态 - 一旦有
fd
就绪(触发EPOLLIN
事件)时,则将就绪的fd
重排到数组的最前面,然后内核就会采用类似callback
的回调机制来激活就绪的fd
,epoll_wait
函数就可以收到通知 epoll_wait
函数进行返回,这里是有返回值的,返回值是指一共有多少个fd
触发了EPOLLIN
事件(比如,这里有5
个fd
触发了EPOLLIN
事件)- 最后只需要遍历对应数组的前
5
个元素即可(所以这里的时间复杂度是O(1)
)
epoll
简介
epoll
是Linux
下IO
多路复用select、poll
的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU
利用率,因为它会复用fd
集合来传递结果,而不用迫使开发者每次等待事件之前都必须重新准备要被监听的fd
集合,另一点原因是获取事件的时候,它无须遍历整个被监听的fd
集合,只需要遍历那些被内核IO
事件异步唤醒而加入Ready
队列的描述符集合就行了(采用回调的机制,不是轮询的方式,不会随着fd
数据的增加导致效率的下降,只有活跃可用的fd
才会调用callback
函数)- 虽然表面上看
epoll
非常好,但是对于连接数少并且连接都十分活跃的情况下,select
和poll
的性能可能比epoll
好,因为epoll
是建立在大量的函数回调的基础之上 epoll
除了提供select、poll
那种IO
事件的水平触发(Level Triggered
,简称LT
,LT
是默认模式,只要这个fd
还有数据可读,每次epoll_wait
都会返回它的事件,提醒用户程序去操作)外,还提供了边缘触发(Edge Triggered
,简称ET
,ET
是高速模式,它只会提醒一次,直到下次再有数据流入之前都不会再提醒了,无论fd
中是否还有数据可读)。在ET
模式下,read
一个fd
的时候一定要把它的buffer
读光,也就是说一直读到read
的返回值小于请求值
或者EAGAIN
错误。- 摆脱了用户态到内核态切换的性能开销
- 只需要
O(1)
的事件复杂度即可
epoll
使用案例
Redis
Nginx
- 在
Linux
下,Java NIO
三、图解BIO
、NIO
、AIO
BIO
网络通信原理

NIO
网络通信原理

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

AIO
网络通信原理
- 异步非阻塞:在基于
AIO
文件的读写API
去读写磁盘文件,自己发起一个读写请求之后,交给操作系统去处理,自己就不需要管了,直到操作系统完成之后,会来回调自己的一个接口,通知自己,这个数据读好了或这个数据写完了 - read:读取数据的时候,提供给操作系统一个空
buffer
,然后自己就可以去干别的事了,即把读数据的事,交给操作系统去干了,操作系统内核将读到的数据放入buffer
中,完事后,来回调你的一个接口,将buffer
再交给你 - write:把放了数据的
buffer
交给操作系统的内核去处理,自己就可以去干别的事了,操作系统内核完成了数据的写之后,来回调你的一个接口,告诉你,你交给我的数据,我都给你写回到客户端了
BIO
、NIO
、AIO
的区别
- BIO(阻塞):同步阻塞,读写卡在那。一个客户端、一个连接、一个线程,线程太多,缺点是不能支持大规模的客户端
- NIO(轮询)
https://qimok.cn :同步非阻塞,发起一个读写请求,就可以去干别的事了,每隔一段事件去看看干完没有,总体来说,还是同步的。一个客户端、一个channel
、一个selector
多路复用轮询多个channel
,有请求来的时候,再创建一个线程专门处理这个请求,对这个请求读写完成之后,线程就释放。即一个线程加少量的线程去支撑大量的客户端 - AIO(回调):异步非阻塞,发起一个读写请求,就不管了,操作系统读写完成之后,来回调你的一个接口,通知你,这个事干完了。一个请求、一个线程,当请求来的时候,
NIO
是同步读写数据,AIO
是异步的,即工作线程不用卡在这里读/写
四、Netty
的架构原理图

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