1、IO读写的基本原理

1.1、用户态和核态

为什么要有用户态和内核态?

由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 — 用户态和内核态。

内核态:处于核心态执行中的进程,则能访问所有的内存空间和对象(例如硬盘,网卡,cpu),且所占有的处理机是不允许被抢占的。
用户态:处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的。


用户态切换到内核态的三种情况:

(1)系统调用 :应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口,即系统调用
(2)异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常
(3)外围设备的中断:当外设完成用户的请求时,会向CPU发送中断信号。


为什么用户态和核态切换成本高?

发生系统中断时需要保护现场(保存中断时的进程状态和数据等信息),中断服务,恢复现场(恢复中断时的进程状态和数据等信息)和中断返回。

1.2、内核缓冲区和进程缓冲区

用户进程通过系统调用访问系统资源的时候,需要切换到内核态,而在系统调用结束后,cpu会从核心模式切回到用户态。为了减少底层系统的时间损耗和性能损耗,于是出现了内存缓冲区。

linux系统中操作系统内核只有一个内核缓冲区,而每个用户程序(进程)有自己独立的缓冲区,叫做进程缓冲区。用户程序的IO读写操作,在大多数情况下并没有进行实际的IO操作,而是在进程缓冲区和内核缓冲区之间进行数据交换。

用户进程是运行在用户空间的,不能直接操作内核缓冲区的数据。 用户进程进行系统调用的时候,会由用户态切换到内核态,待内核处理完之后再返回用户态。所以总是需要将数据由内核缓冲区换到用户缓冲区或者相反。

1.3、read&write

调用操作系统的read,是把数据从内核缓冲区复制到进程缓冲区,而write系统调用,是把数据从进程缓冲区复制到内核缓冲区,write并不一定导致内核的写动作,比如os可能会把内核缓冲区的数据积累到一定量后,再一次写入。
上层程序的IO不是物理设备级别的IO,而是缓存的复制。read&write调用,都不负责数据在内核缓冲区和物理设备(如磁盘)之间的交换,这项底层的读写交换是由操作系统内核完成的。

1.4、典型的系统调用流程

java服务器一次socket请求和响应流程:
(1)Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区
(2)java服务器通过read系统调用,从linux内核缓冲区读取数据,再送入java进程缓冲区
(3)java服务器再自己的用户空间中处理客户端请求
(4)java服务器完成处理后,构建响应数据,将数据从java服务器的进程缓冲区写入到内核缓冲区,这 里用的是write系统调用
(5)linux内核通过网络IO,将内核缓冲区的数据写入网卡,网卡通过底层的通信协议,将数据发送到目 标客户端。

2、三种经典的IO模式

此处的IO模式并不是java中的bio,nio,aio

2.1、三种IO模式简介

基于生活场景举例: 1、食堂排队打饭模式:排队在窗口,打完饭离开 2、点单等待被叫模式:等待叫单,好了自己去端 3、包厢模式:点单后菜做完被端上桌

排队打饭模式 BIO(阻塞同步IO) JDK1.4之前
点单等待被叫模式 NIO(非阻塞同步IO) JDK1.4(2002年,nio包)
包厢模式 AIO(非阻塞异步IO) JDK1.7(2011年)

2.2、同步、异步、阻塞和非阻塞

同步与异步
数据就绪后需要自己去读是同步,数据就绪直接读好再回调给程序是异步

同步IO:用户空间的线程是主动发起IO请求的一方,内核空间是被动接收方
异步IO:系统内核是主动发起IO请求的一方,用户空间的线程是被动接收方

阻塞与非阻塞
无数据时,读会阻塞直到有数据;缓冲区满时写操作也会阻塞。非阻塞遇到这种情况都是直接返回。

阻塞IO:进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。
非阻塞IO**:进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程

2.3、NIO对三种IO模式的支持情况

Netty不建议BIO模式,同时删除了已经实现的AIO的支持

(1)为什么不建议使用BIO模式?
**
连接数高的情况下: 阻塞 -> 耗资源、效率低

NIO一定优于BIO么?

不一定

  • BIO的代码简单
  • 特定场景下:连接数少,并发度低,BIO性能不输NIO

(2)为什么删除了已经实现的AIO的支持?
**

  • Windows实现成熟,但是很少用来做服务器。
  • Linux常用来做服务器,但是AIO实现不够成熟。
  • Linux下AIO相比较NIO的性能提升不明显。

3、进一步对比三种IO模型

3.1、BIO(Blocking IO)通信

一个线程负责监听客户端连接,接收到客户端连接后为每个客户端创建新的线程进行链路处理,通过输出流返回给客户端

优点:应用程序开发简单;阻塞期间用户线程挂起,用户线程基本不会占用cpu资源。
缺点
缺乏弹性伸缩能力,当并发访问量增加以后,服务端的线程个数和客户端的访问个数成1:1正比关系,线程是非常宝贵的资源,当线程数膨胀后系统性能急剧下降,会发生堆栈溢出,创建线程失败的问题。最终导致进程宕机或者僵死,无法提供服务。

BIO通信模型

image.png

BIO具体流程

image.png


基于BIO的伪异步IO通信

和BIO最大区别为服务端不再为每个客户端创建一个线程而是创建一个线程池统一处理所有客户端的接入。

一个线程负责监听客户端连接,当有新的客户端接入的时候,将客户端的socket封装成一个task投递到后端的线程池中进行处理

缺点:
并发量高时线程池阻塞


伪异步IO通信模型**

image.png

3.2、NIO(None Blocking)通信

在 Linux 系统下,可以通过设置将 socket 变成为非阻塞的模式 (Non-Blocking)。使用非阻塞模式的 IO 读写,叫作同步非阻塞 IO(None Blocking IO),简称为 NIO 模式。
在 NIO 模型中,应用程序一旦开始 IO 系统调用,会出现以下两种情况:
(1)在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。
(2)在内核缓冲区中有数据的情况下,是阻塞的,直到数据从内核缓冲复制到用户进程缓冲。 复制完成后,系统调用返回成功,应用进程开始处理用户空间的缓存数据。

同步非阻塞IO特点:应用程序的线程需要不断的进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,知道完成IO系统调用为止。

优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
缺点:不断轮询内核,将占用大量CPU时间,效率低下。

NIO流程:
image.png

3.3、AIO(Asynchronous IO)通信

理论上来说,异步 IO 是真正的异步输入输出,它的吞吐量高于 IO 多路复用模型的吞吐量。 就目前而言,Windows 系统下通过 IOCP 实现了真正的异步 IO。而在 Linux 系统下,异步 IO 模型在 2.6 版本才引入,目前并不完善,其底层实现仍使用 epoll,与 IO 多路复用相同,因此在性 能上没有明显的优势。 大多数的高并发服务器端的程序,一般都是基于 Linux 系统的。因而,目前这类高并发网络应 用程序的开发,大多采用 IO 多路复用模型。

用户线程通过系统调用向内核注册某个IO操作,内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。

一次异步IO的read操作系统调用流程如下:
(1)当用户线程发起了read调用,立刻就可以开始处理别的工作,用户线程不用阻塞
(2)内核开始了IO的第一个准备阶段:准备数据。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区
(3)内核会给用户线程发送一个信号,或者回调用户线程注册的回调接口,告诉用户线程read操作完成
(4)用户线程读取用户缓冲区的数据,完成后续的业务操作

缺点:应用程序仅需要进行事件的注册与接收,其余的工作留给了操作系统,需要操作系统底层内核的支持。

AIO流程

image.png

4、IO多路复用模型

在NIO模型中,如果内核缓冲区没有数据那么系统调用会立即返回,返回给用户无数据信息,如果内核缓冲区有数据,就阻塞,直到数据从内核缓冲区被复制到用户缓冲区。
NIO需要不断的轮询内核,效率低下。为了避免同步非阻塞IO的轮询等待问题,提出了IO多路复用模型。

IO多路复用模型中引入了一种新的调用,即查询IO的就绪状态(linux系统中对应的系统调用为select /epoll),通过该系统调用可以监视多个文件描述符,一旦某个描述符就绪(一般为内核缓冲区可读/可写), 内核能够将就绪的状态返回给应用程序。

在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就进行读写操作。