type
status
date
slug
summary
tags
category
icon
password
 
Netty 是目前比较火的网络编程框架,是 java 进阶的必备知识点,特此记录 Netty 学习过程中的一些知识点用于日后翻阅。本章介绍 Netty 的一些入门准备知识,包括同步和异步、阻塞和非阻塞的区别,还有常见的 IO 模型。

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

这四个概念都是针对一次调用发生的,我们可以这么理解,在一次调用里面存在调用者被调用者调用结果这三个概念。

同步和异步

同步和异步是从调用结果通知机制这个角度来区分的。
  • 同步:调用发生后,调用者 主动去获取 调用结果,这就是同步调用。
  • 异步:调用发生后,调用者被动等待 被调用者 通知 调用结果,这就是异步调用。通常过程是,调用发生后,调用者收到一个来自 被调用者 的回执,这个回执内容通常是是 被调用者 告诉 调用者 我已经收到这个任务了,但是这个任务具体什么时候执行,什么时候能得到执行结果,调用者 不需要管。等到执行完成后,被调用者 主动把 调用结果 通知给 调用者,整个调用过程才算完成。

阻塞和非阻塞

阻塞和非阻塞是从 调用者在获取到调用结果前的行为 这个角度进行区分的。
  • 阻塞:调用者 在获取到 调用结果 前什么都不干,线程整个挂起一直等待 调用结果,这就是阻塞调用。
  • 非阻塞:调用者 在获取 调用结果 前可以做自己的事情,这就是非阻塞调用。

四种概念组合

综上所述,同步和异步、阻塞和非阻塞是两套完全不同而且并不矛盾的概念,所以产生以下四种概念组合:
  • 同步阻塞:调用者 一直挂起什么都不做,主动去获取 调用结果
  • 同步非阻塞:调用者 拿到回执后继续做自己的事情,然后通过轮询机制,每隔一段时间去询问调用结果,直到最终获取 调用结果,调用才算完成。
  • 异步阻塞:调用者 一直挂起什么都不做,直到 被调用者 通知 调用结果
  • 异步非阻塞:调用者拿到回执后继续做自己的事情,等到 被调用者 通知调用结果后,调用才算完成。
这四种概念,是下面常见IO模型的基础。
 

基础概念

在进行解释之前,首先要说明几个概念:
  • 用户空间和内核空间
  • 进程切换
  • 进程的阻塞
  • 文件描述符
  • 缓存 IO

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间`。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
  1. 保存处理机上下文,包括程序计数器和其他寄存器。
  1. 更新PCB信息。
  1. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
  1. 选择另一个进程执行,并更新其PCB。
  1. 更新内存管理的数据结构。
  1. 恢复处理机上下文。

进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的

文件描述符fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

缓存 IO

缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。在 Linux 的缓存 IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。这个概念非常重要!

linux IO 的过程

结合以上的基础概念,linux IO 的整体流程是 数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间
对于本地 read IO,分为以下两个阶段:
  • 第一阶段:等待数据准备 (Waiting for the data to be ready)。
  • 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
对于网络 IO,网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。对于socket流而言,
  • 第一阶段:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
  • 第二阶段:把数据从内核缓冲区复制到应用进程缓冲区。

IO模型

 
根据以上基础知识,我们可以得到这样网络 IO 关系:
阻塞/非阻塞 概念可以发生在 网络数据—>内核空间内核空间—>拷贝到用户空间 这两个阶段。如果应用程序什么事都不做直到本阶段完成就称作 阻塞,如果应用程序在阶段完成前可以做自己的事情就称作 非阻塞
同步/异步 概念发生在 应用程序怎么收到结果 这个阶段。如果应用程序主动去获取结果就称作 同步,如果应用程序被动等待内核通知结果就称作 异步
这些概念是下面 IO 模型的基础。我们常见的有五种 IO 模型。

同步阻塞 IO(blocking IO)

网络模型

同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在这个 IO 模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据。
在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络 IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。流程图如下:
notion image

流程描述

  • 第一个阶段:准备数据。当用户进程调用了 recv()/recvfrom() 这个系统调用之后,用户进程就开始阻塞等待数据。对于网络 IO 来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候 kernel 就要等待足够的数据到来。这个过程也需要等待。也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。
  • 第二个阶段:当kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存。然后 kernel 返回结果,用户进程才解除 block 的状态,重新运行起来。

分析

特点
在 IO 执行的两个阶段都被阻塞。
优点
  • 能够及时返回数据,无延迟。
  • 对内核开发者来说这是省事了。
缺点
性能差,不能支持高并发的应用场景。

同步非阻塞 IO(nonblocking IO)

网络模型

同步非阻塞就是「每隔一会儿瞄一眼进度条」的轮询方式。在这种模型中,非阻塞也会调用 recvform 系统调用,不管数据是否准备好,内核都会马上返回给进程。如果数据还没准备好,就会返回一个错误代码(EAGAIN 或 EWOULDBLOCK),说明这个命令不能立即满足。进程在返回之后,可以干点别的事情,过一段时间再发起recvform 系统调用,重复上面的过程。这个过程通常被称之为轮询。直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。流程如图所示:
notion image

流程描述

  • 用户进程发出 read 操作时,马上就得到了一个结果。
    • kernel中的数据还没有准备好,返回一个 error 信号。
    • 用户进程接收到一个 error 信号时,就知道数据还没有准备好,这时候用户进程可以去做自己的事情。
    • 过一段时间再进行 read 操作。一直等到 kernel 中的数据准备好,返回一个数据已经准备好的信号。
  • 用户进程收到数据准备好的信号后,开始阻塞自己,将 kernel 的数据拷到用户进程空间。

分析

特点
nonblocking IO 的特点是用户进程需要不断的主动询问 kernel 数据好了没有。
同步非阻塞方式相比同步阻塞方式:
优点
能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
缺点
  • 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次 read 操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
  • 由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间

IO 多路复用( IO multiplexing)

网络模型

IO 多路复用就是用一个单独的非用户态的进程去完成同步非阻塞的轮询工作。和同步非阻塞 IO 的区别是,同步非阻塞 IO使用用户态的进程去完成这个轮询工作,而 IO多路复用使用 UNIX/Linux 下的内核函数 select、poll、epoll 都可以完成这个轮询工作(epoll 比 poll、select 效率高,做的事情是一样的)。我们可以把这类函数的工作称为 select 轮询。
select 轮询是内核级别的,可以监听多个 socket,能实现同时对多个IO端口进行监听,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用 I/O 操作函数。然后进程再进行 recvform 系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。流程图如下:
notion image

流程描述

  • 当用户进程调用了 select,整个进程会被 block,而同时,kernel 会监视所有 select 负责的 socket
  • 当任何一个 socket 中的数据准备好了,select 就会返回。
  • 用户进程开始阻塞,将数据从 kernel 拷贝到用户进程。
上面的图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection

分析

如果处理的连接数不是很高的话,使用 select/epoll 不一定比使用 multi-threading + blocking IO 性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。I/O 多路复用技术通过把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求,大大降低系统开销小,节省了系统资源。
I/O多路复用的主要应用场景如下:
  • 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
  • 服务器需要同时处理多种网络协议的套接字。

注意

前面三种IO模式,在用户进程进行系统调用的两个阶段:
  • 第一个阶段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。
  • 第二个阶段可以阻塞又可以不阻塞。
从整个IO过程来看,他们都是同步模型,因为都属于应用程序主动等待且向内核检查状态。

异步非阻塞 IO(asynchronous IO)

网络模型

相对于同步IO,异步IO不是顺序执行。用户进程进行 aio_read 系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
Linux 提供了 AIO 库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如 libevent、libev、libuv。异步过程如下图所示:
notion image

流程描述

  • 用户进程发起 aio_read 操作之后,立刻就可以开始去做其它的事。从 kernel 的角度,当它受到一个asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。
  • kernel 会等待数据准备完成,然后将数据拷贝到用户内存。
  • kernel 会给用户进程发送一个 signal 或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它 read 操作完成了。如果这个进程正在处理别的事情,就看情况进行处理。
    • 如果用户态忙着做别的事(例如在计算两个矩阵的乘积),那就强行打断之,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
    • 如果这个进程正在内核态忙着做别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
    • 如果这个进程现在被挂起了,例如无事可做 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。
Linux 的异步 IO(AIO)支持是 2.6.22 才引入的,还有很多系统调用不支持异步 IO。Linux 的异步 IO 最初是为数据库设计的,因此通过异步 IO 的读写操作不会被缓存或缓冲,这就无法利用操作系统的缓存与缓冲机制。

分析

IO 多路复用和 AIO 都适合高并发的应用场景,两者的区别是:
  • IO多路复用:IO 多路复用是同步非阻塞模式。select 函数所提供的功能(异步阻塞 IO)与 AIO 类似。不过,它是对通知事件进行阻塞,而不是对 IO 调用进行阻塞。当内核数据在准备过程中,select 函数一直被阻塞,等到数据准备完成后会通知应用程序数据准备好了,然后 select 函数的任务就完成了,至于后面把数据从内核空间复制到用户空间(通过阻塞/非阻塞形式都可以)这部分工作由应用程序本身完成。
  • AIO:AIO 是异步模式。内核准备数据和把数据从内核空间复制到用户空间这两部分工作都由系统完成,也就是 IO 过程全部由系统控制,当应用程序收到通知的时候用户空间已经准备好数据了。

信号驱动式IO(signal-driven IO)

信号驱动式I/O:首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。过程如下图所示:
notion image
信号驱动IO实际使用得不多,所以在这里不做深入介绍。

总结

本章介绍了同步/异步、阻塞/非阻塞的概念、linux IO的基础概念和五种常见的 IO 模型:
  1. 同步和异步是从调用结果通知机制这个角度来区分的。同步调用是指调用发生后,调用者主动去获取调用结果;异步调用是指调用发生后,调用者被动等待被调用者通知调用结果
  1. 阻塞和非阻塞是从调用者在获取到调用结果前的行为这个角度进行区分的。阻塞调用是指调用者在获取到调用结果前什么都不干,线程整个挂起一直等待调用结果;非阻塞调用是指调用者在获取调用结果前可以做自己的事情。
  1. 系统空间分为用户空间和内核空间,内核空间供系统内核使用,用户空间供各个进程使用。
  1. 进程被挂起,新的进程或者以前被挂起的某个进程恢复执行,这种过程被称作进程切换。进程从运行态转换成阻塞态,这种过程被称作进程阻塞,进程阻塞后不占用cpu资源。
  1. 文件描述符fd是指向文件的引用的抽象化概念。
  1. linux IO的过程:数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
  1. 五种常见的 IO 模型:
      • 同步阻塞 IO(blocking IO)
      • 同步非阻塞 IO(nonblocking IO)
      • IO 多路复用( IO multiplexing)
      • 异步非阻塞 IO(asynchronous IO)
      • 信号驱动式IO(signal-driven IO)
      其中 IO 多路复用模型是 netty IO 模型的基础,必须要掌握,五种 IO 模型流程简单总结如下:
      notion image

参考

Netty系列(二):Netty介绍Hexo问题汇总
mcbilla
mcbilla
一个普通的干饭人🍚
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉欢迎来到飙戈的博客🎉
-- 感谢您的支持 ---
👏欢迎学习交流👏