一、基础概念
1、IO的含义
IO,Input/Output,即输入/输出。从计算机结构来看,IO描述了计算机系统和外部设备之间通讯的过程。从应用程序角度来看,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。用户进程(应用程序)想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间。我们平常开发接触最多的就是磁盘IO(读写文件)和网络IO(网络请求和响应)完整的IO过程:
(1)应用程序进程向操作系统发起IO调用请求。
(2)操作系统准备数据,把IO外部设备(一般指硬盘、socket通讯的网卡)的数据,加载到内核缓冲区。
(3)操作系统拷贝数据,即将内核缓冲区的数据,拷贝到进程缓冲区。
2、同步、异步
发送请求后,被调用者返回请求结果就是同步的,反之,调用者通过注册回调来获得结果就是异步的。
3、阻塞、非阻塞
调用者阻塞等待结果的返回,挂起当前线程,不能执行其它操作就是阻塞的;反之,调用者可以读到是否有结果,有结果就处理,没有结果就可以去执行其它操作了,就是非阻塞的。
二、IO模型
1、同步阻塞IO模型(BIO)
应用程序发起IO调用,如果内核的数据还没准备好的话,应用程序进程一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示。socket服务端单线程阻塞监听客户端连接,有连接来了就接着往下执行,然后创建一个新的线程去处理连接的读写操作。服务端除了监听的线程,还要为每个客户端连接建立一个新的线程,一个线程对应处理一个客户端连接请求,不适合高并发场景。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
2、同步非阻塞IO模型(NIO)
NIO中的N可以理解为Non-blocking,不单纯是New。NIO虽然说是非阻塞的,但是应有程序在等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的。Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。它支持面向缓冲的,基于通道的 I/O 操作方法。通过选择器Selector轮询有没有客户端连接或读写事件,如果监听到客户端连接ServerSocketChannel,就注册到Selector进行读写事件监听;如果监听到读写事件SocketChannel,进行读写操作;如果没监听到,通过while (selector.select() > 0) 判断持续监听。对于高负载、高并发的(网络)应用,应使用 NIO 。
它相对于阻塞IO,虽然大幅提升了性能,但是它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源。可以用IO多路复用模型,去解决这个问题。
3、IO多路复用模型
系统提供一系列函数(如select、poll、epoll函数),它们可以同时监控多个文件描述符fd的操作,任何一个返回内核数据就绪,应用进程再发起系统调用。
(1)IO多路复用之select
NIO模型,需要N(N>=1)次轮询系统调用,然而基于select的IO多路复用模型只需要发起一次系统调用,通过调用select函数,可以同时监控多个文件描述符fd(File Descriptor),在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起请求去读取数据。但是select有如下缺点:
- 监听的IO最大连接数有限,在Linux系统上一般为1024。
- select函数返回后,是通过遍历fdset,找到就绪的描述符fd。(仅知道有I/O事件发生,却不知是哪几个流,所以遍历所有流)
(2)IO多路复用之poll
与select相比,poll解决了连接数限制问题。但是,poll还是需要通过遍历fd来获取已经就绪的socket。如果同时连接的大量客户端在一时刻可能只有极少处于就绪状态,伴随着监视的fd数量的增长,效率也会线性下降。
(3)IO多路复用之epoll
epoll注册需要监听的fd,一旦某个fd就绪,内核采用回调机制,通知进程。采用监听事件回调的机制,避免了遍历文件描述符的操作。epoll明显优化了IO的执行效率,但在进程监听系统调用时,仍然是阻塞的。windows系统不支持epoll模式,linux系统优先使用epoll模式,可以通过参数改变其它模式。
4、IO模型之信号驱动模型
信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号,然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过信号通知应用进程。应用用户进程收到信号之后,立即去读取数据。但是,数据从内核复制到用户空间的缓冲区,进程还是阻塞的。
5、IO 模型之异步IO(AIO)
BIO、NIO和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此都不是真正的异步。AIO实现了IO全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO操作执行完毕。
BIO、NIO、AIO对比图:
三、应用场景
BIO适合连接数目较少且固定的架构。比较经典的应用就是阻塞socket、Java BIO。
NIO适合连接数目多,但是并发读写操作相对较少的场景。例如netty。
AIO则适合连接数目多,且并发读写操作也多的场景。
四、生活中案例
(1)烧水的案例
- 小时候我妈让我烧水,我比较笨,在水壶旁傻等,也不敢去玩(同步阻塞BIO)。
- 等长大点,知道了水在烧着,中间可以去干其它事情,时不时来看看水开了没(同步非阻塞NIO)。
- 等到我家用上了水开了会发出声音的壶,听到声音了我就知道水开了,在这期间我可以随便干自己的事情(异步非阻塞AIO)。
我在水壶旁傻等就是阻塞的,我可以去干其它事情就是非阻塞的;水壶水烧开了,自动通知我就是异步的,不自动通知我,需要我去看就是同步的。
(2)买烧鸡的案例 - 小明去吃北京烤鸭,取了号在那排队,等了一小时,然后才吃到。(同步阻塞BIO)
- 小红去吃北京烤鸭,也取了号,她一看要等挺久的,于是去逛会商场,每次逛一下,就跑回来看看,是不是轮到她了。于是最后她逛了商场,又吃到了北京烤鸭。(同步非阻塞NIO)
- 小华一样,由于他是高级会员,所以店长说,你去商场随便逛会吧,等下有位置了,我立马打电话给你。于是小华不用干巴巴坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了北京烤鸭。(异步非阻塞AIO)
排队等就是阻塞的,可以去逛商场做其它事情就是非阻塞的;烤鸭店主动电话通知就是异步的,不主动带电话通知,就是同步的。
(3)转账
同步阻塞BIO:页面Loading,等待接口返回转账是否成功。
同步非阻塞NIO:页面显示中间状态,定时任务去调用接口,看是否转账成功。
异步非阻塞AIO:页面调用接口,接口异步返回处理中状态并显示,等到转账结果出来通过WebSocket发送给前端展示。
五、其它
1、内核从磁盘加载数据到pageCahe,多个程序可以共享一个pageCache,但是对应seek不一样。lru 写pageCahe脏的页,将脏页写入磁盘,就不是脏页了,内存不够就会把非脏页淘汰掉。pageCahe优化IO性能,但是不能保证完全不丢数据。
2、硬件有缓存、内核有缓存、进程有缓存(堆内是jvm堆里的,堆外是jvm堆外)。mmap是进程直接调用内核的pageCache。效率上 on heap<off heap<mmap。
3、socket bio 监听客户端连接,监听到就新建一个线程去接受处理信息。
4、netstat -natp //查看建立的连接
lsof -p pid //查看该进程的文件描述符
nc ip 端口 //linux建立socket客户端连接
nc -l ip 端口 //linux建立socket服务端端连接
参考博客:
https://blog.51cto.com/u_12897/11291128
https://blog.csdn.net/u010365819/article/details/119042870
https://blog.csdn.net/weixin_68074170/article/details/140724151
https://blog.csdn.net/qq_43842093/article/details/132769520
www.kegel.com/c10k.html