【Linux】高级IO——五种IO模型和基本概念 ,非阻塞IO,fcntl,实现非阻塞IO,同步通信和异步通信

Linux高级IO

1. 五种IO模型

  IO是什么?

  IO是计算机领域中的缩写,指的是输入/输出(Input/Output)。在计算机系统中,IO通常指的是计算机与外部设备(如键盘、鼠标、显示器、硬盘、网络等)之间的数据交换过程。

  输入(Input) 输入指的是数据从外部设备传输到计算机系统内部的过程。例如:从键盘输入文字或命令到计算机中;从网络接收数据包;从磁盘读取文件内容。

  输出(Output) 输出指的是数据从计算机系统传输到外部设备的过程。例如:将计算机处理后的数据显示在屏幕上;将数据写入到打印机以打印文档;将文件保存到硬盘上。

  

  数据是怎么从输入到输出的呢,过程是什么样的?

  简单的看,调用read和write函数将数据传入OS,由OS传给不同的外设。

  输入和输出(IO)的过程发生在计算机系统与外部环境之间的数据交换过程中。在计算机系统内部,IO的过程涉及多个层次和组件:

  应用层:应用程序通过调用操作系统提供的IO接口(如read和write函数)来实现数据的输入和输出。

  操作系统操作系统负责管理和调度IO操作,包括与设备驱动程序的交互、数据缓存、IO调度等。

  设备驱动程序:设备驱动程序是操作系统和硬件设备之间的接口,负责控制和管理具体的硬件设备,处理来自设备的IO请求和响应。

  总结来说,IO过程涉及了从用户层到操作系统内核的数据传输,以及操作系统到物理设备或网络的数据传输过程。

  

  操作系统怎么判断IO发生,或者说是输入输出发生?

  通常在系统调用,硬件中断,状态改变,网络活动,资源占用这些情况下会产生IO事件。

  在计算机系统中,IO操作通常可以分解为两个主要阶段:等待(或者说请求)和拷贝(或传输)。在理解拷贝条件是否发生之前,需要考虑以下几个关键点:

  判断等待(或请求)条件发生:

  IO请求发起:应用程序发出IO请求,例如读取文件内容、发送网络数据等。

  系统调用:操作系统接收到应用程序的IO请求,然后开始进行IO操作的准备工作,这通常包括分配内核缓冲区、准备硬件接口等。

  判断拷贝(或传输)条件发生:

  系统调用返回:当应用程序发出读取或写入请求后,操作系统会开始处理这个请求,并最终完成数据的拷贝操作。可以通过系统调用的返回状态来判断拷贝操作是否已经完成。

  数据可用性:在读取操作中,可以通过检查读取的数据是否已经在应用程序的缓冲区中可用来判断拷贝操作是否完成。类似地,在写入操作中,可以通过检查数据是否成功地传输到目标设备或目标位置来确认拷贝操作的完成。

  事件通知:有些IO操作在完成后会通过事件通知机制来通知应用程序,例如异步IO模型中的完成事件或回调。应用程序可以通过这些事件或回调来确认拷贝操作已经完成。

  

  所以为了提高数据交互的效率,我们要提高IO操作的效率。

  

  总结来说:

  1. IO就是Input和Output,是计算机与外部进行交换的过程。

  2. 为了实现应用层的数据交换,我们通常使用read和write函数,本质上就是把数据从用户层写给操作系统,再本质上就是拷贝函数。

  3. IO = 等 + 拷贝 ,再进行拷贝之前,我们要判断拷贝条件是否发生。

  4. 所以我们想要实现高效的IO,在单位的时间里,等 或 拷贝 的时间越小效率越高,但是一般拷贝的时间差不多,所以怎么降低 等 的比重,就是我们提高IO效率的办法。

  

  所以说下面的可以提高IO效率的模型,都是降低了等待的时间。

  怎么降低等待的时间呢?

  

  我们先以简单的钓鱼例子来概括一下这五种IO模型:

  阻塞IO模型:你投放鱼饵后,就等待鱼儿咬钩。在等待鱼咬钩的过程中,不能做其他事情,只能一直盯着浮标,直到有鱼上钩。

  非阻塞IO模型:你在每次投放鱼饵后,你不会傻等鱼儿上钩,而是不断定期检查浮标的状态。如果没有鱼上钩,就继续做其他事情,直到鱼上钩就收杆。

  IO复用模型:你可能会使用多根钓竿,使用10个鱼竿,100个鱼竿,可以是无穷的鱼竿,当有任何一根钓竿的浮标动了,你就知道有鱼上钩,然后去收杆。

  信号驱动IO模型:你可能会使用自动报警装置。每次投放鱼饵后,设定了一个报警器,当有鱼上钩时触发报警器发出信号。在听到报警器响时,立即去收杆。

  异步IO模型:你会雇佣一个专业的钓鱼服务公司。他们会使用不同的钓鱼方式,但是你完全不用关心,你只要他们通知你来收杆就可以了。

  

1.1 阻塞IO

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

  阻塞IO是最常见的IO模型。
在这里插入图片描述

  我们写一个简单的套接字阻塞IO:

  Socket.hpp


#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

class Sock
{
public:
    Sock()
    {}

    ~Sock()
    {}

    //创建socket
    void Socket()
    {
        //表示创建一个IPV4,协议为tcp的套接字,0表示使用默认协议,
        _sockfd=socket(AF_INET,SOCK_STREAM,0);
        if(_sockfd){
            perror("Socket creation failed");
            return ;
        }
    }

    //绑定端口
    void Bind(uint16_t port)
    {
        //sockaddr_in结构体用于指定IPv4地址和端口号
        //INADDR_ANY 表示服务器将接受来自本机任意IP地址的连接
        //htons() 用于将端口号从主机字节顺序转换为网络字节顺序
        struct sockaddr_in addr;
        addr.sin_family=AF_INET;
        addr.sin_addr.s_addr=INADDR_ANY;
        addr.sin_port=htons(port);
        if(bind(_sockfd,(struct sockaddr *)&addr,sizeof(addr))==-1)
        {
            perror("Listen failed!");
            return ;
        }
    }

    //监听连接
    void Listen()
    {
        if(listen(_sockfd,5)==-1)
        {
            perror("Listen failed!");
            return ;
        }
    }

    //接受连接并且阻塞
    int Accept(std::string *clientip, uint16_t *clientport)
    {
        int client_fd=accept(_sockfd,nullptr,nullptr);
        if (client_fd == -1)
        {
            perror("Accept failed");
            return -1;
        }
        return client_fd;
    }

    //释放连接
    void Close()
    {
        close(_sockfd);
    }

    //返回sock文件描述符
    int Sockfd()
    {
        return _sockfd;
    }

private:
    int _sockfd;
};

  

  Block.cpp

  在进程接收到数据之前,进行会阻塞,直到有数据输入。

#include "Socket.hpp"

int main()
{
    Sock server_sock;
    
    server_sock.Socket();
    server_sock.Bind(8080);
    server_sock.Listen();

    std::cout << "Waiting for connections...\n";

    while (true)
    {
        int client_fd = server_sock.Accept(nullptr, nullptr);
        std::cout << "Connection accepted. Client connected.\n";

        // 循环接收数据
        while (true)
        {
            char buffer[1024];
            //recv 函数用于从指定的 client_fd 描述符(客户端连接的Socket)接收数据
            //buffer 是用来存储接收数据的缓冲区的指针
            //sizeof(buffer) - 1 表示缓冲区的大小
            //0 是可选的标志参数,这里表示没有特殊的接收行为标志
            //bytes_read 是 recv 函数的返回值,表示实际接收到的字节数
            ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
            if (bytes_read == -1)
            {
                perror("Receive failed");
                break;
            }
            else if (bytes_read == 0)
            {
                std::cout << "Client disconnected.\n";
                break;
            }

            buffer[bytes_read] = '\0';
            std::cout << "Received message from client: " << buffer;
        }

        // 关闭连接
        close(client_fd);
    }
    server_sock.Close();

    return 0;
}

1.2 非阻塞IO

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

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

在这里插入图片描述

  NoBlock.cpp

  不管进程是否接收到数据,函数都会有返回值。

#include "Socket.hpp"
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>
#include <string.h>
#include <errno.h>

int main()
{
    Sock server_sock;
    
    server_sock.Socket();

    //设置为非阻塞
    int flags=fcntl(server_sock.Sockfd(),F_GETFL,0);
    if(flags<0)
    {
        perror("fcntl");
    }
    fcntl(server_sock.Sockfd(),F_SETFL,flags|O_NONBLOCK);

    std::cout<<"set "<<server_sock.Sockfd()<<" nonblock done"<<std::endl;

    server_sock.Bind(8080);
    server_sock.Listen();

    std::cout << "Waiting for connections...\n";

    while (true)
    {
        int client_fd = server_sock.Accept(nullptr, nullptr);
        sleep(1);

        //循环接收数据
        while (true)
        {
            char buffer[1024];
            //recv 函数用于从指定的 client_fd 描述符(客户端连接的Socket)接收数据
            //buffer 是用来存储接收数据的缓冲区的指针
            //sizeof(buffer) - 1 表示缓冲区的大小
            //0 是可选的标志参数,这里表示没有特殊的接收行为标志
            //bytes_read 是 recv 函数的返回值,表示实际接收到的字节数
            ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
            if (bytes_read > 0)
            {
                std::cout << "Connection accepted. Client connected.\n";
                buffer[bytes_read] = '\0';
                std::cout << "Received message from client: " << buffer;
                sleep(1);
            }
            else if (bytes_read == 0)
            {
                std::cout << "Client disconnected.\n";
                break;
            }
            else 
            {
                //1.设置为非阻塞,如果底层的fd数据没有就绪,recv/read/write/send
                //返回值就会以出错的形式返回
                //2.1.真的出错了
                //2.2.底层没有就绪,我们通过errno区分
                //如果错误码是11就表示底层没有就绪
                if(errno==EAGAIN)
                {
                    std::cout<<"0 fd data not ready,try again"<<std::endl;
                    break;
                }
                else
                {
                    std::cout<<"read error"<<" errno code: "<<errno<<" strerror: "<<strerror(errno)<<std::endl;
                    break;
                }
                sleep(1);
            }
        }
        // 关闭连接
        close(client_fd);
    }
    server_sock.Close();

    return 0;
}

  

1.3 信号驱动IO

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

在这里插入图片描述

  

1.4 IO多路转接

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

在这里插入图片描述

  

1.5 异步IO

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

在这里插入图片描述

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

  

2. 同步通信和异步通信

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

  所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果

  异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用

  同步通信:类比钓鱼例子,同步通信就像你投放鱼饵后,自己必须一直盯着浮标,直到鱼上钩才能继续下一步操作。这种方式下,发送方和接收方都必须严格按照某种约定的时间或步骤进行操作,以确保信息的同步传输和处理。

  异步通信:类比钓鱼例子,异步通信就像你投放鱼饵后不再一直盯着浮标,而是定期检查浮标的状态。你可以在等待鱼上钩的同时做其他事情,当浮标动了或者有鱼上钩时,你会得到通知,然后再去收杆。

  

3. 阻塞和非阻塞

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

  阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

  非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

  类比钓鱼例子,阻塞就像你投放鱼饵后,不得不一直盯着浮标,不能离开或者做其他事情,直到鱼上钩。

  类比钓鱼例子,非阻塞就像你投放鱼饵后,定期检查浮标的状态。如果浮标没有动,你可以继续做其他事情,不需要一直等待鱼上钩。

          

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
非阻塞IO是指在进行IO操作时,如果当前没有数据可读或者没有空闲空间可写,不会一直等待,而是立即返回一个错误码,让程序可以继续执行其他任务。 在Linux内核中,实现非阻塞IO的主要方法是使用异步IO和轮询机制。 异步IO实现原理是:应用程序通过对文件描述符进行异步IO设置,当异步IO操作完成时,内核会向应用程序发送一个信号,应用程序可以在信号处理函数中读取数据或者进行其他操作。 轮询机制的实现原理是:应用程序通过对文件描述符进行设置,将其添加到轮询列表中,然后不断地轮询这个列表,检查其中的文件描述符是否可读或可写,如果可读或可写,则进行IO操作,否则继续轮询。 非阻塞IO实现步骤如下: 1. 设置文件描述符为非阻塞模式,可以使用fcntl函数进行设置。 2. 在进行IO操作前,使用select或者poll函数对文件描述符进行轮询或者异步IO设置。 3. 在IO操作返回错误码时,根据错误码进行判断,如果是EAGAIN或者EWOULDBLOCK,则说明当前没有数据可读或者没有空闲空间可写,可以进行其他操作;如果是其他错误码,则可能是IO操作出错,需要进行错误处理。 需要注意的是,在使用非阻塞IO时,应用程序需要不断地进行轮询或者异步IO设置,否则可能会出现数据丢失或者延迟等问题。同时,非阻塞IO也会增加CPU的负载,因此需要进行合理的优化和调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鳄鱼麻薯球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值