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 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
IO 多路复用
为了讲多路复用,当然还是要跟风,采用鞭尸的思路,先讲讲传统的网络 IO 的弊端,用拉踩的方式捧起多路复用 IO 的优势。
敖丙
2021/07/27
9990
IO 多路复用
Netty如何做到单机百万并发?
今天给大家分享一篇万字长文《微言 Netty:百万并发基石上的 epoll 之剑》。
肉眼品世界
2021/06/08
9920
万字图解| 深入揭秘IO多路复用
     通过前面的文章我们已经了解了「数据包从HTTP层->TCP层->IP层->网卡->互联网->目的地服务器」以及「数据包怎么从网线到进程,在被应用程序使用」涉及的知识。      本文将继续介绍网络编程中的各种细节和IO多路复用的原理。
公众号 云舒编程
2024/01/25
4.9K3
万字图解| 深入揭秘IO多路复用
图文详解 epoll 原理【Redis,Netty,Nginx实现高性能IO的核心原理】epoll 详解
输入输出(input/output)的对象可以是文件(file), 网络(socket),进程之间的管道(pipe)。在linux系统中,都用文件描述符(fd)来表示。
一个会写诗的程序员
2021/03/24
12.5K1
图文详解 epoll 原理【Redis,Netty,Nginx实现高性能IO的核心原理】epoll 详解
深入学习IO多路复用select/poll/epoll实现原理
Linux 服务器处理网络请求有三种机制,select、poll、epoll,本文打算深入学习下其实现原理。
涂明光
2022/11/27
1.9K2
I/O多路复用select/poll/epoll
早期操作系统通常将进程中可创建的线程数限制在一个较低的阈值,大约几百个。因此, 操作系统会提供一些高效的方法来实现多路IO,例如Unix的select和poll。现代操作系统中,线程数已经得到了极大的提升,如NPTL线程软件包可支持数十万的线程。
WindSun
2019/09/09
1.4K0
I/O多路复用select/poll/epoll
彻底理解 IO多路复用
https://github.com/caijinlin/learning-pratice/tree/master/linux/io
范蠡
2020/08/18
1.5K0
linux 网络编程 I/O复用 select,poll ,epoll
http://blog.csdn.net/zs634134578/article/details/19929449
bear_fish
2018/09/20
2.8K0
epoll,求知者离我近点
上网一搜epoll,基本是这样的结果出来:《多路转接I/O – epoll模型》,万变不离这个标题。 但是呢,不变的事物,我们就更应该抓出其中的重点了。 多路、转接、I/O、模型。 别急,先记住这几个词,我比较喜欢你们看我文章的时候带着问题。
看、未来
2020/08/25
5520
epoll,求知者离我近点
select poll epoll
IO多路复用之select、poll、epoll详解 这一篇总结得好关于同步,异步,阻塞,非阻塞,IOCP/epoll,select/poll,AIO ,NIO ,BIO的总结
平凡的学生族
2019/05/25
1.1K0
详解I/O多路转接模型:select & poll & epoll
多路转接是IO模型的一种,这种IO模型通过select、poll或者epoll进行IO等待,可以同时等待多个文件描述符,当某个文件描述符的事件就绪,便会通知上层处理对应的事件。
二肥是只大懒蓝猫
2023/10/13
7360
详解I/O多路转接模型:select & poll & epoll
select和epoll模型
转自https://www.cnblogs.com/lojunren/p/3856290.html
大学里的混子
2019/03/14
1.2K0
一文读懂Redis中的多路复用模型
首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。
用户2781897
2021/04/02
1.1K0
IO及IO模型
IO,即Input/Output,指的是程序从外部设备或者网络读取数据到用户态内存/从用户态内存写数据到外部设备或者网络的过程。
月梦@剑心
2023/12/09
3190
IO及IO模型
Linux IO多路复用模型
流指的是可以进行I/O操作的内核对象,例如: 文件,管道和套接字等,流的入口就是文件描述符fd。
大忽悠爱学习
2022/09/29
8710
Linux IO多路复用模型
Linux内核编程--常见IO模型与select/poll/epoll编程
套接字上的数据传输分两步执行:第一步,等待网络中的数据送达,将送达后的数据复制到内核中的缓冲区。第二步,把数据从内核中的缓冲区拷贝到应用进程的缓冲区。整个过程的运行空间是从应用进程空间切换到内核进程空间然后再切换回应用进程空间。
Coder-ZZ
2022/06/23
1.6K0
Linux内核编程--常见IO模型与select/poll/epoll编程
TCP服务器的演变过程:IO多路复用机制select实现TCP服务器
本系列文章旨在带领大家逐步深入TCP服务器的开发世界,从最基础的一对一连接通信开始,逐步探索更复杂、更高效的服务器模型。从零开始,手把手地编写第一个TCP服务器程序,亲身体验“开局一块砖,大厦全靠垒”的成就感。
Lion 莱恩呀
2025/05/18
1210
TCP服务器的演变过程:IO多路复用机制select实现TCP服务器
「网络IO套路」当时就靠它追到女友
今天分享的基本上一面试就会被问的网络IO,文中涉及的代码部分不太重要,重要的是对这概念的理解。在看文章之前大家也可通过下面的思维导图看看自己是否能回答出来。
五分钟学算法
2020/09/27
5730
「网络IO套路」当时就靠它追到女友
一文读懂 Linux 网络 IO 模型
C 是 Client 单词首字母缩写,10K 指 1 万,C10K 指单机同时处理 1 万个并发连接问题。
恋喵大鲤鱼
2023/11/22
5200
一文读懂 Linux 网络 IO 模型
框架篇:linux网络I/O+Reactor模型
网络I/O,可以理解为网络上的数据流。通常我们会基于socket与远端建立一条TCP或者UDP通道,然后进行读写。单个socket时,使用一个线程即可高效处理;然而如果是10K个socket连接,或者更多,我们如何做到高性能处理?
潜行前行
2020/12/11
1.2K0
框架篇:linux网络I/O+Reactor模型
相关推荐
IO 多路复用
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档