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

Linux之【网络I/O】前世今生(二)

前文回顾

在这里插入图片描述

通过学习 Linux之【网络I/O】前世今生(一),我们知道了I/O 请求可以分为两个阶段,分别为 I/O 调用和 I/O 执行:

  • I/O 调用

即用户进程向内核发起系统调用(通过 0x80 中断)。

  • I/O 执行

内核等待 I/O 请求处理完成返回。该阶段分为两个过程:

  1. 等待数据就绪,并写入内核缓冲区;
  2. 将内核缓冲区数据拷贝至用户态缓冲区。

一、 网络 I/O 模型

在 Linux之【磁盘 IO】前世今生 一文中,我们提到了 I/O的三种模型:同步阻塞I/O (BIO)、同步非阻塞I/O (NIO)、异步I/O (AIO)。

对于网络I/O,我们是否可以采用上述I/O模型呢 ?

网络I/O 会涉及 Socket ,且同一时间可能有大量 Socket 连接和读写操作,需要考虑并发量和性能……

  • 首先可以排除异步I/O,因为Linux 异步I/O最初是为数据库设计的;
  • 同步阻塞I/O (BIO):考虑并发情况的话,只能使用多线程模型,一个请求对应一个线程。但是,线程的资源是有限且宝贵的,创建过多的线程会增加线程切换的开销。
    • 请求数只有 100 个时,这种方式自然没问题,但增加到 10000 个请求时,10000 个进程或线程的调度、上下文切换乃至它们占用的内存,都会成为瓶颈。
  • 同步非阻塞I/O (NIO):NIO 相比 BIO 虽然大幅提升了性能,但是轮询过程中大量的系统调用导致上下文切换开销很大。所以,单独使用 NIO 时效率并不高,而且随着并发量的提升,非阻塞 I/O 会存在严重的性能浪费。

1.1 I/O多路复用(I/O Multiplexing)

针对上文分析结果,同时结合网络I/O 的并发量考虑,我们改进下 NIO 模型:

  • NIO 轮询产生的多次上下文切换,我们可以减少,正如我们我们之前用 sendfile 替代 [ read + write ] 一样,我们把轮询工作放在内核态进行;
  • 所有 Socket 都注册到内核里,由内核统一负责监听所有Socket的I/O数据是否就绪;
  • 线程资源有限且宝贵,我们通过减少线程数量来降低线程切换开销,一个用户线程负责处理多个Socket;
  • 任一Socket 的 I/O 就绪,内核就通知应用程序;

我们用 多路复用器(Selector)来封装上述改进结果,即通过一次系统调用,检查多个Socket的状态,将这种改进后 I/O 模型记为I/O多路复用(I/O Multiplexing)

  • 多路复用实现了一个线程处理多个 I/O 句柄的操作。多路指的是多个数据通道,复用指的是使用一个或多个固定线程来处理每一个 Socket。线程一次 select 调用可以获取内核态中多个数据通道的数据状态。多路复用解决了同步阻塞 I/O 和同步非阻塞 I/O 的问题,是一种非常高效的 I/O 模型。
  • I/O 多路复用内部实现需要使用非阻塞 I/O
    • 多路复用内部需要遍历 Socket 集合,如果使用阻塞I/O,当前遍历 Socket 数据未就绪会阻塞,那么后续 Socket 将没有机会遍历检查,直到当前阻塞解除;
    • 注意:这里说的是 I/O 多路复用内部实现,而不是说,使用 I/O 多路复用就必须使用非阻塞 I/O。

在这里插入图片描述

Linux中 I/O 多路复用的具体实现有:select、poll、epoll ;kueue 是在UNIX上比较高效的IO复用技术。

1.1.1 select

select 允许应用程序监视一组 Socket 文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。调用 select 会一直阻塞直到有描述符的事件到达或者等待超时(timeout 参数精度为微秒)。

具体实现方式如下:
  1. 将已连接的 Socket 文件描述符放到一个 BitsMap 数组,然后调用 select 函数将 BitsMap 数组拷贝到内核里;
  2. 内核通过遍历BitsMap 数组,来检查是否有网络事件产生,当检查到有事件产生后,将此 Socket 标记为可读或可写;
  3. BitsMap 数组从内核拷贝到用户态;
  4. 用户态遍历 BitsMap 找到可读写的Socket,然后再对其处理。
int select(int nfds,   // 需要监听的 socket (也叫文件描述符(file descriptor))数量; 
         fd_set *restrict readfds, // 可读集合
         fd_set *restrict writefds,// 可写集合
         fd_set *restrict errorfds,// 异常集合

		 // 超时设置,NULL,会无限阻塞直到某个描述符就绪;如果 timeout 参数设为 0,会立即返回,不阻塞。
         struct timeval *restrict timeout);
  • 数组遍历时间复杂度为O(n);
  • 每次调用BitsMap需要拷贝2次,高并发场景下这样的拷贝消耗的资源是惊人的;
  • BitsMap 长度由参数 FD_SETSIZE 限制,默认最大值为1024,即只能监听0~1023的文件描述符;
应用场景
  • select 的 timeout 参数精度为微秒,因此 select 更加适用于实时性要求比较高的场景;
  • select 可移植性更好,几乎被所有主流平台所支持;
  • 同时监控小于 1000 个描述符。

1.1.2 poll

为了摆脱select文件描述符最大个数的限制(当然还会受到系统文件描述符限制),poll 在内核态将文件描述符数组改为链表,链表节点内容基于pollfd

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
    int fd;           // 要监听的文件描述符
    short events;     // 要监听的事件 读事件、写事件、异常事件
    short revents;    // 文件描述符fd上实际发生的事件
};
  • 链表遍历时间复杂度为O(n);
  • poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高;
  • select 会修改描述符,而 poll 不会。
应用场景

poll 的 timeout 参数精度为毫秒,且poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,可以使用 poll 而不是 select。

1.1.3 epoll

Linux 2.6 新增的 epoll(eventpoll,事件驱动的 poll) 是对 select 和 poll 的改进,解决了 “全量复制集合” 和 “文件描述符数量少” 这两个缺点,是性能最高的多路复用实现方式,能支持的并发量也是最大。

kueue 是 unix (FreeBSD,NetBSD, OpenBSD, DragonFly BSD, macOS)中用来替换select和poll的。

改进点如下:
  • 内核使用红黑树存储所有需要监听的 Socket 连接,每个Socket 连接只需在添加(通过系统调用epoll_ctl)时传入一次,无需用户每次都重新传入;

  • 内核用链表存储就绪的 Socket 连接;这样用户态只需处理该链表即可,无需遍历整个Socket集合;

  • 内核采用事件驱动机制来监听Socket是否就绪,避免轮询扫描全部 Socket 连接;

// 创建一个 epoll 实例,同时返回一个引用该实例的文件描述符。
int epoll_create(int size);

// 监听文件描述符 fd 上发生的 event 事件。op 表示要对 fd 执行的操作,添加,删除、更新
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

// 功能相当于 select/poll。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
               
struct eventpoll {
 wait_queue_head_t wq;      // 等待队列链表,存放阻塞的进程
 struct list_head rdllist;  // 存放数据就绪的 socktet
 struct rb_root rbr;        // redBlackTree 红黑树,管理用户进程下添加进来的所有 socket 连接
        ......
}

在这里插入图片描述

  • 红黑树查找,时间复杂度为O(log n);
应用场景
  • epoll 的 timeout 参数精度为毫秒,有大量的描述符需要同时轮询,并且这些连接是长连接、不活跃;
  • C10K、C100K、C1000K:即单机同时处理 1 万(十万、百万)个请求(并发连接 1 万、10万、100万)的问题,首字母 C 是 Client 或者Concurrent的缩写。再往上走到 C10M问题 用 epoll 已经搞不定了?得用用户态网络协议栈,扯远了……

1.1.4 select vs poll vs epoll

项目selectpollepoll
socket 数据就绪复制数量全部复制(包含没有就绪的)全部复制(包含没有就绪的)仅复制数据就绪集合
socket集合复制次数每次调用和返回均需复制每次调用和返回均需复制仅第一次调用复制,后续返回仅复制少量数据
集合大小有限制没有限制没有限制
数据就绪机制遍历遍历事件驱动
集合底层结构数组链表红黑树
时间复杂度O (n)O (n)O (log n)
阻塞超时精度微秒毫秒毫秒
使用场景实时性要求比较高,并发低于1000实时性要求不高,并发稍微高于1000海量并发,长连接、不活跃

1.1.5 I/O 事件通知的方式

  • 水平触发(LT,Level Trigger):当数据就绪时,会触发通知,如果用户程序没有一次性把数据读/写完,下次还会发出可读/可写信号进行通知。

  • 边缘触发(ET,Edge Trigger):仅当数据从未就绪变为就绪时,通知一次,之后不会再通知。

    边缘触发时,应用程序需要尽可能多地执行 I/O,直到无法继续读写,才可以停止。如果 I/O 没执行完,或者因为某种原因没来得及处理,那么这次通知也就丢失了。

  • 水平触发、边缘触发的名称来源:数字电路当中的电位水平,高低电平切换瞬间的触发动作叫边缘触发,而处于高电平的触发动作叫做水平触发。
  • 区别:边缘触发效率更高,减少了事件被重复触发的次数,函数不会返回大量用户程序可能不需要的文件描述符。
  • select 、poll 只支持水平触发;
  • epoll 支持水平触发和边缘触发;

1.2 信号I/O

信号驱动 I/O 并不常用,它是一种半异步的 I/O 模型。在使用信号驱动 I/O 时,当数据准备就绪后,内核通过发送一个 SIGIO 信号通知应用进程,应用进程就可以开始读取数据了。

在这里插入图片描述

信号I/O和异步I/O区别:

  • 信号I/O,内核只负责发信号通知用户态,有没有数据;即信号在数据就绪时发送;
  • 异步I/O,内核会帮助用户态拷贝数据;即在数据拷贝完成时通知用户。

二、 I/O模型总结对比

在 Linux之【磁盘 IO】前世今生 我们介绍了阻塞I/O 、非阻塞I/O和异步I/O,本文我们梳理了I/O多路复用和信号I/O,至此,I/O 5 种模型已介绍完毕,对比如下:

在这里插入图片描述

Linux 前世今生系列文章:

  • Linux之【网络I/O】前世今生(二)

  • Linux之【网络I/O】前世今生(一)

  • Linux之【磁盘 IO】前世今生

  • Linux之【文件系统】前世今生(二)

  • Linux之【文件系统】前世今生(一)

  • Linux之【内存管理】前世今生(一)


http://www.kler.cn/a/548877.html

相关文章:

  • 基于巨控GRM242Q-4D4I4QHE模块的农村供水自动化监控技术方案
  • 【计算机网络】网络层数据包(Packet)格式
  • 箭头函数(js)
  • 软件运维资料-系统移交运维计划(原件)
  • pandas(11 分类数据和数据可视化)
  • 仿叮咚买菜鸿蒙原生APP
  • AAOS Audio 快问快答,快速理解系统特点
  • 【第10章:自然语言处理高级应用—10.3 NLP在智能客服、舆情分析与情感倾向判断中的应用案例】
  • ECharts 实战指南:组件封装+地图轮廓高亮 + 自定义 Tooltip+轮播+锥形柱子
  • 【MODIS\VIIRS】h5文件信息查看python代码
  • Pytorch深度学习教程_3_初识pytorch
  • 基于AWS云平台的法律AI应用系统开发方案
  • 【iOS】包大小和性能稳定性优化
  • 如何测试和验证CVE-2024-1430:Netgear R7000 路由器信息泄露漏洞分析
  • 详细介绍:封装简易的 Axios 函数获取省份列表
  • 【云安全】云原生-K8S(四)安全问题分析
  • 【前端】自己从头实现一个gpt聊天页面
  • 用命令模式设计一个JSBridge用于JavaScript与Android交互通信
  • Visionpro 齿轮测量
  • DeepSeek-R1使用生存指南