引言
文章相关代码已收录至我的github,欢迎star:
lsylulu/myarticle
有一说一,无套路系统学习下Linux的五种IO模型,顺便献上我的陈年老笔记~
文章导读
- 基本概念(相关系统调用函数,同步&异步,阻塞&非阻塞)
- 阻塞IO模型
- 非阻塞IO模型
- IO多路复用模型
- 信号驱动IO模型
- 异步IO模型
- Java中的BIO,NIO,AIO
一、基本概念
五种IO模型包括:
阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO
。
首先需要了解下系统调用的几个函数和基本概念。
1.1 简单介绍几个系统调用函数
由于我对于C语言不熟悉,几个系统函数参考了一些文章,如果错误欢迎指出!
recvfrom
Linux系统提供给用户用于接收网络IO的系统接口。
从套接字上接收一个消息
,可同时应用于面向连接和无连接的套接字。
如果此系统调用返回值<0,并且 errno为EWOULDBLOCK或EAGAIN(套接字已标记为非阻塞,而接收操作被阻塞或者接收超时 )时,连接正常,
阻塞
接收数据(这很关键,前4种IO模型都设计此系统调用)。
select
select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出的完成。以
数组
形式存储文件描述符,64位机器默认
2048
个。当有数据准备好时,无法感知具体是哪个流OK了,所以需要一个一个的遍历,函数的时间复杂度为
O(n)
。
poll
以
链表
形式存储文件描述符,没有长度限制。本质与select相同,函数的时间复杂度也为
O(n)
。
epoll
是基于事件驱动的,如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,函数的时间复杂度为
O(1)
。
sigaction
用于设置对信号的处理方式,也可检验对某信号的预设处理方式。Linux使用
SIGIO信号
来实现IO异步通知机制。
1.2 同步&异步
同步和异步是针对应用程序和内核交互而言的,也可理解为被
被调用者(操作系统)
的角度来说。
同步是用户进程触发IO操作并等待或轮询的去查看是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知,需要CPU支持
1.3 阻塞&非阻塞
阻塞和非阻塞是针对于进程在访问数据的时候,也可理解为
调用者(程序)
角度来说。根据IO操作的就绪状态来采取的不同的方式。
阻塞方式下读取或写入方法将一直等待,而非阻塞方式下读取或写入方法会立即返回一个状态值。
下午撸代码饿了,好久没吃KFC了,决定去
整个全家桶
,这一切都要从一个全家桶说起~
我跑去肯德基买全家桶,但是很不巧,轮到我时,全家桶
卖完了
,我只能等着新做一份 …
二、阻塞IO模型
学习过操作系统的知识后,可以知道:不管是网络IO还是磁盘IO,对于读操作而言,都是等到网络的某个数据分组到达后/数据
准备好
后,将数据
拷贝到内核空间的缓冲区中
,再从内核空间
拷贝到用户空间的缓冲区
。
有关操作系统的知识可以参考我之前写的操作系统相关文章~
拓展阅读:
https://www.cnblogs.com/sunsky303/p/8962628.html
此时我已饥渴难耐,全程
盯着
后厨,等待着一分一秒(别多想 ),终于全家桶做好了,在此期间虽然什么事也没干,但是最后能吃到全家桶,我很幸福。
此处需要一个清新的脑回路,我就是程序,我想要全家桶,于是
发起了系统调用
,而后厨加工的过程就是在做
数据准备和拷贝
工作。全家桶最终到手,数据终于从内核空间拷贝到了用户空间。
简单看下
执行流程
:
接下来发挥看图说话的专长了:阻塞IO的执行过程是进程进行
系统调用
,
等待内核
将数据准备好并复制到用户态缓冲区后,进程
放弃使用CPU
并
一直阻塞
在此,直到数据准备好。
三、
非阻塞IO模型
此时我
每隔5分钟
询问全家桶好了没,在数次盘问后,终于出炉了。在每一次盘问之前,对于程序来说是
非阻塞的
,
占用CPU资源
,可以做其他事情。
每次应用程序
询问内核
是否有数据准备好。如果就绪,就进行
拷贝
操作;如果未就绪,就
不阻塞程序
,内核直接返回未就绪的返回值,等待用户程序下一个轮询。
大致经历两个阶段:
-
等待数据阶段
:未阻塞, 用户进程需要盲等,不停的去轮询内核。 -
数据复制阶段
:阻塞,此时进行数据复制。
在这两个阶段中,用户进程只有在数据复制阶段被阻塞了,而等待数据阶段没有阻塞,但是用户进程需要盲等,不停地轮询内核,看数据是否准备好。
四、IO多路复用模型
排了很长的队,终于轮到我支付后,拿到了一张小票,上面有
号次
。当全家桶出炉后,会喊相应的号次来取。KFC营业员小姐姐打小票出号次的动作相当于操作系统
多开了个线程
,专门接收客户端的连接。我只关注叫到的是不是我的号,因此程序还需在服务端
注册我想监听的事件
类型。
多路复用一般都是用于网络IO,服务端与多个客户端的建立连接。下面是神奇的多路复用执行过程:
相比于阻塞IO模型,多路复用只是多了一个
select/poll/epoll函数
。select函数会不断地轮询自己所负责的文件描述符/套接字的到达状态,当某个套接字就绪时,就对这个套接字进行处理。select负责
轮询等待
,recvfrom负责
拷贝
。当用户进程调用该select,select会监听所有注册好的IO,如果所有IO都没注册好,调用进程就阻塞。
对于客户端来说,一般
感受不到阻塞
,因为请求来了,可以用放到线程池里执行;但对于执行select的操作系统而言,是阻塞的,需要阻塞地
等待某个套接字变为可读
。
IO多路复用其实是阻塞在select,poll,epoll这类系统调用上的,复用的是执行select,poll,epoll的线程。
五、信号驱动IO模型
跑KFC嫌麻烦,刚好有个会员,直接点份外卖,美滋滋。当外卖送达时,会收到取餐电话(信号)。在收到取餐电话之前,我可以愉快地吃鸡或者学习。
当数据报准备好的时候,内核会向应用程序
发送一个信号
,进程对信号进行
捕捉
,并且调用信号处理函数来获取数据报。
该模型也分为两个阶段:
-
数据准备阶段
:未阻塞,当数据准备完成之后,会主动的通知用户进程数据已经准备完成,对用户进程做一个回调。 -
数据拷贝阶段
:阻塞用户进程,等待数据拷贝。
六、异步IO模型
此时科技的发展已经超乎想象了,外卖机器人将全家桶自动送达并
转换成营养
快速注入我的体内,同时还能得到口感的满足。注入结束后,机器人会提醒我注入完毕。在这个期间我可以放心大胆的玩,甚至注射的时候也
不需要停下来
!
类比一下,就是用户进程发起系统调用后,立刻就可以开始去做其他的事情,然后直到I/O
数据准备好并复制完成后
,内核会给用户进程
发送通知
,告诉用户进程操作
已经完成
了。
特点:
-
异步I/O执行的两个阶段
都不会阻塞读写操作,
由内核完成。 -
完成后内核将数据放到指定的缓冲区,
通知
应用程序来取。
七、Java中的BIO,NIO,AIO
操作系统的IO模型是底层基石,Java对于IO的操作其实就是
进一步的封装
。适配一些系统调用方法,让我们玩地更爽。
BIO,NIO,AIO涉及
相关实操代码
已收录至我的github,欢迎star~
7.1 BIO–同步阻塞的编程方式
JDK1.4之前常用的编程方式。
实现过程
:首先在服务端启动一个ServerSocket来
监听网络请求
,客户端启动Socket发起网络请求,默认情况下ServerSocket会
建立一个线程
来处理此请求,如果服务端没有线程可用,客户端则会
阻塞等待
或遭到
拒绝
,
并发效率比较低
。
服务器实现的模式是
一个连接一个线程
,若有客户端有连接请求服务端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的
线程开销
。当然,也可以通过线程池机制改善。
使用场景
BIO适用于连接数目比较小且固定的架构,对服务器资源要求高,并发局限于应用中。
7.2 NIO–同步非阻塞的编程方式
7.2.1 NIO简介
NIO 本身是基于
事件驱动
思想来完成的,当 socket 有流可读或可写入时,操作系统会相应地
通知
应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。
一个有效的请求
对应
一个线程
,当连接没有数据时,是没有工作线程来处理的。
服务器实现模式为
一个请求一个通道
,即客户端发送的连接请求都会
注册到多路复用器
上,多路复用器轮询到连接
有 I/O 请求时
才
启动
一个线程进行处
使用场景
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程复杂,JDK1.4 开始支持。
7.2.2 NIO中的几种重要角色
有缓冲区Buffer,通道Channel,多路复用器Selector。
7.2.2.1 Buffer
在NIO库中,所有数据都是用
缓冲区(用户空间缓冲区)
处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实际上是一个数组,并提供了对数据的
结构化访问
以及
维护读写位置
等信息。
Buffer的应用固定逻辑
相关的代码我会更新至github~
写操作顺序
- clear()
- put() -> 写操作
- flip() ->重置游标
- SocketChannel.write(buffer); ->将缓存数据发送到网络的另一端
- clear()
读操作顺序
- clear()
- SocketChannel.read(buffer); ->从网络中读取数据
- buffer.flip()
- buffer.get() ->读取数据
- buffer.clear()
7.2.2.2 Channel
nio中对数据的读取和写入要通过Channel,它就像
水管一样
,是一个通道。通道不同于流的地方就是通道是
双向
的,可以用于读、写和同时读写操作。
7.2.2.3 Selector
多路复用器,用于注册通道。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理
7.3 AIO–异步非阻塞编程方式
进行读写操作时,只须直接调用api的read或write方法即可。一个有效请求对应一个线程,客户端的IO请求都是OS先完成了再通知服务器应用去启动线程进行处理。
使用场景
AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK1.7 开始支持。
总结
从效率上来说,可以简单理解为阻塞IO<非阻塞IO<多路复用IO<信号驱动IO<异步IO。从同步和异步来说,
只有异步IO模型是异步的
,其他均为同步。
文章如有错误,欢迎指出~
卑微求 ,感谢大佬垂怜 ~
参考文章:
https://zhuanlan.zhihu.com/p/113467811
https://zhuanlan.zhihu.com/p/60