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

BIO、NIO、IO多路复用(select/poll/epoll)、信号驱动IO、异步IO

BIO、NIO、IO多路复用【select/poll/epoll】、信号驱动IO、异步IO

    • Linux用户空间和内核空间
    • 阻塞IO(Blocking IO)
    • 非阻塞IO(Noblocking IO)
    • IO多路复用(IO Multilpexing)
      • select
      • poll
      • epoll
    • 信号驱动IO(Signal Driven IO)
    • 异步IO(Asynchronous IO)
    • 五种IO模型对比

Linux用户空间和内核空间

程序运行时,为了避免用户应用发生冲突,甚至发生系统冲突,用户应用和内核是分类:
进程的寻址空间会划分为两部分:内核空间、用户空间。
当进程运行在用户空间时,我们称之为用户态,当进程运行在内核空间的时候,我们称之为内核态。

  • 用户空间只能执行受限的命令,而且不能调用系统资源,必须通过内核提供的接口来访问。
  • 内核空间可以执行特权命令,调用一切资源。

Linux为了提高IO效率会在用户空间和内核空间都加入缓冲区:

  • 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写去设备。
  • 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区。

其中读数据的过程如下所示:
在这里插入图片描述
读数据主要分为两部分:

  1. 等待数据就绪:我们向内核空间发起请求,等待数据,内核空间将数据准备好之后,放到内核的缓冲区。
  2. 从内核缓冲区拷贝数据到用户缓冲区。然后用户程序就可以对用户缓冲区内的数据进行操作。

阻塞IO(Blocking IO)

阻塞IO指的是在 等待数据阶段由内核拷贝数据到用户空间阶段 都阻塞等待的IO。

等待数据阶段: 用户进程尝试读取数据,此时数据尚未到达,内核需要等待数据,此时用户线程也处于阻塞状态。
拷贝数据阶段: 数据到达并拷贝到内核缓冲区,代表已经就绪;将内核数据拷贝到用户缓冲区;拷贝过程中,用户进程依然阻塞等待;拷贝完成,用户进程阻塞解除,处理数据。
在这里插入图片描述
用户应用调用recvfrom读取数据,数据未准备好,用户应用阻塞等待,直到1,2都完成才能解除阻塞等待。

非阻塞IO(Noblocking IO)

非阻塞IOrecvfrom操作会立即返回结果而不是阻塞用户进程。

等待数据阶段:
用户进程尝试读取数据,此时数据尚未到达,内核需要等待数据,返回异常给用户进程,用户进程拿到error之后,再次读取失败,之后不停调用recvfrom,直到数据就绪。

数据拷贝阶段:
用户进程阻塞等待,直到拷贝完成,用户进程解除阻塞,处理数据。

在这里插入图片描述
虽然第二个阶段是非阻塞状态,但是性能并没有得到提高,而且忙等机制会导致CPU空转,CPU使用率暴增。

IO多路复用(IO Multilpexing)

文件描述符File DescriptorFD, 是一个从0开始的无符号整数,用来关联Linux中的一个文件。在Linux中一切皆文件,例如常规文件、视频、硬件设备等,也包括网络套接字。

IO多路复用是利用单个线程来同时监听多个FD,并在FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。

等待数据阶段:用户进程调用select,指定要监听的FD集合,内核监听FD对应的多个socket,任意一个或多个socket数据就绪就返回readable,该过程中用户线程阻塞。

数据拷贝阶段:用户线程找到就绪的socket,依次调用recvfrom读取数据,内核将数据拷贝到用户空间,用户进程处理数据。
在这里插入图片描述
IO多路复用中监听FD的方式,有多种实现,常见的有:selectpollepoll
其中:

  • selectpoll只会通知用户进程有FD就绪,但不确定是哪个FD,需要用户进程遍历FD来确认。
  • epoll则会在通知用户进程FD就绪的同时,把已经就绪的FD写入用户空间。

select

select的部分代码如下:

// 定义类型别名 __fd_mask,本质是 long int
typedef long int __fd_mask;
/* fd_set 记录要监听的fd集合,及其对应状态 */
typedef struct {
    // fds_bits是long类型数组,长度为 1024/32 = 32
    // 共1024个bit位,每个bit位代表一个fd,0代表未就绪,1代表就绪
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    // ...
} fd_set;
// select函数,用于监听fd_set,也就是多个fd的集合
int select(
    int nfds, // 要监视的fd_set的最大fd + 1
    fd_set *readfds, // 要监听读事件的fd集合
    fd_set *writefds,// 要监听写事件的fd集合
    fd_set *exceptfds, // // 要监听异常事件的fd集合
    // 超时时间,null-用不超时;0-不阻塞等待;大于0-固定等待时间
    struct timeval *timeout
);

其中,存储FD的数据结构为fd_set, fd_set中有一个数组fds_bits,其大小为32,fds_bits数组的类型为long int4字节,因此总共为32x4x8=1024位,每个bit位代表一个fd0代表未就绪,1代表就绪

其过程如下:
在这里插入图片描述
在这里插入图片描述
select模式存在的问题

  • 需要将整个fd_set从用户空间拷贝到内核空间,select结束还要再次拷贝回用户空间
  • select无法得知具体是哪个fd就绪,需要遍历整个fd_set
  • fd_set监听的fd数量不能超过1024

poll

poll的部分代码如下:

// pollfd 中的事件类型
#define POLLIN     //可读事件
#define POLLOUT    //可写事件
#define POLLERR    //错误事件
#define POLLNVAL   //fd未打开

// pollfd结构
struct pollfd {
    int fd;     	  /* 要监听的fd  */
    short int events; /* 要监听的事件类型:读、写、异常 */
    short int revents;/* 实际发生的事件类型 */
};
// poll函数
int poll(
    struct pollfd *fds, // pollfd数组,可以自定义大小
    nfds_t nfds, // 数组元素个数
    int timeout // 超时时间
);

IO流程:

  1. 创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
  2. 调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
  3. 内核遍历fd,判断是否就绪
  4. 数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
  5. 用户进程判断n是否大于0
  6. 大于0则遍历pollfd数组,找到就绪的fd

select对比:

  • select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
  • 虽然fd数量无限制,但是监听FD越多,每次遍历消耗时间也越久,性能反而会下降

epoll

epoll模式是对select和poll的改进,它提供了三个函数:

struct eventpoll {
    //...
    struct rb_root  rbr; // 一颗红黑树,记录要监听的FD
    struct list_head rdlist;// 一个链表,记录就绪的FD
    //...
};
// 1.创建一个epoll实例,内部是event poll,返回对应的句柄epfd
int epoll_create(int size);
// 2.将一个FD添加到epoll的红黑树中,并设置ep_poll_callback
// callback触发时,就把对应的FD加入到rdlist这个就绪列表中
int epoll_ctl(
    int epfd,  // epoll实例的句柄
    int op,    // 要执行的操作,包括:ADD、MOD、DEL
    int fd,    // 要监听的FD
    struct epoll_event *event // 要监听的事件类型:读、写、异常等
);
// 3.检查rdlist列表是否为空,不为空则返回就绪的FD的数量
int epoll_wait(
    int epfd,                   // epoll实例的句柄
    struct epoll_event *events, // 空event数组,用于接收就绪的FD
    int maxevents,              // events数组的最大长度
    int timeout   // 超时时间,-1用不超时;0不阻塞;大于0为阻塞时间
);

在这里插入图片描述
select模式存在的三个问题

  • 能监听的FD最大不超过1024
  • 每次select都需要把所有要监听的FD都拷贝到内核空间
  • 每次都要遍历所有FD来判断就绪状态

`poll模式的问题

  • poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降

epoll模式

  • 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
  • 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
  • 利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降

信号驱动IO(Signal Driven IO)

信号驱动IO是与内核建立SIGIO的信号关联并设置回调,当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。

等待数据阶段

  1. 用户进程调用sigaction,注册信号处理函数
  2. 内核返回成功,开始监听FD
  3. 用户进程不阻塞等待,可以执行其它业务
  4. 当内核数据就绪后,回调用户进程的SIGIO处理函数

数据拷贝阶段

  1. 收到SIGIO回调信号
  2. 调用recvfrom,读取
  3. 内核将数据拷贝到用户空间
  4. 用户进程处理数据

在这里插入图片描述
当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间的频繁信号交互性能也较低。

异步IO(Asynchronous IO)

异步IO的整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其它事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。

等待数据阶段

  1. 用户进程调用aio_read,创建信号回调函数
  2. 内核等待数据就绪
  3. 用户进程无需阻塞,可以做任何事情

数据拷贝阶段

  1. 内核数据就绪
  2. 内核数据拷贝到用户缓冲区
  3. 拷贝完成,内核递交信号触发aio_read中的回调函数
  4. 用户进程处理数据

异步IO模型中,用户进程在两个阶段都是非阻塞状态。

五种IO模型对比

在这里插入图片描述

相关文章:

  • Echarts y轴相关配置
  • 02.6 概率
  • 【web-渗透测试方法】(15.2)分析应用程序、测试客户端控件
  • 03.1线性回归
  • 【智能优化算法】基于觅食生境选择的改进粒子群算法(FHSPSO)附 Matlab代码
  • 密码相关----对称加密,非对称加密
  • 02.1、数据操作
  • 离散数学 --- 命题逻辑 -- 命题符号化与命题公式
  • 回坑记之或许是退役赛季?
  • 初识OpenGL (-)EBO元素缓冲对象(Element Buffer Object)
  • typescript真的有学习的必要吗?
  • PyTorch Lightning入门教程(二)
  • 【滤波跟踪】基于变分贝叶斯卡尔曼滤波器实现目标跟踪附matlab代码
  • C++ mutex 与 condition_variable
  • 基础 | Spring - [单例创建过程]
  • 8年软件测试工程师感悟——写给还在迷茫中的朋友
  • Android 架构优化~MVP 架构改造
  • Angular 响应式表单 基础例子
  • const let
  • CSS进阶篇--用CSS开启硬件加速来提高网站性能
  • es6
  • github从入门到放弃(1)
  • HTML中设置input等文本框为不可操作
  • JavaScript实现分页效果
  • Linux编程学习笔记 | Linux多线程学习[2] - 线程的同步
  • nginx 配置多 域名 + 多 https
  • Rancher如何对接Ceph-RBD块存储
  • Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)
  • SAP云平台里Global Account和Sub Account的关系
  • vue.js框架原理浅析
  • windows-nginx-https-本地配置
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 第13期 DApp 榜单 :来,吃我这波安利
  • 浮现式设计
  • 用jquery写贪吃蛇
  • 2017年360最后一道编程题
  • 正则表达式-基础知识Review
  • ​比特币大跌的 2 个原因
  • ​草莓熊python turtle绘图代码(玫瑰花版)附源代码
  • #LLM入门|Prompt#1.7_文本拓展_Expanding
  • #mysql 8.0 踩坑日记
  • (007)XHTML文档之标题——h1~h6
  • (4)事件处理——(7)简单事件(Simple events)
  • (PHP)设置修改 Apache 文件根目录 (Document Root)(转帖)
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (一)Neo4j下载安装以及初次使用
  • (转)AS3正则:元子符,元序列,标志,数量表达符
  • (转)linux自定义开机启动服务和chkconfig使用方法
  • (转)项目管理杂谈-我所期望的新人
  • .cn根服务器被攻击之后
  • .NET Core中的去虚
  • .NET MVC 验证码
  • .NET/C# 避免调试器不小心提前计算本应延迟计算的值
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)...
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复