Linux —— 五种IO模型与非阻塞IO

目录

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

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

[2.1 同步通信 vs 异步通信(synchronous communication/ asynchronouscommunication)](#2.1 同步通信 vs 异步通信(synchronous communication/ asynchronouscommunication))

[2.2 阻塞 VS 非阻塞](#2.2 阻塞 VS 非阻塞)

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

[3.1 fcntl](#3.1 fcntl)

[3.2 实现函数SetNoBlock](#3.2 实现函数SetNoBlock)

[3.3 代码的实现](#3.3 代码的实现)


1. 五种IO模型

五种IO模型就是解决IO通信时,如何高效通信的问题。

例子:钓鱼

钓鱼 = 等 + 钓 !!! 高效的钓鱼 = 等的比重,非常低,单位时间内,钓鱼的效率很高!!!

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

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

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

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

  • 信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应⽤程序进行IO操作。(用的不多,SIGIO信号默认是没有开启的)
  • IO多路转接: 虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个⽂件描述符的就绪状态。

select 、poll、epoll 只解决等的问题,拷贝依旧是之前老的IO函数来做,但是一次只能等到 1个fd。select 、poll、epoll一次能等待多个 fd。

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

小结

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

2. 高级IO重要概念

2.1 同步通信 vs 异步通信(synchronous communication/ asynchronous

communication)

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

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

另外, 在之前的多进程多线程的时候, 也提到同步和互斥. 这⾥的同步通信和进程之间的同步是完全不相⼲的概念。

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

在以后看到 "同步" 这个词的时候,一定要搞清楚是同步通信异步通信的同步,还是同步与互斥中的同步。二者是没有任何关系的。同步IO和异步IO的区分:只要参加了 等 + 拷贝的任意一项就是同步IO,一项都没参加的话就是异步IO。

什么叫做IO模型?

模型就是一种规律,IO的情况只有这五种!!要么是五种当中的一种,要么是任意多个的组合!大部分都是阻塞式IO!

2.2 阻塞 VS 非阻塞

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

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

3. 非阻塞IO

3.1 fcntl

例子:scanf()

scanf() 函数,键盘不进行输入的话,就会被阻塞住,阻塞住的原因是读条件不就绪,读条件不就绪就会阻塞住。阻塞是OS自动做的。

代码:将阻塞变为非阻塞

open() 函数中的 flag 参数,默认是阻塞的方式打开的,flag当中可以设置一个选项:O_NONBLOCK

recv() 函数中的 flag 参数也是可以进行设置的:

一旦设置了,如果底层文件描述符没有数据的话,此时recv就会出错返回。

如何设计非阻塞?将 fd 直接设置为非阻塞的方式 -> fcntl(上层即便用阻塞的接口,它也是非阻塞的。任何文件在底层都有 struct file 结构体,包含了 int flag 、int mode,一个文件究竟是阻塞还是非阻塞本质就是 struct file 结构体中的flag标志位是否被设置NONBLOCK,此时上层是阻塞的接口来调用的话,依旧是非阻塞的。)

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 实现函数SetNoBlock

cpp 复制代码
void SetNoBlock(int fd) {
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0) 
	{
		perror("fcntl");
		return;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);  // 设置fd为非阻塞!
}

之前是阻塞的就是因为没有设置上面的代码中的标志位。

返回值:失败返回 -1

3.3 代码的实现

以 read() 为例,因为 read() 是默认的阻塞式的IO。

cpp 复制代码
// testNonBlock.cc 

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

void SetNonBlock(int sockfd)
{
    int f1 = fcntl(sockfd, F_GETFL);
    if (f1 < 0)
    {
        perror("fcntl");
        return;
    }
    fcntl(sockfd, F_SETFL, f1 | O_NONBLOCK); // O_NONBLOCK:设置fd为非阻塞
}

int main()
{
    // 默认标准输入是阻塞的!
    char buffer[64];
    while (true)
    {

        ssize_t n = read(0, buffer, sizeof(buffer) - 1); // sizeof(buffer):期望读取的大小  n:实际读取的大小
        if (n > 0)
        {
            buffer[n] = 0;
            printf("##%s", buffer);
        }
        else if (n == 0)
        {
            printf("read end of file!\n");
        }
        else
        {
            perror("read");
        }
    }
}

运行结果:阻塞式IO

变为非阻塞式IO:

运行结果:没有卡住

以上信息是它打出来的:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

返回值:

调用系统函数失败,最关心的就是为什么会失败???全局变量:errno,当一个库函数或系统调用失败,该函数自动设置errno,表明该函数所对应的错误码!!!多进程中errno会写时拷贝,多线程中errno是线程局部存储,errno变量每个线程一旦修改,调用库函数,修改errno,每个线程都有自己的errno,互相不冲突。errno会线程安全的在多线程多进程中使用。错误码就会告诉我们是真的出错了还是其他情况!!!

cpp 复制代码
printf("read error, reat ret: %ld, errno:%d\n", n, errno);

将错误码准换成错误码的字符串描述:

cpp 复制代码
printf("read error, reat ret: %ld, errno:%d, err msg:%s\n", n, errno, strerror(errno));

之前用到的perror函数,就是拿着错误码转成错误码描述,上个函数出错了,将全局变量errno设置了。

cpp 复制代码
// errno = 11 -> EAGAIN or EWOULDBLOCK (两个宏值)
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

void SetNonBlock(int sockfd)
{
    int f1 = fcntl(sockfd, F_GETFL);
    if (f1 < 0)
    {
        perror("fcntl");
        return;
    }
    fcntl(sockfd, F_SETFL, f1 | O_NONBLOCK); // O_NONBLOCK:设置fd为非阻塞
}

int main()
{

    SetNonBlock(0);  // 设置非阻塞只需要设置一次就行了

    // 默认标准输入是阻塞的!
    char buffer[64];
    while (true)
    {

        ssize_t n = read(0, buffer, sizeof(buffer) - 1); // sizeof(buffer):期望读取的大小  n:实际读取的大小
        if (n > 0)
        {
            buffer[n] = 0;
            printf("##%s", buffer);
        }
        else if (n == 0)
        {
            printf("read end of file!\n");
            break;
        }
        else
        {
            // IO非阻塞,你就是不输入 -> 读条件不满足 -> 以后讨论IO问题,全部站在内存(进程)视角,看待IO
            // read 会以出错形式返回!
            // 读条件不满足算真的出错了吗?---- 严格来说不算出错!!!
            // 为什么read()返回值 < 0 ??? 
            // a.真的IO出错了
            // b.其他情况,被迫返回小于0!!!
            // perror("read");  // Resource temporarily unavailable
            // printf("read error, reat ret: %ld, errno:%d, err msg:%s\n", n, errno, strerror(errno));
            // EAGAIN or EWOULDBLOCK

            // errno = 11 -> EAGAIN or EWOULDBLOCK (两个宏值)
            
            if(errno == EAGAIN || errno ==EWOULDBLOCK )
            {
                printf("你的数据没有准备好,下次再来!!\n");
                // 做其他事情 !
                continue;
            }
            else
            {
                perror("raed error\n");
            }
            sleep(1);
        }
    }
}

轮询检测:

在读数据之前,IO函数可能会被信号给中断。回来的时候,继续read()

cpp 复制代码
if (errno == EAGAIN || errno == EWOULDBLOCK)
            {
                printf("你的数据没有准备好,下次再来!!\n");
                // 做其他事情 !
                sleep(1);
                continue;
            }
            if (errno == EINTR)
            {
                sleep(1);
                // 信号中断,回来的时候,继续read
                continue;
            }
            else
            {
                perror("raed error\n");
            }

为什么用标准输入来演示,为什么不用标准输出???

---- 因为标准输出,大部分的情况下输出条件都是满足的!!输出缓冲区默认是空的,输出条件默认是满足的,所以printf这些接口不会卡住,看不到卡住的现象,很难看到现象。标准输入没有数据时,比较容易看到现象。

相关推荐
筝筝ba21 分钟前
extended/factory_reset.robot
linux·运维·服务器·网络·网络协议·p2p
优化Henry26 分钟前
5G网络切片之S-NSSAIlist相关参数解析
运维·服务器·网络·笔记·学习·信息与通信
子有内涵32 分钟前
【Linux】C++ 实现可扩展 TCP 服务器|回响、字典、远程命令
linux·运维·服务器
新兴AI民工37 分钟前
# 【Linux内核三十六】进程管理模块:CFS负载均衡(一):sched_domain与sched_group层级构建
linux·运维·负载均衡
luj_176842 分钟前
毒素驱动的生物自毁机制探析
服务器·c语言·开发语言·经验分享·算法
weixin_307779131 小时前
Databricks里用PySpark统计指定表和字段中各字段的空值、空字符串或零值比例
运维·数据仓库·python·spark·云计算
躺不平的理查德1 小时前
Nginx与Ajax 备忘录
linux·运维·服务器
多多鼠1 小时前
Tool Calling的信任边界:从协议校验到执行沙箱的完整链路设计
运维·开发语言·网络·人工智能·python·langchain
谢亮_vipxieliang1 小时前
ValidX vs Apache Commons Validator:功能与性能对比
java·服务器·spring boot·后端·spring cloud·apache·hibernate