高级I/O知识分享【5种IO模型 || select || poll】

博客主页:花果山~程序猿-CSDN博客

文章分栏:Linux_花果山~程序猿的博客-CSDN博客

关注我一起学习,一起进步,一起探索编程的无限可能吧!让我们一起努力,一起成长!

目录

一,前文

2,5种IO模型(钓鱼例子)

[3. 5种IO模型图](#3. 5种IO模型图)

4,进程同步&IO同步区分

那如何区分同步通信和异步通信?

5,阻塞与非阻塞区别

二,非阻塞IO

1.设置IO为非阻塞

[2. I/O多路转接(select)](#2. I/O多路转接(select))

参数理解

用select优化listen_socket

select的优缺点

3.I/O多路转接(poll)


嗨!收到一张超美的图,愿你每天都能顺心!

一,前文

我们曾经学习过类似IO的知识,例如接口:C语言的fwrite,fread,操作系统的read,write等对磁盘的IO,我们现在可以称他为------单机IO,本质上是内存到磁盘之间的读写操作,相比与网络IO,本地IO有着一些先天的优势(物理优势------近):

  1. 低延迟:由于数据交换不涉及网络传输,因此相对网络IO,单机IO的延迟较低。
  2. 简单直接:实现较为简单,通常使用操作系统提供的API(如C语言中的fopen, fread, fwrite等)直接进行操作。
  3. 可靠性高:相比网络环境,本地IO受外界干扰小,数据传输更可靠。

应用的场景多是对本地文件内容的读写。

网络IO本质也是对外设进行读写(接口如:send,recv) ,但由于需要经过长延迟的网络环境与经历的繁琐步骤,网络IO的效率极其的低下,优化网络效率的问题被大家所关注。

网络IO的劣势:

  1. 高延迟:数据需要经过网络传输,可能受到网络延迟、丢包、拥塞等因素影响,因此延迟通常高于单机IO。
  2. 复杂性:需要处理网络连接建立、数据包封装/解封装、错误处理、流量控制等复杂问题。

如何理解IO时间 = 等 + 数据拷贝

答:

本地IO: 当我们通过read打开某个文件,实质上是让操作系统帮我们向磁盘中获取数据,我们应用层只需要等待 操作系统的缓冲区开始有数据,等到一定的时机,我们再拷贝到应用层准备好的缓冲区中。

网络IO: 网络版本其实也是类似外设从磁盘变成了网卡 ,依旧是操作系统为我们提取数据,应用程序等待 操作系统从网络接收数据到内核的套接字缓冲区,然后再拷贝到应用层缓冲区中。

2,5种IO模型(钓鱼例子)

5种模型为:IO = 等 + 数据拷贝,这里用一个人在河边钓鱼为例子,"等"=等鱼上钩,"数据拷贝" = 将鱼钓上放在桶(应用层缓冲区)里。

  • 阻塞式 :这个人 ,就坐在哪儿,盯着鱼漂,啥事不干,就单纯在哪儿等鱼上钩,再调上。
  • 非阻塞轮询式:这个人,过一会来看看是否有上钩,上钩了再把鱼钓起来。
  • 信号驱动式:这个人将鱼钩上安装一个上钩提醒器,而他就做其他的事情,等到提醒了才过来把鱼钓起。
  • 多路复用,多路转接式 :这个人安装100个鱼竿,坐在旁边,那个杆上钩了,把那个鱼钓起,因此这种方式效率最高
  • 异步IO式 :有两个人,老板自己嫌累,雇了个员工,员工在那里钓鱼,钓上来的鱼放在桶里,等到桶被装满,员工会提醒老板过来取鱼。

其中,除了异步IO,其余都是同步IO,那如何区分同步,异步IO只要(进程,线程)参与了等或者数据拷贝,都算同步IO,都没参加就是异步IO

通过了上面的IO模型下面我们来看看官方怎么解释的:(光看图理解是浅显的,这里就简单看看,后面会有代码实践

3. 5种IO模型图

阻塞 IO 是最常见的 IO 模型。

非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回 EWOULDBLOCK错误码
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为 轮询 . 这对 CPU来说是较大的浪费, 一般只有特定场景下才使用。

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。

IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够 同时等待多个文件
描述符的就绪状态.

异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

小结

任何IO过程中, 都包含两个步骤. 第一是 等待 , 第二是 拷贝。 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少

4,进程同步&IO同步区分

另外, 我们回忆 多进程多线程 的时候, 也提到 同步 。这里的 同步通信进程之间的同步是完全不想干的概念。
多进程多线程的同步 : 在操作系统或程序内部, 多进程或多线程之间的同步 是指控制多个进程或线程 访问共享资源的方式,以避免竞态条件和其他并发问题。常见的同步机制有:互斥锁,条件便利,信号量,原子操作等

同步通信 : 同步通信是指两个或多个网络实体之间进行有序的信息交换,其中一方在发送消息后 必须等待另一方的响应才能继续执行。

那如何区分同步通信和异步通信

以钓鱼为例,"在河边等待鱼上钩"=等待,"将鱼吊起"=拷贝,只要进程参与任一一步都理解为同步IO;都不参加,直接来提桶拿鱼为异步IO
以后在看到 "同步" 这个词, 一定要先搞清楚大背景是什么。这个同步, 是同步通信异步通信的同步, 还是进程,线程同步与互斥的同步。

5,阻塞与非阻塞区别

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

二,非阻塞IO

首先我们先看看我们常见的阻塞式IO:

cpp 复制代码
int main()
{
    while (1)
    {
        char buff[1024];
        int ret = read(0, buff, sizeof buff - 1);
        if (ret)
        {
            buff[ret] = 0;
            std::cout << "echo#:" << buff ;
        } else
        {
            std::cout << "....." << std::endl;
        }
    }
    return 0;
}

结论:I/O的文件描述符默认是阻塞式的。如果非阻塞式,需要我们通过fcntl设置。最常见的IO阻塞就是标准输入,会阻塞的等待我们输入内容。

1.设置I/O为非阻塞(fcntl)

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

(注:这个接口适用于 **所有的本地IO,**网络IO所打开的 **文件描述符设置是否阻塞形式。**有了这个我们就可以设置非阻塞IO,不用单独学习特定接口参数来设置非阻塞)
fcntl函数有5种功能(方法):

  • 复制一个现有的描述符(cmd=F_DUPFD).
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

废话少说,先上手看看,写一下用于 设置文件描述符方式为非阻塞式的函数,如下:

cpp 复制代码
int SetNoBlock(int fd)
{
    int ret = fcntl(fd, F_GETFL);  // 获取原来fd的读写标志位,就比如:open时设置的O_RDONLY,O_RDWR等等
    if (ret == -1)
        return -1;
    fcntl(fd, F_SETFL, ret | O_NONBLOCK);
    // fcntl 更换为设置读写标准位,并在原来标准位的基础上添加 非阻塞模式
    return 0;
}

我们此处只是用第三种功能, 获取/设置文件状态标记 , 就可以将一个文件描述符设置为非阻塞。设置好非阻塞后,在循环内文件描述符会被轮询,如果没有资源就绪,会 设置错误码,如:EAGAIN,EWOULDBLOCK------资源未就绪。

因此,修改后的非阻塞IO案例,如下:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include <string.h>

int SetNoBlock(int fd)
{
    int ret = fcntl(fd, F_GETFL);  // 获取原来fd的读写标志位,就比如:open时设置的O_RDONLY,O_RDWR等等
    if (ret == -1)
        return -1;
    fcntl(fd, F_SETFL, ret | O_NONBLOCK);
    // fcntl 更换为设置读写标准位,并在原来标准位的基础上添加 非阻塞模式
    return 0;
}

int main()
{
    SetNoBlock(0);  //文件描述符也只需要设置一次,如果还要设置标准位可以后面自己添加
    char buff[1024];
    while (1)
    {
        sleep(1);
        errno = 0;
        int ret = read(0, buff, sizeof buff - 1);
        if ( ret > 0)
        {
            buff[ret] = 0;
            std::cout << "echo# " << buff;
            continue;
        }else  if( ret == 0)
        {
            std::cout << "echo# try again"  << std::endl;
        }else if (ret == -1)
        {
            std::cout << "echo# "<< errno << ": "<< strerror(errno);
            // 由于文件描述符采用非阻塞式后,我们无法区分是错误导致,还是单纯是IO资源未就绪
        }

        // 针对无法区分非阻塞式IO错误,我们需要对错误码进行区分
        if (errno == EAGAIN || errno == EWOULDBLOCK) // RWOULDBLOCK 本质是 EAGAIN
        {
            std::cout << " IO资源未就绪, try again" << std::endl;
            continue; 
        }else if (errno == EINTR) // read时被signal打断,返回时无法再调用read
        {
            continue;
        }else
        {
            break;
        }
    }
    return 0;
}

设置非阻塞后,socket的读事件未就绪,需要对错误值进行,判断。
其他错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足

2. I/O多路转接(select)

select接口 是一个用于监控多个文件描述符(file descriptors,简称fd)的系统调用,它可以检测多个文件描述符上是否有事件发生(如数据可读、可写或异常)。这对于需要同时处理多个客户端连接的服务器程序来说非常有用,比如Web服务器、聊天服务器等。(AI)

参数理解

1). timeval结构体 类型,里面存放着微秒数,如下:

输入意义:告诉此次select 阻塞tv_sec + tv_usec秒后select超时返回。

输出意义:如果在阻塞时间内,有fd事件就绪,则会设置剩余时间量

这里扩展一下,获取时间戳的方法,如:C语言的time_t time(time_t *),系统接口gettimeofday

2). fd_set类型

select接口中有三个fd_set类型,意义:让系统分别注意,读事件,写事件,其他事件fd就绪。

fd_set 本质是一个位图数据结构 ,其中位图下标值代表fd ,一个位图最大能容纳1024个fd;

每个位的值为1,

  • 输入意义:告诉系统注意该fd的读事件(写事件,其他事件);
  • 输出意义:select会设置fd_set,输出时标记fd就绪的下标位为1;

设置fd_set类型,需要特定操作宏:

用select优化listen_socket

首先,我们需要理解listen_socket在网络通信中的角色。listen_socket就像是餐馆门口的迎宾员,负责接收潜在顾客(客户端)的到来,并准备好迎接他们(监听连接请求)。accept操作则像是餐馆内的服务员,负责正式接待顾客并安排座位(接受连接请求并创建新的套接字来处理这个连接)。

listen_socket不仅要监听传入的连接请求,还要与客户进行TCP的三次握手,以确认连接的建立。只有当三次握手成功完成后,accept操作才能接收这个新连接,并为其提供服务(创建新的套接字来处理连接)。

当客户完成数据交换并准备断开连接时,需要进行TCP的四次挥手,以确保双方都知道连接将要关闭,并且释放占用的资源。

使用如下例子:

cpp 复制代码
        MySocket tool;
        tool.listen_run(_server_socket);

        // 实现多路转接select,这里只有一个listen,不过演示一下即可
        fd_set fdset;
        while (1)
        {    
            timeval time = {0, 0} //非阻塞
            FD_ZERO(&fdset);
            FD_SET(_server_socket, &fdset);
            int n = select(_server_socket + 1, &fdset, nullptr, nullptr, &time);
           switch (n)
           {
           case 0:
                continue;
            break;
           
           case -1:
                Logmessage(ERROR, "select fail errno %d: %s", errno, std::to_string(errno).c_str());
                exit(-1);
           break;

           default:
                Logmessage(INFO, "select get a connect");
                // 获取成功
            }
         }

上面是select只监视listen_socket一个标识符的读事件就绪,那如何将已连接客户端(或者本地IO)描述符一起管理起来呢?

思路:

  1. 需要一个外部的int数组,自称它为socket数组集,用于存放select关注的所有描述符。
  2. 每次客户端连接成功后,将soket放入数组集中;当select监视到其中某socket读事件就绪并处理后,需要从数组集中消除。
  3. 3.有个该数组集,我们可以每次遍历数组集循环设置fd_set,同时方便FD_ISSET判断。

下面是用web服务器,对socket的select处理代码示例链接(仅看webserver.hpp):

IO_Model/webserver.hpp · 逆光/Linux - 码云 - 开源中国 (gitee.com)

select的优缺点

优:

缺点:

1. 用户,OS层存在大量遍历:用户层,需要不断修改第三方数组;OS层,也需要不断遍历fd_set与修改

2. fd_set本身的1024上限。

  1. 输入输出型参数,从用户到内核,从内核到用户存在频繁的数据拷贝。

4.编码复杂

3.I/O多路转接(poll)

针对select的缺点,经过过优化后的poll能解决大部分问题。


fds 是一个 poll 函数监听的结构列表 . 每一个元素中 , 包含了三部分内容 : 文件描述符 , 监听的事件集合 , 返回的事件集合。
nfds 表示: fds 数组的长度。
timeout 表示: poll 函数的超时时间 , 单位是毫秒 (ms)。
events 如何设置事件集合

使用示例如下:

cpp 复制代码
#include <stdio.h>
#include <sys/time.h>
#include <sys/poll.h>

int main() {
    struct pollfd fds[2];
    int timeout;

    // 初始化第一个文件描述符为标准输入
    fds[0].fd = 0; // 标准输入
    fds[0].events = POLLIN; // 只关心可读事件

    // 假设这里有一个套接字,其文件描述符为 sockfd
    int sockfd = 3;
    fds[1].fd = sockfd;
    fds[1].events = POLLRDNORM; // 监听套接字是否有数据可读

    timeout = 5000; // 超时时间为5秒

    // 进行 poll 调用
    int ret = poll(fds, 2, timeout);

    if (ret > 0) {
        for (int i = 0; i < 2; i++) {
            if (fds[i].revents & POLLIN) {
                printf("文件描述符 %d 可读\n", fds[i].fd);
                fds[i].revents = 0; //poll不在关注该描述符
            }
        }
    } else if (ret == 0) {
        printf("超时\n");
    } else {
        perror("poll error");
    }

    return 0;
}

poll优点:

缺点:

  1. 用户层,OS层任存在不少的遍历。用户层需要不断检测数组就绪;OS层不断检测fd就绪,本质上与select一样需要维护第三方数组。

  2. 当有大量链接接入,但就绪事件较少,这样遍历fd数组量加大,效率线性下降。

  3. 编码较select容易些。
    没关系,epoll会解决,按照 man 手册的说法 : 是为处理大批量句柄而作了改进的 poll . epoll 几乎具备了之前所说的一切优点,被公认为Linux2.6 下性能最好的多路 I/O 就绪通知方法。

结语

本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获,请动动你发财的小手点个免费的赞,你的点赞和关注永远是博主创作的动力源泉。

相关推荐
用户03284722207019 小时前
如何搭建本地yum源(上)
运维
大树884 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠4 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质4 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz4 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工4 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
网络研究院4 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智4 天前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest4 天前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
shushangyun_4 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化