IO读写原理;BIO、NIO、AIO(是否同步、是否阻塞)、IO多路复用--知识不用就会忘

IO读写原理;BIO、NIO、AIO(是否同步、是否阻塞)、IO多路复用-- 知识不用就会忘

有些东西,开发的时候你都懂。等你开发完了,你就记忆模糊了,哈哈。不知道是不是只有我这样。额

在介绍三者之前,先聊一聊IO读写的基础原理

《Netty、Redis、ZooKeeper高并发实战》图书

1. IO读写基础原理

1.1 read、write系统调度

    相信大家都听说过缓冲区这个词,其实只要进行IO操作,就离不开它。
    就Linux而言,程序并非直接和底层物理内存进行数据的交互,中间还要经过缓冲区

  • 上层应用->缓冲区->物理设备内存
  • 物理设备内存->缓冲区->上层设备

    上层设备调用操作系统的readwrite都会涉及缓冲区。

  • 调用操作系统的read
    把数据从内核缓冲区复制到进程缓冲区
  • 调用操作系统的write
    把数据从进程缓冲区复制到内核缓冲区

    readwrite两个系统调用,都不会负责数据在内核缓冲区和实际物理设备(如磁盘、内存)之间的交换,这项底层的读写交换,实际是由操作系统的内核(Kernel)完成的。
    在用户程序中,无论是Socket的IO、还是文件的IO操作,都是上层应用的开发,其输入Input和输出Output的处理,在编程流程上都是一致的。
[实际上我们说的Linux指的就是其内核Kernel,而具体的Debian、CentOS等Linux发行版本,才是基于Linux内核的完整操作系统,包括GUI组件和许多其他工具。通常可以理解操作系统是系统内核上的一套软件,我们的应用至多和操作系统层面打交道,而物理设备的具体操作还是操作系统调用内核的接口,由内核处理的]

关于read和write系统调度,感兴趣的可以看看这几篇网文。
linux中的read和write系统调用
read和write函数

1.2 内核缓冲区与进程缓冲区

《Netty、Redis、ZooKeeper高并发实战》图书
Linux用户空间与内核空间(理解高端内存)
什么是内核缓冲区,用户缓冲区

    众所皆知,CPU处理指令很快,如果获取数据每次都需要从内存临时读取,那么太慢了。所以CPU和内存之间,还有一个CPU的寄存器。内存会自己将获取到的需要处理的数据即时传输到寄存器,这样CPU就不需要每次都去操作内存看是否需要处理数据了。
    类似CPU和内存之间需要寄存器来暂存数据一样。我们的应用和物理设备的实际IO之间,隔着一对缓冲区(内核缓冲区进程缓冲区)。实际上,内核缓冲区进程缓冲区就是对内存资源的一种划分形式。
    有了缓冲区后,上层应用使用read系统调用时,仅仅把数据从内核缓冲区区复制到上层应用的缓冲区(进程缓冲区)。[内核缓冲区的数据,由系统内核从物理设备中读取,这个过程是透明的,用户无法感知]
    上层应用调用write系统调用时,仅把数据从进程缓冲区复制到内核缓冲区。底层操作会对内核缓冲区进行监控,等待缓冲区的数据达到一定数量级时,请求IO设备的中断处理(操作系统级别),集中执行物理设备的实际IO操作。这种机制提高了系统的性能。至于什么时候中断(读中断、写中断),由操作系统的内核来决定,用户程序无需关心。

    (我理解就是,内核缓冲区获取数据,并且申请系统中断,此时CPU会处理对应的物理设备的IO事件。内存将数据交给CPU寄存器,CPU读取数据和指令,进行处理。实际应该更加复杂,希望看官们补充下自己的见解)

1.3 典型的系统调用流程

《Netty、Redis、ZooKeeper高并发实战》图书

IO读写原理;BIO、NIO、AIO(是否同步、是否阻塞)、IO多路复用--知识不用就会忘_第1张图片
这里用read系统调用为例,完整的输入流程包括两个阶段:

  1. 等待数据准备好
  2. 从内核向进程复制数据

如果是read一个socket(套接字),上述两阶段具体处理流程如下:

  1. 等待数据从网络中到达网卡。当所有等待的分组到达时,它被复制到内核中的缓冲区。这个工作由操作系统自动完成,用户无感知。
  2. 把数据从内核缓冲区复制到对应的进程缓冲区

如果是具体的Java服务端,完成一次socket请求和响应,完整流程如下:

  1. 客户端请求:Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区
  2. 获取请求数据:Java服务器通过read系统调用,从Linux内核缓冲区读取数据,再送入进程缓冲区。
  3. 服务器端业务处理:Java服务器再自己的用户空间(后面IO的讲解也会提到这个词)中处理客户端的请求。
  4. 服务器端返回数据:Java服务器完成处理后,构建好响应数据,通过write系统调用,将这些数据从用户缓冲区进程缓冲区)写入内核缓冲区
  5. 发送给客户端:Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,将数据发送给目标客户端。(这里个人认为,内核缓冲区到网卡,中间也会经过内存 这里是我傻了,内核缓冲区本身就是内存,只不过是对指定区块的内存地址命名为内存缓冲区)

这里补充一下网卡接收数据的流程。这个比较复杂,感兴趣的可以自己深入了解。

Linux网络 - 数据包的接收过程

    这个我在自己的github笔记也有过文字描述了。这里直接摘过来了。
网卡接收数据:

    网卡网络服务中,网卡NIC通过硬件中断IRQ通知CPU有数据要处理时,CPU在查询已注册的中断函数,请求调用网卡驱动NIC Driver的相对应函数,这个过程所有其他硬件不能请求硬中断,也就是没法和CPU交互;然后网卡驱动NIC Driver收到请求后,先禁用网卡的中断IQR,这样CPU就能再响应其他硬件的中断了。此时NIC Driver启用软中断,经过内核读取网卡写到内存的数据包,调用相应软中断处理函数,然后网卡把数据交给CPU让其软中断上下文中处理网络数据,接着调用协议栈响应的函数将数据给协议栈处理,待内存中所有数据包处理完后(即poll函数执行完后),才启用网卡的硬中断,这样网卡下次就能再收到数据通知CPU。

    大致就是:网卡NIC-- 请求硬中断–> CPU — 请求网卡驱动调用函数 --> NIC Driver – 禁用网卡硬中断,此时别的硬件能够对CPU请求硬中断操作 --> NIC Driver – 启用软中断 --> 和内核、内存、CPU在软中断过程交互处理网卡写到内存的数据 – > 交给协议栈处理 --内存所有数据包处理完后–> 启用网卡硬中断,能够再收到数据并通知CPU

2. 四种IO(BIO、NIO、AIO、IO多路复用)

    BIO、NIO、AIO三者无非就“是否同步”、“是否阻塞”的区别。在讲解三者之前,先聊聊“同步”和“异步(非同步)”;“阻塞”和“非阻塞”。

2.1 同步和异步(非同步)

参考资料
java BIO NIO AIO讲解
《Netty、Redis、ZooKeeper高并发实战》图书

    首先,同步异步,区别在于用户空间内核空间IO发起方式
同步IO和异步IO
    同步IO中,用户进程的具体某一线程作为主动发起方,发起IO请求,而内核空间作为被动接受方异步IO则相反,系统内核作为主动发起方发起IO请求,而用户空间的线程是被动接收方
    听上去很绕口、抽象。同步IO即用户请求IO操作时,应用程序内部会自动轮询查看操作系统对IO事件的处理,当处理成功或者出现异常时,再将结果返回给用户。而异步IO,在用户请求IO操作后,用户可接下去继续其他操作,这时候由操作系统来监听查看IO事件的处理,如果完成或者出现异常,再将信息返回给应用程序(一般应用程序有预设的回调函数、钩子函数,操作系统将信息返回给这些预设的函数,然后应用程序会在收到通知后自动调用这些函数)。
    简单讲,同步和异步的区别,就是监听IO事件的执行过程,是由谁完成的。如果是同步IO,应用程序会执行内部循环监听IO事件的函数等操作,所以用户没法继续别的操作;而异步IO,操作系统也将监听IO事件的处理的任务包揽了,会自己将处理结果(成功、异常)返回给应用程序预设的函数,由于不需要监听IO事件的执行情况,这时候用户就可以继续别的操作了。

2.2 阻塞IO和非阻塞IO

参考资料
java BIO NIO AIO讲解
《Netty、Redis、ZooKeeper高并发实战》图书

    阻塞非阻塞,区别在于用户空间是否需要等待内核空间IO处理彻底完成
    非阻塞IO,即用户空间的程序不需要等待内核IO彻底完成,可以立即返回用户空间执行用户的操作,用户空间处于非阻塞的状态,于此同时,内核空间会立即返回给用户一个状态值。
    简单说就是,用户空间(调用线程)拿到内核空间返回的状态值,就立刻将其返回给自己的空间,IO操作可以干就干,不可以干就继续干别的事情。
    非阻塞IO要求socket设置为NONBLOCK。
这里的NIO(同步非阻塞)模型,并非Java的NIO(New IO)库,Java的NIO更偏向IO多路复用理念
    可能有人就有疑惑了,我IO操作,不都希望得到IO处理结果吗,那我为啥用非阻塞IO。其实这个确实用的很少,如果需要即时处理数据,往往需要用while循环等反复判断非阻塞IO是否处理完IO事件,其实也约等于用成了阻塞IO。不过如果偏底层的程序,如果不需要实时处理IO事件,但是又需定时判断某一IO事件是否处理完成,那就用得到非阻塞IO了。(暂时没想到比较贴近生活的应用场景,阅历高的看官们可以给点建议。)

2.3 BIO、NIO、AIO、IO多路复用

《Netty、Redis、ZooKeeper高并发实战》图书
epoll原理详解及epoll反应堆模型

    根据前面的学习,知道了IO读写原理、同步与异步、阻塞和非阻塞,那么BIO、NIO、AIO、IO多路复用就很好理解了。

  1. BIO:同步阻塞IO(Blocking IO)。即用户程序(用户空间)作为IO事件的主动发起方,向内核空间发起IO事件,并且阻塞线程,直到IO事件彻底完成后,用户程序重新获取到CPU的执行权,能够往下运行程序。
    我们生活中普通的Hello world程序,普通的Scanner输入,文件File的读取、写入,都是BIO
  2. NIO:同步非阻塞IO(Non-Blocking IO)。用户程序(用户空间)主动发起IO事件,但是立即获取到IO事件的状态值,无需等待IO事件完成,可继续进行别的操作。
    这个实际应用少一点。简单想想,比如你的文件读取是NIO操作,那么你代码执行到File的readNextLine()之类的操作时,假如它直接返回给你一个状态值“可以读取下一行,正准备读取”,然后你直接就往下执行程序了,要是下一行就是对读取到的String数据的操作,这时候String变量还没有真正获取到IO数据的值,那么会是null,很可能导致你接下去的程序出现异常。所以实际很多场景还是需要用while循环判断NIO是否将IO事件处理完成了,等于还是用成了BIO。
  3. AIO: 异步IO(Asynchronous IO)。只能明确是异步,但是可能是非阻塞IO,也可能是阻塞IO。也就是用户空间作为IO事件的被动接收方,在请求IO事件处理后,监听IO事件处理的过程全权交由操作系统处理,内核空间之后再将结果返回给用户程序指定的回调函数。如果是阻塞IO,那么即时是异步,用户仍然将CPU执行交由操作系统处理IO事件,必须等待IO事件完成,才能继续向下执行代码;如果是非阻塞IO,那么用户直接得知当时那一刻的IO事件处理状态(这往往没什么太大用)。
  4. IO多路复用: (IO Multiplexing),使用的效果上类似异步IO,实际为同步IO,且为阻塞IO,但是和BIO不同,其算是NIO的改良版本。这个要具体展开的话,可以说很多(建议看看上面多次提到的《Netty、Redis、ZooKeeper高并发实战》)。
        IO多路复用,常见于经典的Reactor反应器模式、Java的NIO(New IO)的Select选择器和Linux的epoll
        NIO中,非阻塞IO操作,导致需要我们在用户空间手动while循环判断IO事件的处理状态。而IO多路复用,通过selectepoll等系统调用,完成阻塞IO的IO状态监听。
    需要底层操作系统的支持,select系统调用,几乎所有的操作系统都有支持,有良好的跨平台性。而epoll是Linux2.6内核提出的,是select系统调用的Linux增强版本
        在IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。在Linux系统中,对应的系统调用为select/epoll系统调用。通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核就能够将就绪的状态返回给应用程序,随后,应用程序根据就绪的状态,进行相应的IO系统调用
  • 选择器注册:首先将选择器注册到select/epoll选择器中,Java中对应的是Selector类。然后开启整个IO多路复用模型的轮询流程。
  • 就绪状态的轮询:通过选择器的查询方法,查询所有socket连接的就绪状态。(通过select/epoll系统调用,其会收集所有当前就绪的socket,返回列表,这一选择过程,需要阻塞线程,直到至少一个socket就绪返回【当然也可能执行选择操作时,返回含有多个就绪socket的列表】)。
  • 复制数据:通过选择,获取到就绪状态列表后,根据其中的socket连接,发起read系统调用,用户线程阻塞,内核复制数据,将数据从内核缓冲区复制到用户缓冲区
  • 处理数据:selectread都完成后,执行其他操作,处理数据什么的。

    和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。
    IO多路复用模型与同步非阻塞IO模型有密切关系。对于注册在选择器上的每一个可以查询的socket连接,一般都设置成为NIO同步非阻塞模型。(对于用户程序而言是无感知的)

    IO多路复用模型的优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。

    IO多路复用模型的缺点本质上,select/epoll系统调用是阻塞式的,属于同步IO
    在实际开发中,比如在Netty中运用IO多路复用时,TCP通讯的话,需要一个连接对应一个Channel通道,往往可以开启一个线程(或多个线程=CPU内核数)当作Boss线程组,专门处理连接请求(就绪状态轮询);然后另外开一个线程组(CPU内核数*2)的Worker工作线程用于处理数据的读取、处理。这样,Boss线程组中1个线程就可以监听处理N个Socket连接事件(Channel通道的连接),而对应连接就绪的Channel通道获取数据、处理数据时,由Worker线程组的空闲线程分担处理。
(个人理解,IO多路复用在实际开发中,类似事件驱动模式。)

    最后自己宣传下自己最近和队友制作的Android安卓小程序,采用核心技术为:SpringCloud、Netty(UDP通讯)、Flutter移动端开发。
有排面app-介绍视频(原长版)

你可能感兴趣的:(IO,AIO,BIO,NIO,IO多路复用,操作系统)