Linux的五种IO模型与Java的BIO、NIO、AIO

0. 前言

本文讲述Linux五种IO模型以及java的三类api调用
 有个自己的服务器会方便很多 用自己的服务器,来试验各种功能吧,一天不到三毛钱
【腾讯云】云产品限时秒杀,爆款1核2G云服务器,首年99元
【阿里云】云产品限时秒杀,爆款1核2G云服务器,首年96元

1.IO简介

之前的IO操作以一次读取和写入解读过,涉及到用户空间、内核空间等,可以看传统IO到零拷贝

1.1. 阻塞与非阻塞

  1. 阻塞:发起的请求调用者要等待返回结果,否则当前线程无法处理其他事物。
  2. 非阻塞:发起的请求调用者不要等待返回结果,也可以处理其他事物。
    阻塞和非阻塞区别在于,调用者需不需等待结果,才能处理其他事情。

1.2. 同步与异步

  1. 同步:发起一个调用后,被调用者处理完请求之前,调用不返回。
  2. 异步:发起一个调用后,调用者先返回已接到申请,然后异步去处理,处理完之后再通过事件,回调等机制来通知调用者其返回结果。

同步和异步的区别在于,被调用方的执行方式和返回时机。

通俗来讲,只有三种存在,同步阻塞(一直等待调用结果),同步非阻塞(直接做其他任务,时不时看调用结果有没有返回),异步非阻塞(直接做其他任务,被调用者回调或者事件触发结果返回)。

2. Linux五种IO模型

2.1. Linux的函数

在介绍Linux IO模型前,先介绍几个linux系统调用函数。

函数名称 用法
recvfrom 用户用于接收网络IO的系统接口。从套接字上接收一个消息,可同时应用于面向连接和无连接的套接字。如果此系统调用返回值<0,并且 errno为EWOULDBLOCK或EAGAIN(套接字已标记为非阻塞,而接收操作被阻塞或者接收超时 )时,连接正常,阻塞接收数据
select select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出的完成。以数组形式存储文件描述符,64位机器默认2048个。当有数据准备好时,无法感知具体是哪个流OK了,所以需要一个一个的遍历,函数的时间复杂度为O(n)。
poll 以链表形式存储文件描述符,没有长度限制。本质与select相同,函数的时间复杂度也为O(n)。
epoll 是基于事件驱动的,如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,函数的时间复杂度为O(1)。
sigaction 用于设置对信号的处理方式,也可检验对某信号的预设处理方式。Linux使用SIGIO信号来实现IO异步通知机制。

2.2. 五种IO模型简介

下面以一次数据交互为例,说明下五种数据模型。

  1. 阻塞IO模型:进程等待内核数据准备好,数据copy到用户空间。
  2. 非阻塞IO模型:进程无需等待内核数据准备结果,直接返回,然后通过轮询去问结果,如果结果为准备好,进程把数据copy到用户空间。
  3. 信号驱动IO模型:进程无需等待内核数据准备结果,直接返回,事件信号通知进程结果,进程把数据copy到用户空间。
  4. IO复用模型:多个进程的IO可以注册到同一个管道上。当管道中的某一个请求需要的数据准备好之后,进程把数据copy到用户空间。
  5. 异步IO模型:应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。

注意:

  1. 前边四种模型都可以看做有两个阶段:数据准备阶段、数据拷贝阶段。在数据拷贝阶段都是阻塞的,所以才可以看做是同步模型。
  2. 只有异步IO模型实现了全部非阻塞。
  3. IO复用模型是为了解决多个IO的问题。

五种IO模型流程图
Linux的五种IO模型与Java的BIO、NIO、AIO_第1张图片
Linux的五种IO模型与Java的BIO、NIO、AIO_第2张图片

2.3. 多路复用模型

其中IO多路复用有三种 select,poll,epoll

2.3.1. select

它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。

select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

select 选择句柄的时候,是遍历所有句柄,也就是说句柄有事件响应时,select需要遍历所有句柄才能获取到哪些句柄有事件通知
select支持的句柄数是有限制的, 同时只支持1024个,这个是句柄集合限制的,如果超过这个限制,很可能导致溢出,而且非常不容易发现问题, 当然可以通过修改linux的socket内核调整这个参数。

2.3.2. poll

poll没有最大文件描述符数量的限制。
select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

2.3.3. epoll

Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。最好的多路I/O就绪通知方法

epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发)

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

epoll对于句柄事件的选择不是遍历的,是事件响应的,就是句柄上事件来就马上选择出来,不需要遍历整个句柄链表,因此效率非常高,内核将句柄用红黑树保存的。

CPU密集型的服务,事件驱动并不合适

事件驱动适合于IO密集型服务,多进程或线程适合于CPU密集型服务,它们各有各的优势,并不存在谁取代谁的倾向。
Linux的五种IO模型与Java的BIO、NIO、AIO_第3张图片

3.Java IO中的三种API

举个简单的例子,小明用水壶去烧水,阻塞不阻塞是取决于小明,同步和异步是取决于水壶。
小明去烧水,要一直站在水壶前(阻塞),等水壶里的水烧开(同步)。
小明去烧水,在此期间还去扫地,刷碗(非阻塞),每隔一段时间去观察一下(同步)。
小明去烧水,水壶更新了,到了100度会报警,小明放心的去玩(非阻塞),水烧开水壶报警了(异步)。

Java中有三个模型,分别对照上边的三种形式。

  1. BIO (Blocking I/O):同步阻塞,多个线程(水壶),一个线程(水开)结束,才去执行下一个线程。
  2. NIO (New I/O):不断的轮询每个线程状态(水壶中水是否开了)。
  3. AIO ( Asynchronous I/O):每个线程执行完(水烧开)会通知我(报警)。

3.1 BIO(同步阻塞IO)

Linux的五种IO模型与Java的BIO、NIO、AIO_第4张图片BIO通信模型,通常由一个独立的 Acceptor 线程负责监听客户端的连接。
服务端会有一个while循环来接收客户端的请求(accept),客户端发起一次请求,服务端相应一次,线程销毁。一请求,一应答通信模型。
如果需要高并发,就需要创建多个线程。有多少个客户端就有多少个线程与之相对应。如果后端建立线程池,可复用线程,减少线程创建和销毁时间。

3.2. NIO(同步非阻塞)

NIO通信模型,通过通道(Channel)进行读写,通过选择器使用单个线程处理多个通道。
java的NIO有缓冲区Buffer,通道Channel,多路复用器Selector。
底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%。

3.3. AIO(异步非阻塞IO)

进行读写操作时,只须直接调用api的read或write方法即可。一个有效请求对应一个线程,客户端的IO请求都是OS先完成了再通知服务器应用去启动线程进行处理。
java7以后的AIO(NIO2)。

你可能感兴趣的:(基础学习,epoll,IO模型)