写点什么

小六六学 Netty 系列之 unix IO 模型

作者:自然
  • 2022 年 9 月 07 日
    广东
  • 本文字数:4981 字

    阅读完需:约 16 分钟

前言

文本已收录至我的 GitHub 仓库,欢迎 Star:https://github.com/bin392328206/six-finger

种一棵树最好的时间是十年前,其次是现在

我知道很多人不玩 qq 了,但是怀旧一下,欢迎加入六脉神剑 Java 菜鸟学习群,群聊号码:549684836 鼓励大家在技术的路上写博客

絮叨

今天来学习学习 unix Io 模型吧,为后面的 NIO Netty 打下基础,这篇文章,我打算来谈谈 unix 的 io 模型,其中会涉及到下面的内容:

背景知识

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

首先大家心中需要有以下的清晰认知:


  • 阻塞操作不等于同步(blocking operation does NOT equal to synchronous)

  • 非阻塞操作不等于异步(non-blocking operation does NOT equal to asynchronous)

  • 事实上,同步异步于阻塞和非阻塞没有什么直接的关联关系


同步和异步


  • 同步是指在发出一个 function 调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到调用结果了。这个结果可能是一个正确的期望结果,也可能是因为异常原因(比如超时)导致的失败结果。换句话说,就是由调用者主动等待这个调用的结果。

  • 异步是调用在发出之后,本次调用过程就直接返回了,并没有同时没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态变化、事件通知等机制来通知调用者,或通过回调函数处理这个调用。


阻塞和非阻塞


  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回

  • 非阻塞是指在不能立刻得到结果之前,该调用不会阻塞当前线程。

文件描述符 fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。


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

用户空间(user space)与内核空间(kernel space)

学习 Linux 时,经常可以看到两个词:User space(用户空间)和 Kernel space(内核空间)。



简单说,Kernel space 是 Linux 内核的运行空间,User space 是用户程序的运行空间。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。


Kernel space 可以执行任意命令,调用系统的一切资源;User space 只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核发出指令。


str = "my string" // 用户空间x = x + 2 // 用户空间file.write(str) // 切换到内核空间y = x + 4 // 切换回用户空间
复制代码


上面代码中,第一行和第二行都是简单的赋值运算,在 User space 执行。第三行需要写入文件,就要切换到 Kernel space,因为用户不能直接写文件,必须通过内核安排。第四行又是赋值运算,就切换回 User space。

进程的阻塞

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

进程切换

为了控制进程的执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。进程之间的切换其实是需要耗费 cpu 时间的。

缓存 I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。


  • 读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。

  • 写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了 sync 同步命令


缓存 I/O 的优点:


  • 在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全;

  • 可以减少物理读盘的次数,从而提高性能。


缓存 I/O 的缺点:


  • 在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样,数据在传输过程中需要在应用程序地址空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。


因为这个原因的存在,所以又设计到 zero copy 技术。关于 zero copy 这个内容,可以再后面的文章看到

unix IO 模型

在 linux 中,对于一次 IO 访问(以 read 举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个 read 操作发生时,它会经历两个阶段:


  • 等待数据准备就绪 (Waiting for the data to be ready)

  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)


正式因为这两个阶段,linux 系统产生了下面五种网络模式的方案:


  • 阻塞式 IO 模型(blocking IO model)

  • 非阻塞式 IO 模型(noblocking IO model)

  • IO 复用式 IO 模型(IO multiplexing model)

  • 信号驱动式 IO 模型(signal-driven IO model)

  • 异步 IO 式 IO 模型(asynchronous IO model)


下面我们来分别谈一下这些 IO 模型

阻塞式 IO 模型(blocking IO model)

在 linux 中,默认情况下所有的 IO 操作都是 blocking,一个典型的读操作流程大概是这样:



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


所以:blocking IO 的特点就是在 IO 执行的下两个阶段的时候都被 block 了。


  • 等待数据准备就绪 (Waiting for the data to be ready) 「阻塞」

  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「阻塞」

非阻塞 I/O(nonblocking IO)

linux 下,可以通过设置 socket 使其变为 non-blocking。通过 java 可以这么操作:


InetAddress host = InetAddress.getByName("localhost");Selector selector = Selector.open();ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(hos1234));serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
复制代码


socket 设置为 NONBLOCK(非阻塞)就是告诉内核,当所请求的 I/O 操作无法完成时,不要将进程睡眠,而是返回一个错误码(EWOULDBLOCK) ,这样请求就不会阻塞。



当用户进程调用了 recvfrom 这个系统调用,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 EWOULDBLOCK error。从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 EWOULDBLOCK error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户空间缓冲区,然后返回。


可以看到,I/O 操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。整个 I/O 请求的过程中,虽然用户线程每次发起 I/O 请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源。


所以,non blocking IO 的特点是用户进程需要不断的主动询问 kernel 数据好了没有:


  • 等待数据准备就绪 (Waiting for the data to be ready) 「非阻塞」

  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「阻塞」


一般很少直接使用这种模型,而是在其他 I/O 模型中使用非阻塞 I/O 这一特性。这种方式对单个 I/O 请求意义不大,但给 I/O 多路复用铺平了道路.

I/O 多路复用( IO multiplexing)

IO multiplexing 就是我们常说的 select,poll,epoll,有些地方也称这种 IO 方式为 event driven IO。select/epoll 的好处就在于单个 process 就可以同时处理多个网络连接的 IO。它的基本原理就是 select,poll,epoll 这些个 function 会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。



当用户进程调用了 select,那么整个进程会被 block,而同时,kernel 会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。


所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。


这个图和 blocking IO 的图其实并没有太大的不同,事实上因为 IO 多路复用多了添加监视 socket,以及调用 select 函数的额外操作,效率更差。还更差一些。因为这里需要使用两个 system call (select 和 recvfrom),而 blocking IO 只调用了一个 system call (recvfrom)。但是,但是,使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 I/O 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 I/O 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。


所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)


在 IO multiplexing Model 中,实际中,对于每一个 socket,一般都设置成为 non-blocking,但是,如上图所示,整个用户的 process 其实是一直被 block 的。只不过 process 是被 select 这个函数 block,而不是被 socket IO 给 block。


因此对于 IO 多路复用模型来说:-等待数据准备就绪 (Waiting for the data to be ready) 「阻塞」


  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「阻塞」

异步 I/O(asynchronous IO)

接下来我们看看 linux 下的 asynchronous IO 的流程:



用户进程发起 aio_read 调用之后,立刻就可以开始去做其它的事。而另一方面,从 kernel 的角度,当它发现一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。然后,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户进程发送一个 signal,告诉它 read 操作完成了。


异步 I/O 模型使用了 Proactor 设计模式实现了这一机制。


因此对异步 IO 模型来说:


  • 等待数据准备就绪 (Waiting for the data to be ready) 「非阻塞」

  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「非阻塞」

信号驱动式 IO 模型(signal-driven IO model)

首先我们允许 socket 进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。



但是这种 IO 模确用的不多,所以我这里也就不详细提它了。

结尾

因此我们会得出下面的分类:


同步 IO (synchronous IO)


  • blocking IO model

  • non-blocking IO model

  • IO multiplexing model


异步 IO (asynchronous IO)


  • asynchronous IO model

参考


日常求赞

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是真粉


创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见


六脉神剑 | 文 【原创】如果本篇博客有任何错误,请批评指教,不胜感激 !

发布于: 刚刚阅读数: 3
用户头像

自然

关注

还未添加个人签名 2020.03.01 加入

小六六,目前负责营收超百亿的支付中台

评论

发布
暂无评论
小六六学Netty系列之unix IO模型_Netty_自然_InfoQ写作社区