【Linux网络编程】第十九弹---深入探索:五种IO模型与高级IO概念,揭秘非阻塞IO实战

✨个人主页:熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

目录

[1、五种 IO 模型](#1、五种 IO 模型)

1.1、阻塞IO

1.2、非阻塞IO

1.3、信号驱动IO

1.4、多路转接IO

1.5、异步IO

1.6、小结

[2、高级 IO 重要概念](#2、高级 IO 重要概念)

[2.1、同步通信 vs 异步通信](#2.1、同步通信 vs 异步通信)

[2.2、阻塞 vs 非阻塞](#2.2、阻塞 vs 非阻塞)

[2.3、其他高级 IO](#2.3、其他高级 IO)

[3、非阻塞 IO](#3、非阻塞 IO)

3.1、fcntl

3.2、阻塞IO

3.3、SetNoBlock()

3.4、非阻塞IO主函数


1、五种 IO 模型

首先回顾我们前面学习的几个概念:

1、网络通信需要使用到进程通信!

2、进程间通信的本质就是IO!

3、IO ,input(输入) && output(输出)

4、站在进程角度 && 站在内存的角度

IO如何理解?read/recv/send/write

IO = 等 + 拷贝

什么叫做高效的IO呢?

本质就是单位时间内,减少等的比重!

此处使用一个钓鱼的例子来讲解五种IO模型!

1.1、阻塞IO

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

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

1.2、非阻塞IO

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

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

1.3、信号驱动IO

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

1.4、多路转接IO

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

1.5、异步IO

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

1.6、小结

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

2、高级 IO 重要概念

2.1、同步通信 vs 异步通信

同步和异步关注的是消息通信机制.

  • 所谓同步 ,就是在发出一个调用 时,在没有得到结果之前,该调用就不返回.但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
  • 异步 则是相反,调用 在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

另外, 我们回忆在讲多进程多线程 的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相干的概念.

  • 进程/线程同步也是进程/线程之间直接的制约关系
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.

uu们以后在看到 "同步 " 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步.

2.2、阻塞 vs 非阻塞

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

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

2.3、其他高级 IO

非阻塞 IO,纪录锁,系统 V 流机制,I/O 多路转接(也叫 I/O 多路复用),readv 和writev 函数以及存储映射 IO(mmap) ,这些统称为高级 IO.

3、非阻塞 IO

非阻塞IO(Non-blocking I/O)是一种I/O操作模式,允许程序在发起I/O请求后不必等待该请求完成,而是可以继续执行其他任务。
一个文件描述符, 默认都是阻塞 IO,要实现非阻塞IO,需要对fd进行控制操作,因此需要使用fcntl()函数!

3.1、fcntl

fcntl()

fcntl - 对已打开的文件描述符进行各种控制操作

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

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

参数:

  1. fd:文件描述符,表示要操作的文件或套接字。
  2. cmd :操作命令,它是一个整型值,通常以F_开头,后跟一些特定的字符来表示不同的操作。例如,F_DUPFD用于复制文件描述符,F_GETFD用于获取文件描述符的标志,等等。
  3. arg :这是一个可选参数,其类型和含义取决于cmd的值。例如,对于F_DUPFD命令,arg是一个整型值,表示要复制的文件描述符的最小值;对于F_SETFL命令,arg是一个整型值,表示要设置的文件状态标志。

常用命令及其功能:

  1. F_DUPFD :复制一个现有的文件描述符。返回一个新的文件描述符,该描述符与原始描述符共享相同的文件偏移量、访问模式和文件状态标志。新描述符是最小的大于或等于arg的一个可用描述符。
  2. F_GETFD :获取文件描述符的标志。返回的文件描述符标志通常用于控制文件描述符在exec调用时的行为。例如,如果返回的标志与FD_CLOEXEC进行按位与运算的结果不为0,则在exec调用时该文件描述符将被关闭。
  3. F_SETFD :设置文件描述符的标志。通过arg参数设置文件描述符的标志。例如,可以通过将arg设置为FD_CLOEXEC来设置文件描述符在exec调用时被关闭的标志。
  4. F_GETFL:获取文件状态标志。返回的文件状态标志包括文件的访问模式(读、写或读写)、是否设置了非阻塞标志、是否设置了同步I/O标志等。
  5. F_SETFL :设置文件状态标志。通过arg参数设置文件的状态标志。例如,可以通过将arg设置为O_NONBLOCK来设置非阻塞I/O标志。
  6. F_GETOWN:获取当前正在接收SIGIO或SIGURG信号的进程ID或进程组ID。
  7. F_SETOWN:设置将接收SIGIO和SIGURG信号的进程ID或进程组ID。
  8. F_GETLKF_SETLKF_SETLKW :这些命令用于获取、设置或等待文件记录锁。它们需要传递一个指向flock结构的指针作为arg参数。flock结构包含了锁的类型、起始位置、长度等信息。

返回值:

fcntl函数的返回值取决于执行的命令 。如果命令执行成功,则返回0或特定的正值(如F_DUPFD返回新的文件描述符)。如果命令执行失败,则返回-1,并设置errno来指示错误的原因。

传入的 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).

我们此处只是用第三种功能, 获取/设置文件状态标记 , 就可以将一个文件描述符设置为非阻塞.

3.2、阻塞IO

实现非阻塞IO之前,我们需要实现阻塞IO进行对比差别,此处使用read()函数进行从标准输入流中轮询读取数据!

主函数

int main()
{
    char buffer[1024];
    SetNonBlock(0); // 设置非阻塞等待
    while (true)
    {
        printf("Enter# "); // 没有回车不会直接刷新缓冲器数据,需手动刷新
        fflush(stdout);
        ssize_t n = read(0, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            printf("echo# %s", buffer); // 会输入\n无需手动加
        }
        else if (n == 0)
        {
            printf("read done\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    return 0;
}

1、当我们没有输入的时候,读取会卡在这里
2、而输入之后数据会回显到显示器
3、输入回车之后会读取到从标准输入流发送过来的数据

运行结果

过程分析

3.3、SetNoBlock()

将文件描述符fd设置成非阻塞先获取文件状态标志,再设置文件状态标志

void SetNonBlock(int fd)
{
    int fl = fcntl(fd,F_GETFL); // 获得文件状态标记,返回文件描述符标志
    if(fl < 0)
    {
        std::cout << "fcntl error" << std::endl;
        return;
    }
    ::fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 将fd设置成非阻塞
}

3.4、非阻塞IO主函数

主函数(代码一)

int main()
{
    char buffer[1024];
    SetNonBlock(0); // 设置非阻塞等待
    while (true)
    {
        printf("Enter# "); // 没有回车不会直接刷新缓冲器数据,需手动刷新
        fflush(stdout);
        ssize_t n = read(0, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            printf("echo# %s", buffer); // 会输入\n无需手动加
        }
        else if (n == 0)
        {
            printf("read done\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    return 0;
}

直接向阻塞等待的方式判断,会直接报错退出程序,因为非阻塞等待的返回值也是小于0 ,此处可以分别打印返回值(返回值是long int类型,需使用%ld)和错误码调试一下

调试代码

在perror()函数与bread之前增加调试打印信息即可!

printf("%ld\n",n); // 打印read返回值
printf("%d\n",errno); // 打印错误码

因此我们可以使用错误码来区分底层不就绪还是真的出错!!!

主函数(代码二)

// 非阻塞版本
int main()
{
    char buffer[1024];
    SetNonBlock(0); // 设置非阻塞等待
    while (true)
    {
        printf("Enter# "); // 没有回车不会直接刷新缓冲器数据,需手动刷新
        fflush(stdout);
        ssize_t n = read(0, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            printf("echo# %s", buffer); // 会输入\n无需手动加
        }
        else if (n == 0)
        {
            printf("read done\n");
            break;
        }
        else
        {
            // 如果是非阻塞,底层数据就没有就绪,IO接口,会以错误形式返回
            // 所以,如何区分 底层不就绪 vs 真的出错呢? 根据errno错误码
            // EWOULDBLOCK; EAGAIN; // 11号错误码
            if (errno == EWOULDBLOCK)
            {
                sleep(1);
                std::cout << "底层数据没有就绪,开始轮询检测" << std::endl;
                std::cout << "可以做其他事情" << std::endl;
                // do other thing
                continue;
            }
            else
            {
                perror("read");
                break;
            }
        }
    }
    return 0;
}

为了不让刷屏,我们可以每隔一秒打印一次做其他事情的消息!

小优化

我们可能还会遇到信号中断的情况,信号中断也需要继续轮询检查,此处可以再做一个判断!

主函数(代码三)

// 非阻塞版本
int main()
{
    char buffer[1024];
    SetNonBlock(0); // 设置非阻塞等待
    while (true)
    {
        printf("Enter# "); // 没有回车不会直接刷新缓冲器数据,需手动刷新
        fflush(stdout);
        ssize_t n = read(0, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            printf("echo# %s", buffer); // 会输入\n无需手动加
        }
        else if (n == 0)
        {
            printf("read done\n");
            break;
        }
        else
        {
            // 如果是非阻塞,底层数据就没有就绪,IO接口,会以错误形式返回
            // 所以,如何区分 底层不就绪 vs 真的出错呢? 根据errno错误码
            // EWOULDBLOCK; EAGAIN; // 11号错误码
            if (errno == EWOULDBLOCK)
            {
                sleep(1);
                std::cout << "底层数据没有就绪,开始轮询检测" << std::endl;
                std::cout << "可以做其他事情" << std::endl;
                // do other thing
                continue;
            }
            // 被系统中断的系统调用
            else if (errno == EINTR)
            {
                continue;
            }
            else
            {
                perror("read");
                break;
            }

            // EWOULDBLOCK; EAGAIN; // 11号错误码
            // perror("read");
            // printf("%ld\n",n); // 打印read返回值
            // printf("%d\n",errno); // 打印错误码
            // break;
        }
    }
    return 0;
}

总结:

1、IO = 等(多路转接的作用) + 拷贝

2、为了等待多个fd,等待fd上面的新事件就绪,通知程序员,事件已经就绪,可以进行IO拷贝了!

3、新事件就绪:OS底层有数据了(读事件就绪),或者OS底层有空间了(写事件就绪)!

相关推荐
zeijiershuai1 分钟前
Java jdk8新特性:Stream 流
java·开发语言
YOULANSHENGMENG1 分钟前
linux上使用cmake编译的方法
开发语言·c++
c的s10 分钟前
在一台服务器上使用docker运行kafka集群
服务器·docker·kafka
学计算机的睿智大学生14 分钟前
关于python的数据分析与应用
开发语言·python·数据分析
晚安~~17 分钟前
共享充电宝系统|Java|SSM|VUE| 前后端分离
java·开发语言·tomcat·maven
找了一圈尾巴27 分钟前
Wend看源码-Java-Arrays 工具集学习
java·开发语言·学习
神仙别闹29 分钟前
基于QT(C++)实现的坦克大战
数据库·c++·qt
HelloZheQ29 分钟前
Java与AI:构建智能应用的强大组合
java·开发语言·人工智能
自律小仔29 分钟前
前端开发语言涉及到 的基本数据类型(Primitive Data Types)
开发语言·后端·golang
S-X-S32 分钟前
八万字Java面试高频题目汇总(冲刺春招!)
java·开发语言·面试