阻塞IO、非阻塞IO、同步IO、异步IO、IO多路复用模型

1、流、IO、文件描述符

文件其实就是一串二进制流,而不论是文件、socket还是pipe等进行IO操作的我们都可以称之为“流”。

通常在信息交互的过程中,我们会对流进行数据的收发操作,可以称之为IO(input/output),从流中读取数据使用输出流,往流中写入数据使用输入流。

文件描述符(file descriptor)又叫fd,是一个非负整数,打开或新建文件的时候,内核会返回一个文件描述符。对文件的读写操作都会转化为对文件描述符的操作。

2、IO的阻塞、非阻塞、同步、异步

  • 阻塞IO:执行一个操作之后,进程触发IO操作并等待并一直等待CPU处理完成,然后才执行后面的操作。

  • 非阻塞IO:执行一个IO操作之后,如果资源没准备好,直接返回一个状态而不会一直等待。

  • 同步IO:执行一个操作之后,进程触发IO操作并等待(也就是我们说的阻塞)或者轮询的去查看IO操作(也就是我们说的非阻塞)是否完成,等待结果,然后才继续执行后续的操作。

  • 异步IO:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。

阻塞和非阻塞最大的区别就是,阻塞需要等待IO操作结束才能继续向下执行,而非阻塞在资源没准备好的情况下会直接返回一个结果,一般需要添加轮询查询IO操作是否完成。

通常一个IO的读分为两部分:
- 1.数据通过网管到达内核,内核准备好数据
- 2.数据从内核中写入到用户线程

同步和异步对上面两步的执行是:
- 同步:上面两步都是由用户线程自己去读取数据,处理数据
- 异步:第二步是由内核写入的,并写到用户线程,写入完毕后通知用户线程

所以同步和异步最大的区别就是,同步需要用核线程自己去读取数据到自身线程中,而异步可以通过内核去将数据写入到用户线程。

只有用户线程在操作IO的时候完全不关心IO的过程,完全交给内核去完成IO的过程,自身只需要等待一个完成信号的过程才叫异步IO。所以拉一个子进程去轮询或者使用select、poll、epool都是同步IO。

所以阻塞和非阻塞关注点在于资源没准备好的时候是否等待,同步和异步关注点在于数据写入用户线程的过程由内核完成还是用户线程完成。

同步IO才分阻塞和非阻塞,异步IO一定是非阻塞的。所以IO就可以划分为同步阻塞IO(BIO)、同步非阻塞IO(NIO)和异步IO(AIO)三种。

3、非阻塞IO的轮询机制

Java IO中的流都是阻塞的,当一个线程调用read()时,该线程被阻塞,直到有数据可被读取。该线程在阻塞期间不能做任何其他事情。

Java NIO中的流是非阻塞的,当一个线程调用read()时,如果有数据可以读取则读取数据,如果没有数据可被读取则直接返回结果,线程可以去执行其他任务,而不会被阻塞。

同样的输出流和输入流在IO和NIO中的执行逻辑和输入流是一样的。

我们可以使用非阻塞的IO配合轮询机制实现一个线程同时管理多个流的读写过程,由于非阻塞IO在资源没有准备好的时候会直接返回,所以我们可以使用轮询机制去轮询查看所有需要管理的流,如果资源准备好了就进行读写操作,如果没有准备好,直接返回结果,继续对下一个流进行操作。

4、IO多路复用机制常用的方法

上面说到的非阻塞IO轮询机制中需要把所有的流都遍历一遍,但这样很不友好,如果所有的流都没有准备好,那么会一直在跑空循环,浪费CPU的时间片。

那么我们可以加一个中间层,由中间层来帮我们去监测流的准备状态就不需要去遍历所有的流了,这个中间层主要有select、poll和epoll三种。

select
  • 通过设置或者检查存放fd标志位的数据结构来检测所有的流
  • 当检测的流中有IO事件发生了,才会遍历所有的流,没有IO事件发生不会进行遍历
  • 能检测流的数量有上限
  • 仅仅知道有IO事件发生了,但不知道是哪个或者哪几个,所以需要遍历所有的流
  • 时间复杂度O(n)
pool
  • 本质上和select一致
  • 基于链表来存储检测的fd,没有检测上限
  • 时间复杂度O(n)
epoll
  • epoll可以理解为event pool,即事件驱动
  • epoll会把哪个流发生了怎样的IO事件告诉我们
  • 能检测的流数量没有上限
  • 时间复杂度O(k)

其中epoll比其他两种效率都要高,使用也最广泛,主要优点在于:
- 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
- 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数,即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

5、IO多路复用机制的实现方式

其实IO多路复用机制的实现方式在上面已经讲过了,这里在总结一下

忙轮询

忙轮询就是将所有的流都轮询一遍,检查是否有流已经准备就绪。

如果没有流准备就绪,那么会一直空跑循环,浪费CPU的时间片,时间复杂度O(n)。

无差别轮询

为了避免拜拜浪费CPU的时间片,可以引入一个代理,这个代理叫select/poll,这个代理可以同时观察多个流的事件。

当所有的流都没有准备好的时候,就阻塞线程;只有当存在流准备好的时候采取轮询所有的流。

事件复杂度是O(n),所以当监看的流过多的时候,性能会降低。

最小轮询方式

为了避免无差别轮询在流数量多的时候性能低的问题,还有新的一种轮询方式,最小轮询方式。

使用epoll来观察多个流,epoll只会把发生IO事件的流通知我们,我们只操作这些发生IO事件的流,时间复杂度降到O(1)。

6、IO多路复用机制的有点

IO复用机制可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立即通知相应程序进行读或写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

7、异步IO

当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。

当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。

在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。

这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

8.总结

相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。

况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。


喜欢这篇文章的朋友,欢迎扫描下图关注公众号lebronchen,第一时间收到更新内容。
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值