Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一文读懂五大 IO 模型的前世今生( select、epoll、epoll)

一文读懂五大 IO 模型的前世今生( select、epoll、epoll)

原创
作者头像
Lorin 洛林
发布于 2024-07-08 11:17:45
发布于 2024-07-08 11:17:45
1.3K0
举报
文章被收录于专栏:操作系统操作系统

序言

  • 计算机编程中,IO模型是描述程序与输入/输出操作之间交互方式的抽象概念。不同的IO模型可以影响程序的性能、可扩展性和资源利用效率。我们常见有五种 IO 模型:阻塞式 IO、非阻塞式 IO 、IO 多路复用、信号驱动 IO、异步 IO。

阻塞式 IO

服务端如何处理客户端请求

  • 服务端为了处理客户端的连接和数据处理,可以按照以下伪代码实现:
代码语言:Java
AI代码解释
复制
listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  int n = read(connfd, buf);  // 阻塞 读数据
  doSomeThing(buf);  // 处理数据
  close(connfd);     // 关闭连接
}
  • 从上面的伪代码中我们可以看出,服务端处理客户端的请求阻塞在两个地方,一个是 accept、一个是 read ,我们这里主要研究 read 的过程,可以分为两个阶段:等待读就绪(等待数据到达网卡 & 将网卡的数据拷贝到内核缓冲区)、读数据。

阻塞式 IO

  • 上述场景中,read 的第一个阶段阻塞的,这就是我们常说的阻塞式 IO,即如果read 第一个阶段等待读就绪是阻塞的,我们就称为阻塞式IO:

非阻塞式 IO

伪非阻塞(多线程)

  • 为了让上面操作中的读操作 read 不再主线程中阻塞,我们可以使用多线程实现非阻塞:
代码语言:Java
AI代码解释
复制
listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  newThreadDeal(connfd)       // 当有新连接建立时创建一个新线程处理连接
}

newThreadDeal(connfd){
  int n = read(connfd, buf);  // 阻塞 读数据
  doSomeThing(buf);  // 处理数据
  close(connfd);     // 关闭连接 
}

真正的非阻塞式 IO

  • 伪非阻塞(多线程)实现方案是通过创建多线程的方式来处理不同的连接从而避免主线程阻塞,但实际上子线程内部读操作 read 还是阻塞的,这只是用户层的小把戏。
  • 真正实现非阻塞式 IO 我们应该让操作系统提供一个非阻塞的 read() 函数,当第一阶段读未就绪时返回 -1 ,当读已就绪时才进行数据的读取。
代码语言:Java
AI代码解释
复制
arr = new Arr[];
listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  arr.add(connfd);
}

// 异步线程检测 连接是否可读
new Tread(){
  for(connfd : arr){
    // 非阻塞 read 最重要的是提供了我们在一个线程内管理多个文件描述符的能力
    int n = read(connfd, buf);  // 检测 connfd 是否可读
    if(n != -1){
       newThreadDeal(buf);   // 创建新线程处理
       close(connfd);        // 关闭连接 
       arr.remove(connfd);   // 移除已处理的连接
    }
  }
}

newTheadDeal(buf){
  doSomeThing(buf);  // 处理数据
}
  • 从上面我们可以看出:所谓非阻塞 IO 是将第一阶段的等待读就绪改为非阻塞,但是第二阶段的数据读取还是阻塞的,非阻塞 read 最重要的是提供了我们在一个线程内管理多个文件描述符的能力。
  • 非阻塞式 IO 流程图:

IO 多路复用

  • 上面服务端通过多线程的方式处理客户端请求实现了主线程的非阻塞,使用不同线程处理不同的连接请求,但是我们并没有那么多的线程资源,并且等待读就绪的过程是耗时最多的,那么有没有什么办法可以将连接保存起来,等读已就绪时我们再进行处理。
  • 基于非阻塞式 IO ,一些聪明的小伙伴可能会这样实现(即上文的示例):
代码语言:Java
AI代码解释
复制
arr = new Arr[];
listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  arr.add(connfd);
}

// 异步线程检测 连接是否可读
new Tread(){
  for(connfd : arr){
    // 还有一个弊端:可读 connfd 只能串行处理
    // 获取直接开多线程处理连接 但线程资源有限
    int n = read(connfd, buf);  // 检测 connfd 是否可读
    if(n != -1){
       newThreadDeal(buf);   // 创建新线程处理
       close(connfd);        // 关闭连接 
       arr.remove(connfd);   // 移除已处理的连接
    }
  }
}

newTheadDeal(buf){
  doSomeThing(buf);  // 处理数据
}
  • 上面的实现看着很不错,但是却存在一个很大的问题,我们需要不断的调用 read() 进行系统调用,这里的系统调用我们可以理解为分布式系统的 RPC 调用,性能损耗十分严重,因为这依然是用户层的一些小把戏。
  • 这时我们自然而然就会想到把上述循环检测连接(文件描述符)可读的过程交给操作系统去做,从而避免频繁的进行系统调用。当然操作系统给我们提供了这样的函数:select、poll、epoll。

select

  • select 是操作系统提供的系统函数,通过它我们可以将文件描述符发送给系统,让系统内核帮我们遍历检测是否可读,并告诉我们进行读取数据。
代码语言:Java
AI代码解释
复制
arr = new Arr[];
listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  arr.add(connfd);
}

// 异步线程检测 通过 select 判断是否有连接可读
new Tread(){
  while(select(arr) > 0){
    for(connfd : arr){
      if(connfd can read){
        // 如果套接字可读 创建新线程处理
        newTheadDeal(connfd);
        arr.remove(connfd);   // 移除已处理的连接
      }
    }
  }
}

newTheadDeal(connfd){
    int n = read(connfd, buf);  // 阻塞读取数据
    doSomeThing(buf);  // 处理数据
    close(connfd);        // 关闭连接 
}
  • 从上面我们可以看出 select 运行的整个流程:

减少大量系统调用但也存在一些问题

  • 每次调用需要在用户态和内核态之间拷贝文件描述符数组,在高并发场景下这个拷贝的消耗是很大的。
  • 内核检测文件描述符可读还是通过遍历实现,当文件描述符数组很长时,遍历操作耗时也很长。
  • 内核检测完文件描述符数组后,当存在可读的文件描述符数组时,用户态需要再遍历检测一遍。

poll

  • poll 和 select 原理基本一致,最大的区别是去掉了最大 1024 个文件描述符的限制。
  • select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。
  • poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

epoll

  • 大家还记得上面 select/poll 存在的三个问题?epoll 主要优化了上面三个问题实现。
代码语言:txt
AI代码解释
复制
- 每次调用需要在用户态和内核态之间拷贝文件描述符数组,但高并发场景下这个拷贝的消耗是很大的。
方案:内核中保存一份文件描述符,无需用户每次传入,而是仅同步修改部分。

- 内核检测文件描述符可读还是通过遍历实现,当文件描述符数组很长时,遍历操作耗时也很长。
方案:通过事件唤醒机制唤醒替代遍历。

- 内核检测完文件描述符数组后,当存在可读的文件描述符数组时,用户态需要再遍历检测一遍。
方案:仅将可读部分文件描述符同步给用户态,不需要用户态再次遍历。
  • epoll 基于高效的红黑树结构,提供了三个核心操作,主要流程如下所示:
  • 伪代码:
代码语言:Java
AI代码解释
复制
listenfd = socket();   // 打开一个网络通信套接字
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
int epfd = epoll_create(...); // 创建 epoll 对象
while(1) {
  connfd = accept(listenfd);  // 阻塞 等待建立连接
  epoll_ctl(connfd, ...);  // 将新连接加入到 epoll 对象
}

// 异步线程检测 通过 epoll_wait 阻塞获取可读的套接字
new Tread(){
  while(arr = epoll_wait()){
    for(connfd : arr){
        // 仅返回可读套接字
        newTheadDeal(connfd);
    }
  }
}

newTheadDeal(connfd){
    int n = read(connfd, buf);  // 阻塞读取数据
    doSomeThing(buf);  // 处理数据
    close(connfd);        // 关闭连接 
}

边缘触发和水平触发

  • select/poll 只有水平触发模式,epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT),epoll 默认的触发模式是水平触发。

边缘触发

  • 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完。

水平触发

  • 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取。

事件驱动 IO

  • 发起读请求后,等待读就绪事件通知再进行数据读取。

异步 IO

  • 发起读请求后,等待操作系统读取完成后通知,完全将功能交给操作系统实现。

总结

  • IO 分为等待读就绪和读取数据两个阶段,阻塞和非阻塞指的是等待读就绪阶段。
  • IO 模型发展从阻塞 read 函数开始,它整个过程都是阻塞的,为了解决这个问题,我们在用户态通过异步线程实现主线程的非阻塞,但是子线程的 read 过程还是阻塞的,但是线程资源是有限的,且等待读就绪的过程是耗时最多的环节,因此我们在一个线程内通过 while 和非阻塞 read 的能力避免等待读就绪过程中线程资源占用,后来操作系统发现这个场景比较多,便提供了 select、epoll、epoll 函数实现上述功能来减少系统调用。我们会发现,包括后面的异步 IO 我们其实是把更多的功能交给了操作系统实现。
  • 比如大家常说的 IO 多路复用效率之所以高是因为可以通过一个线程管理多个文件描述符,当然这也是其中一个原因,另外一个原因是因为减少了大量的系统调用。

参考

个人简介

👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.

🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。

🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。

💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。

🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。

📖 保持关注我的博客,让我们共同追求技术卓越。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Spring | Bean自动装配详解
由于在手动配置xml过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。
啵啵鱼
2022/11/23
8410
Spring | Bean自动装配详解
spring框架应用系列一:annotation-config自动装配
本文系作者原创,转载请注明出处:http://www.cnblogs.com/further-further-further/p/7716678.html
用户7225427
2020/09/03
5630
spring框架应用系列一:annotation-config自动装配
【Spring实战】—— 8 自动装配
本篇介绍一下自动装配的知识,Spring为了简化配置文件的编写。采用自动装配方式,自动的装载需要的bean。 自动装配 有以下几种方式:   1 byName 通过id的名字与属性的名字进行判断,要保证Bean实例中属性名字与该装配的id名字相同。   2 byType 通过类型确定装配的bean,但是当存在多个类型符合的bean时,会报错。   3 contructor 在构造注入时,使用该装配方式,效果如同byType。   4 autodetect 自动装配,这个测试了,3.0.5版本不可用了
用户1154259
2018/01/17
5620
Spring之IOC自动装配
jdk1.5支持的注解,Spring2.5就支持注解了! The introduction of annotation-based configuration raised the question of whether this approach is "better"than XML.(基于注释的配置的引入引发了这样一个问题:这种方法是比XML“更好”。)
楠羽
2022/11/18
3880
Spring之IOC自动装配
Spring-04 Bean的自动装配
由于在手动配置xml过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。
张小驰出没
2021/04/15
6740
Spring-04  Bean的自动装配
Spring Bean 的装配方式以及Autowired与Resource的使用及区别
在Spring的使用中,如果要将一个bean实例化,可以通过配置文件,也可以通过在java代码里面的注解来实现,Spring能够根据自动协作这些bean之间的关系,这种自动协作的过程,也称之为自动装配。 自动装配模式有如下四种模式:
冬天里的懒猫
2021/09/08
7650
spring框架应用系列二:component-scan自动扫描注册装配
本文系作者原创,转载请注明出处:http://www.cnblogs.com/further-further-further/p/7717331.html
用户7225427
2020/09/03
9250
spring框架应用系列二:component-scan自动扫描注册装配
spring之自动装配的三种方式
springIOC容器可以自动装配Bean,需要做的仅仅是在<bean>中的autowire属性里指定自动装配的方式。
西西嘛呦
2020/08/26
4660
Spring 框架学习(七)---- bean自动装配、注解开发
自动装配就是给bean中的属性进行设置值进行注入,如果是引用类型的话,spring会在上下文中进行查找进行装配属性。
RAIN7
2022/06/30
4810
Spring 框架学习(七)---- bean自动装配、注解开发
Spring 学习笔记(五)—— Bean之间的关系、作用域、自动装配
  Spring提供了配置信息的继承机制,可以通过为<bean>元素指定parent值重用已有的<bean>元素的配置信息。
Rekent
2018/09/04
5430
Spring 学习笔记(五)—— Bean之间的关系、作用域、自动装配
【Spring】005-Bean的自动装配
②在java中显式配置(见【Spring】007-使用JavaConfig实现配置);
訾博ZiBo
2025/01/06
1450
【Spring】005-Bean的自动装配
Spring学习3:自动装配与注解开发
例子说明:创建一个User来控制一个Cat,使用Cat的shout方法。(一个人让自己的猫叫) 1.新建cat类
zstar
2022/06/14
2070
Spring学习3:自动装配与注解开发
Spring之IOC容器
spring core提供了IOC,DI,Bean配置装载创建的核心实现 核心概念: Beans、BeanFactory、BeanDefinitions、ApplicationContext
Java微观世界
2025/01/21
2690
Spring之IOC容器
[Spring]04_最小化Spring XML配置
静默虚空
2018/01/05
6610
Spring注解-@Autowired注解使用
首先要知道另一个东西,default-autowire,它是在xml文件中进行配置的,可以设置为byName、byType、constructor和autodetect;比如byName,不用显式的在bean中写出依赖的对象,它会自动的匹配其它bean中id名与本bean的set**相同的,并自动装载。 @Autowired是用在JavaBean中的注解,通过byType形式,用来给指定的字段或方法注入所需的外部资源。 两者的功能是一样的,就是能减少或者消除属性或构造器参数的设置,只是配置地方不一样而已。 autowire四种模式的区别
SerMs
2022/04/11
1.1K0
Spring注解-@Autowired注解使用
跟着柴毛毛学Spring(3)——简化Bean的配置
通过前面的学习,我们会感觉到对于一个有较多Bean的大项目,Spring的配置会比较复杂。那么接下来我们就介绍如何简化Spring的配置。 简化Spring的配置主要分为两类: 1. 自动装配 2. 自动扫描 下面就详细介绍这两种简化配置的方式。 自动装配 自动装配的种类 byName:根据属性的名字自动装配 byType:根据属性的类型自动装配 constructor:根据构造器的参数类型自动装配 autodetect:最佳自动装配。首先采用constructor自动装配,若没有发现与构造器相匹配
大闲人柴毛毛
2018/03/09
7980
第2章—装配Bean—自动化装配Bean
CD和CDPlayer,如果你不将CD放入(注入)到播放器中,那么CD播放器其实是没多大用处的,所以说,CD播放器是依赖于CD才能完成它的使命.
Dream城堡
2018/09/10
4190
Spring的自动装配
坏处:依赖不能明确管理,可能会有多个bean同时符合注入规则,没有清晰的依赖关系。
SerMs
2022/04/11
6560
Spring的自动装配
【Spring实战】—— 6 内部Bean
本篇文章讲解了Spring的通过内部Bean设置Bean的属性。   类似内部类,内部Bean与普通的Bean关联不同的是:   1 普通的Bean,在其他的Bean实例引用时,都引用同一个实例。   2 内部Bean,每次引用时都是新创建的实例。   鉴于上述的场景,内部Bean是一个很常用的编程模式。   下面先通过前文所述的表演者的例子,描述一下主要的类: package com.spring.test.setter; import com.spring.test.action1.Per
用户1154259
2018/01/17
6100
3. 装配 Bean
通过 ClassPathXmlApplicationContext 进行文件加载。
acc8226
2022/05/17
4600
3. 装配 Bean
相关推荐
Spring | Bean自动装配详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档