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

【Linux高级IO】五种IO模型 多路转接(select)

目录

1. 五种IO模型

 1.1 阻塞式IO

 1.2 非阻塞IO

1.3 信号驱动IO

1.4  多路转接

 1.5 异步IO

2. 同步通信与异步通信

3. 多路转接

3.1 select

总结


在这里插入图片描述

1. 五种IO模型

 1.1 阻塞式IO

        阻塞式IO最为常见,在内核将数据准备好之前, 系统调用会一直等待,所有的套接字默认都是阻塞方式;

 最常见的就是C语言中调用scanf,程序启动,等待输入,不输入一直阻塞等待;

 比如:小明去钓鱼,阻塞式IO就类似于小明一直盯着鱼竿,什么都不干就等着鱼上钩;

 1.2 非阻塞IO

非阻塞IO:如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码;

非阻塞IO往往需要程序员使用循环的方式反复尝试读写文件描述符,这个过程称为轮询;这对CPU来说是较大的浪费, 一 般只有特定场景下才使用;

比如:小王也去钓鱼,在等待鱼上钩的这段时间,小王可以看看书,吃点零食...做其他的事情;

1.3 信号驱动IO

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

 比如:小聪也去钓鱼,他在鱼竿加了一个铃铛,在等待鱼上钩的期间,他可以干其他事,等有鱼上钩铃铛就会提醒有鱼了;

1.4  多路转接

IO多路转接:多路转接能够同时等待多个文件 描述符的就绪状态.  

从流程图上看起来和阻塞IO类似,如何理解?

比如:小张也是去钓鱼,他拿了很多鱼竿,同时使用多个鱼竿钓鱼;一个人盯着多个鱼竿;

 1.5 异步IO

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

 怎么去理解?

比如:小李是一个老板,走到鱼塘边看到这么多人在钓鱼,他想吃鱼了,但是他又不想自己钓,于是和自己的司机说,你去钓鱼,钓上来鱼给我,然后通知另一个司机来接他;小李全程不参与钓鱼,由他的司机给他钓;司机就相当于操作系统,小李相当于是一个线程,钓鱼是IO,鱼就是数据;

2. 同步通信与异步通信

同步通信

在同步通信中,发送方和接收方在通信过程中是时间上密切关联的。发送方发送消息后,需要等待接收方确认已经接收到消息,才能继续进行后续操作。

特点

  • 阻塞:发送方在发送消息后会被阻塞,直到接收方处理完毕并回复。这意味着发送方的执行流会暂停,直至接收方确认。
  • 实现简单:因其简单的请求、响应模式,容易实现和理解。
  • 实时性:适用于需要即时反馈的场景,比如电话通话或某些即时消息服务。

 比如:打电话;

 异步通信

 在异步通信中,发送方和接收方之间的通信不需要严格的时间同步。发送方在发送完消息后,不必等待接收方的确认,可以继续执行其他任务。

特点

  • 非阻塞:发送方在发送消息后不会被阻塞,可以自由进行其他操作。接收方会在适当的时候处理接收到的消息。
  • 复杂性高:由于涉及到消息的队列、存储和后续处理,通常实现起来比同步通信复杂。
  • 适应性强:适用于高并发、跨网络或需要高响应性的场景,比如事件驱动的编程模型、某些消息队列系统。

 比如:电子邮箱;

 两种通信的关键点在于阻塞与非阻塞;

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回;
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程. 等待需要时可以再获取执行结果;

 如何设置非阻塞?

fcntl

文件描述符, 默认都是阻塞IO,但是可以通过接口设置为非阻塞; 

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

 参数:

  • fd:文件描述符
  • cmd:操作命令,指定要执行的操作类型。
  • arg:可选参数,根据 cmd 的不同而有所不同,某些操作不需要这个参数。

 传入的cmd的值不同, 后面追加的参数也不相同;

 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).

 使用示例:

将文件描述符设置为非阻塞; 

void SetNoBlock(int fd) { 
    int fl = fcntl(fd, F_GETFL); 

    if (fl < 0) { 
        perror("fcntl");
        return; 
    }

    fcntl(fd, F_SETFL, fl | O_NONBLOCK); 
}

F_GETFL将当前的文件描述符的属性取出来(这是一个位图);该位图表示文件描述符的各种状态和行为;这里是获取出它的状态和行为,然后添加一个非阻塞的状态;

文件访问模式: 

  • O_RDONLY:以只读模式打开文件。
  • O_WRONLY:以只写模式打开文件。
  • O_RDWR:以读写模式打开文件。

 文件状态标志:

  • O_APPEND:文件指针在每次写操作后移到文件末尾。这意味着任何写入都将附加到文件的现有内容后面。
  • O_NONBLOCK:文件描述符被设置为非阻塞模式。对该文件描述符的读写操作不会导致进程阻塞。
  • O_SYNC:写入操作会在返回前完成同步,这样就可以确保数据已经写入磁盘。
  • O_DSYNC:确保数据已写入磁盘。

 验证一下非阻塞IO:

#include <iostream>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>

void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if(fl < 0)
    {
        std::cerr << "fcntl error" << std::endl;
        exit(0);
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main()
{
    SetNonBlock(0);
    while (true)
    {
        char buffer[1024];
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "echo# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "end stdin" << std::endl;
            break;
        }
        else
        // 非阻塞等待, 如果数据没有准备好,返回值会按照出错返回, s == -1
        // 数据没有准备好 vs 真的出错了 : 处理方式一定不是一样的。 s无法区分!
        // 数据没有准备好,算读取错误吗?不算。read,recv以出错的形式告知上层,数据还没有准备好
        {
            if (errno == EWOULDBLOCK)
            {
                std::cout << "OS的底层数据未准备就绪, errno: " << errno << std::endl;
            }
            else if(errno == EINTR)//信号导致中断
            {
                std::cout << "IO interrupted by signal, try again" << std::endl;
            }
            else
            {
                std::cout << "read error!" << std::endl;
            }
        }
        sleep(1);
    }
    return 0;
}

3. 多路转接

多路转接有什么优势?

比如:TCPServer,服务端需要创建一个监听套接字,然后接收客户端发来的连接请求,然后返回新的fd,服务端需要维护多个连接,对于每个连接都可能需要读数据/写数据;也就是说,服务端需要监控多个fd的IO事件;没有多路转接:

  • 把所有的fd都设置非阻塞,然后不停的循环遍历每个fd,判断是否有IO事件;
  • 创建多个线程,让线程去监控IO事件;
  • 基于信号以及子进程的方式,让子进程去执行处理对应的IO操作,设置忽略SIGCHLD
  • 使用孙子进程,子进程退出,自动回收资源

 这几种方式都有很大的效率和内存开销的问题:

  • 循环遍历的方式:CPU 资源浪费严重,因为即使没有 I/O 事件发生,CPU 也会不断地进行循环检查;可扩展性差,随着连接数的增加,性能会急剧下降;
  • 线程/进程的创建和销毁开销较大,过多的线程/进程会导致系统资源耗尽;

 应对多个连接的IO时,多路转接无疑是最合适的方式;

3.1 select

  IO总结就两个过程:

  • 等(阻塞,等待数据)
  • 就是拷贝(读/写数据,read、write、recv、send)

 多路转接帮我们解决的就是IO中的等;有IO事件就绪就返回;并且它可以一次等待多个文件描述符;

接口原型:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

 参数:

nfds:需要监控的最大文件描述符编号加 1。select 函数会检查从 0 到 nfds - 1 的所有文件描述符。例如,如果要监控的文件描述符是 3、5、7,那么 nfds 应该设置为 8(最大文件描述符 7 加 1)。

readfds:指向一个 fd_set 类型的集合,该集合包含了需要监控读事件的文件描述符。当集合中的某个文件描述符,select 函数会将其标记为就绪。如果不需要监控读事件,可以将其设置为 NULL;

writefds:指向一个 fd_set 类型的集合,该集合包含了需要监控写事件的文件描述符。当集合中的某个文件描述符可以进行写操作时,select 函数会将其标记为就绪。如果不需要监控写事件,可以将其设置为 NULL。

exceptfds:指向一个 fd_set 类型的集合,该集合包含了需要监控异常事件的文件描述符。当集合中的某个文件描述符发生异常时,select 函数会将其标记为就绪。如果不需要监控异常事件,可以将其设置为 NULL;

timeout:指向一个 struct timeval 类型的结构体,用于设置 select 函数的超时时间。struct timeval 结构体定义如下:

struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

// 示例
struct timeval timeout ={5,0};// 5秒以内阻塞等待,5秒以后非阻塞;(阻塞+非阻塞)
// 5秒以内阻塞式等待,5秒内就绪了select正常返回,5秒内没有就绪,返回值就为0(超时返回)

struct timeval timeout ={0,0};// 立即检测,有就绪的直接返回,没有返回0(非阻塞)
struct timeval timeout= nullptr;// (阻塞)

 返回值:

  •  大于 0:表示就绪的文件描述符的总数,即 readfds、writefds 和 exceptfds 三个集合中就绪的文件描述符数量之和。
  • 0:表示超时,即在指定的时间内没有文件描述符就绪。
  • -1:表示发生错误,此时会设置相应的 errno 变量,可以通过 perror 函数输出错误信息

 设置位图不允许私自设置,系统也提供了接口:

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int  FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

 示例:

简易的SelectServer,详细可见我的码云:SelectSever示例

Select的优缺点
优点:select只负责等待,可以等待多个fd,IO的时候效率比较高;
缺点:

  • 每次都要对select的参数重置;
  • 编写程序的时候,select要是有第三方数组,所以充满遍历,可能会影响select的效率
  • 用户到内核,内核到用户,每次select调用和返回,都要对位图进行重新设置,内核和用户之间要一直进行数据拷贝
  • select让OS在底层遍历要关心的所有fd,这也会导致效率低下,内核在检查文件描述符状态时,采用的是线性遍历的方式,即从 0 到 nfds - 1 逐个检查每个文件描述符是否就绪。随着要监控的文件描述符数量增加,遍历的时间也会线性增长;
  • fd_set是系统提供的类型,fd_set大小是固定的;也就是说位图的大小也是固定的,select最多能检测的fd总数是有上限的;

后续会介绍其他实现多路转接的接口,他们解决了select中存在的一些问题,也是现在最为常用的方式;


总结

        以上便是本文的全部内容,希望对你有所帮助,感谢阅读!

相关文章:

  • ArcGIS Pro应用:精准计算容积率的详细指南
  • 基于STM32的智能停车场管理系统
  • 《AI强化学习:元应用中用户行为引导的智能引擎》
  • 【Qt】编程基础
  • 大白话React Hooks(如 useState、useEffect)的使用方法与原理
  • API网关相关知识点
  • 软件工程----4+1架构模型
  • GitHub 入门指南(2025最新版)
  • 【如何避免dify分类问题总是返回第一个分类错误】
  • LeetCode 2656 K个元素的最大和
  • electron多进程通信
  • 深度解读 AMS1117:从电气参数到应用电路的全面剖析
  • DeepSeek在PiscTrace上完成个性化处理需求案例——光流法将烟雾动态可视化
  • 删除变慢问题
  • vue3.0 + vue-waterfall2:瀑布流布局
  • CMU15445(2024 fall) Project #0 - C++ Primer
  • 用大白话解释缓存Redis +MongoDB是什么有什么用怎么用
  • 基于深度学习+NLP豆瓣电影数据爬虫可视化推荐系统
  • Python标准库【os.path】操作路径
  • vue el-table-column 单元表格的 省略号 实现
  • 做内容网站好累/免费seo推广计划
  • ftp网站服务器/网络营销案例视频
  • 秦皇岛网站制作的流程/网站建设公司哪家好
  • wordpress 素材网站模版/推广策划方案怎么做
  • 网站开发检测用户微信号/推广app有哪些
  • 网站建设属于设备吗/网络推广网站的方法