当前位置: 首页 > news >正文

区分三种IO模型和select/poll/epoll

部分内容来源:JavaGuide 


select/poll/epoll 和 三种IO模型之间的关系是什么?

区分普通IO和IO多路复用

普通IO,即一个线程对应一个连接,因为每个线程只处理一个客户端 socket,目标明确:线程中直接操作该 socket 的 read() / write(),无需关心其他连接,也就是无需遍历文件描述符

可以理解成,一个线程只持有一个Socket的文件描述符

而IO多路复用是一个线程持有多个Socket的文件描述符,所以它有一个文件描述符集合,内核要遍历这个文件描述集合检查每个Socket中是否有数据


区分select/poll/epoll和三种IO模型

select/poll/epoll 是 如何遍历文件描述符寻找有数据的 Socket 的方法;

IO 模型是Socket 拿到数据后如何处理的策略

IO多路复用是单线程如何高效管理和寻找多个Socket,而IO模型是如何处理多个Socket的数据

并且IO多路复用属于同步IO

Reactor是基于select/epoll这些IO多路复用机制实现的,也就是基于同步IO

Proactor是基于异步IO机制实现的


IO多路复用的演进流程

线程池实现资源复用

如果要让服务器服务多个客户端,那么最直接的方式就是为每一条连接创建线程

其实创建进程也是可以的,原理是一样的

进程和线程的区别在于线程比较轻量级些,线程的创建和线程间切换的成本要小些

为了描述简述,后面都以线程为例

处理完业务逻辑后,随着连接关闭后线程也同样要销毁了,但是这样不停地创建和销毁线程,不仅会带来性能开销,也会造成浪费资源,而且如果要连接几万条连接,创建几万个线程去应对也是不现实的

要这么解决这个问题呢?我们可以使用「资源复用」的方式

通过池化思想保存历史连接

也就是不用再为每个连接创建线程,而是创建一个「线程池」,将连接分配给线程,然后一个线程可以处理多个连接的业务。


线程怎样才能高效地处理多个连接的业务?

当一个连接对应一个线程时,线程一般采用「read -> 业务处理 -> send」的处理流程

如果当前连接没有数据可读,那么线程会阻塞在 read 操作上(socket 默认情况是阻塞 I/O)

不过这种阻塞方式并不影响其他线程。

但是引入了线程池,那么一个线程要处理多个连接的业务,线程在处理某个连接的 read 操作时,如果遇到没有数据可读,就会发生阻塞,那么线程就没办法继续处理其他连接的业务。

要解决这一个问题,最简单的方式就是将 socket 改成非阻塞,然后线程不断地轮询调用 read 操作来判断是否有数据,这种方式虽然该能够解决阻塞的问题

但是解决的方式比较粗暴

因为轮询是要消耗 CPU 的,而且随着一个线程处理的连接越多,轮询的效率就会越低

为什么Socket要设置为非阻塞?

当socket设为非阻塞时,即使没有数据可读,read()也会立即返回一个【无数据的错误】

(如 Linux 的EAGAIN),不会阻塞线程

此时线程可以通过轮询处理多个连接

线程循环遍历自己负责的所有socket描述符,对每个socket调用read()

  1. 如果read()返回数据,就处理该连接的业务;
  2. 如果read()返回 “无数据”,就跳过这个socket,继续轮询下一个

监听连接

上面的问题在于,线程并不知道当前连接是否有数据可读,从而需要每次通过 read 去试探

那有没有办法在只有当连接上有数据的时候,线程才去发起读请求呢?

答案是有的,实现这一技术的就是 I/O 多路复用

I/O 多路复用技术会用一个系统调用函数来监听我们所有关心的连接,也就说可以在一个监控线程里面监控很多的连接


三种IO模型

小区别

IO多路复用属于网络监听模型,例如Redis实现网络监听的时候就是使用IO多路复用的,当Redis启动的时候,它会主动使用IO多路复用网络监听模型去监听

每个 Socket 在内核中都有对应的接收缓冲区发送缓冲区

read:从该 Socket 对应的接收缓冲区中读取数据到用户缓冲区


Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式

这里先给大家复习下阻塞、非阻塞、同步、异步 I/O 的概念

阻塞I/O

先来看看阻塞 I/O,当用户程序执行 read,线程会被阻塞

一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回

注意:阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程

过程如下图:


非阻塞 I/O

知道了阻塞 I/O,来看看非阻塞 I/O,非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续下执行

此时应用程序不断轮询内核直到数据准备好

内核将数据拷贝到应用程序缓冲区,read 才可以获取到结果

过程如下图:

注意,这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程


异步IO

如果 socket 设置了 O_NONBLOCK 标志,那么就表示使用的是非阻塞 I/O 的方式访问

而不做任何设置的话,默认是阻塞 I/O

因此,无论 read 和 send 是阻塞 I/O,还是非阻塞 I/O 都是同步调用

因为在 read 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间

为什么这么说?

同步 的核心是:用户线程必须亲自参与数据拷贝的等待过程

并且用户 线程必须等待拷贝完成才能继续执行

同步IO要等待的两个步骤:

  1. 「内核数据准备好」
  2. 「数据从内核空间拷贝到用户空间」

阻塞IO:等待。你到咖啡店后,发现咖啡还没做好,于是站在柜台前一直等

非阻塞IO:轮询。你到咖啡店后如果没好,那你就去附近晃悠一会儿,然后再问咖啡时候做好了,直到店员说好了


真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待

当我们发起 aio_read(异步 I/O)之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间

这个拷贝过程同样是异步的,内核自动完成的

和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作

过程如下图:


理解IO的简单例子

举个你去饭堂吃饭的例子,你好比应用程序,饭堂好比操作系统


阻塞 I/O

你去饭堂吃饭,但是饭堂的菜还没做好,然后你就一直在那里等啊等,等了好长一段时间终于等到饭堂阿姨把菜端了出来(数据准备的过程),但是你还得继续等阿姨把菜(内核空间)打到你的饭盒里(用户空间),经历完这两个过程,你才可以离开。


非阻塞 I/O

你去了饭堂,问阿姨菜做好了没有,阿姨告诉你没,你就离开了,过几十分钟,你又来饭堂问阿姨,阿姨说做好了,于是阿姨帮你把菜打到你的饭盒里,这个过程你是得等待的


异步 I/O

你让饭堂阿姨将菜做好并把菜打到饭盒里后,把饭盒送到你面前,整个过程你都不需要任何等待


很明显,异步 I/O 比同步 I/O 性能更好

因为异步 I/O 在「内核数据准备好」和「数据从内核空间拷贝到用户空间」这两个过程都不用等待

Proactor 正是采用了异步 I/O 技术,所以被称为异步网络模型


简单总结-IO多路复用的演进流程

面试引导:

线程创建销毁的开销

->池化思想

->select,poll存在的问题

->轮询机制对比事件驱动机制

->epoll,基于红黑树和哈希表而不是遍历文件描述符

->引出IO多路复用的两种实现,即Reactor和Proactor


池化思想:

如果让一个服务器能够服务更多的服务端,最直接的方式就是给每一条连接创建一个线程

但是线程的创建和销毁是有一定的开销的,会消耗CPU的时间片轮转的资源

所以为了达到资源复用就出现了【池化思想】,也就是我们的线程池


不要弄混执行任务的线程池和处理IO的线程池,这两个东西只是分别处理的东西不同

线程的职责可以根据任务类型划分

有的线程专注于处理连接的 I/O 操作

有的线程专注于执行业务逻辑任务


IO线程如何高效处理多个连接的业务:

一个连接对应一个线程时线程的处理流程:

「read -> 业务处理 -> send」

线程池的目的是让一个线程照顾多个连接

但一线程对应多连接时,线程在处理某个连接的 read 操作时,如果没有数据可读,则会阻塞

导致线程无法执行其它的IO任务

简单解决方式:将Socket换成非阻塞,之后线程不断地轮询调用 read 操作来判断是否有数据

但轮询是要消耗 CPU 的,随着一个线程处理的连接越多,轮询的效率就会越低

当socket设为非阻塞时,即使没有数据可读,read()也会立即返回一个【无数据的错误】,不会阻塞线程


监听连接:

问题所在:线程并不知道当前连接是否有数据可读

因此需要每次通过 read 去试探(轮询)

我们可以引入一种事件驱动机制,等连接来了我们再去执行,这就是epoll


三种IO模型-快速复习

小区别(防止概念混淆):

IO多路复用属于网络监听模型,例如Redis实现网络监听的时候就是使用IO多路复用的,当Redis启动的时候,它会主动使用IO多路复用网络监听模型去监听

每个 Socket 在内核中都有对应的接收缓冲区发送缓冲区

read:从该 Socket 对应的接收缓冲区中读取数据到用户缓冲区


理解三种IO模型:

read 和 send 是阻塞 I/O,还是非阻塞 I/O 都是同步调用

因为在 read 调用时,内核将数据从内核空间拷贝到用户空间的过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间

为什么这么说?

同步 的核心是:用户线程必须亲自参与数据拷贝的等待过程

并且用户 线程必须等待拷贝完成才能继续执行

同步IO要等待的两个步骤:

  1. 「内核数据准备好」
  2. 「数据从内核空间拷贝到用户空间」

阻塞IO:等待。你到咖啡店后,发现咖啡还没做好,于是站在柜台前一直等

非阻塞IO:轮询。你到咖啡店后如果没好,那你就去附近晃悠一会儿,然后再问咖啡时候做好了,直到店员说好了


真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待

当我们发起 aio_read(异步 I/O)之后,就立即返回

内核自动将数据从内核空间拷贝到用户空间

这个拷贝过程同样是异步的,内核自动完成的

和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作

http://www.lryc.cn/news/587339.html

相关文章:

  • Java设计模式之行为型模式(命令模式)
  • Spring Boot + MyBatis 实现用户登录功能详解(基础)
  • JAVA学习笔记 JAVA开发环境部署-001
  • 深入分析---虚拟线程VS传统多线程
  • 力扣刷题记录(c++)09
  • 在 OCI 生成式 AI 上搭一个「指定地区拉面店 MCP Server」——从 0 到 1 实战记录
  • opencv中contours的使用
  • 【设计模式】策略模式(政策(Policy)模式)
  • Java小白-设计模式
  • Java 接口 剖析
  • 操作系统-第四章存储器管理和第五章设备管理-知识点整理(知识点学习 / 期末复习 / 面试 / 笔试)
  • 什么是渐进式框架
  • 什么时候会用到 concurrent.futures?要不要背?
  • 17.使用DenseNet网络进行Fashion-Mnist分类
  • 2024CVPR:Question Aware Vision Transformer for Multimodal Reasoning介绍
  • Action-Agnostic Point-Level Supervision for Temporal Action Detection
  • 【读书笔记】《Effective Modern C++》第4章 Smart Pointers
  • 从零开始学习深度学习—水果分类之PyQt5App
  • gcc 源码阅读--C语言预处理
  • 深度学习16(对抗生成网络:GAN+自动编码器)
  • 深入理解 Java JVM
  • Java: OracleHelper
  • MYSQL笔记2
  • 线性基学习笔记
  • 查看Linux服务器显卡使用情况的详细教程
  • 【UE教程/进阶】使用Slate
  • 【unitrix】 5.0 第二套类型级二进制数基本结构体(types2.rs)
  • SQL预编译:安全高效数据库操作的关键
  • 苍穹外卖Day3
  • markdown-it-mathjax3-pro —— 新一代 Markdown 数学公式渲染插件