什么是IO?

IO是指计算机系统与外部世界进行数据交换的过程。在计算机中,IO通常用于与外部设备通信,这些设备包括键盘、鼠标、打印机、显示器、网络等。通过IO操作,计算机系统可以接收来自外部设备的输入数据,也可以将处理后的数据输出到外部设备。

在网络通信上,主要表示在计算机与计算机之间能通过互联网来进行数据交换,从而实现远程数据与资源的共享。

五种IO模型

阻塞IO

在这里插入图片描述
用户线程在发起IO请求后会阻塞,直到IO操作完成(内核数据准备好)并返回结果。这种方式简单但效率低下,特别是在高并发场景下。

非阻塞IO

在这里插入图片描述
如果内核还没有准备好数据,内核也会进行返回,并且返回一个EWOULDBLOCK错误码;

非阻塞IO需要在代码中循环,不断的查询读写这个文件描述符,这种操作称为轮询。在特定场景中,才会进行使用,会比较浪费CPU资源。

信号驱动IO

在这里插入图片描述
当内核将数据准备好时,内核会向应用进程发送一个SIGIO信号,通知程序进行IO操作。

IO多路转接

在这里插入图片描述

系统调用允许单个线程同时监视多个文件描述符的IO就绪状态,从而提高了IO操作的效率。

异步IO

在这里插入图片描述

异步IO是指系统内核主动发起IO请求,用户空间的线程被动接受IO操作的结果。这种方式下,用户线程无需等待IO操作完成即可继续执行其他任务,当IO操作完成后,内核会通知用户线程处理结果。

小结

任何IO过程,都包含两个步骤:等待数据+拷贝数据

在实际应用中,等待消耗的时间往往大于拷贝消耗的时间。
所以,要想提高IO的效率,就要减少等待的消耗时间。

高级IO的重要概念

同步IO

同步IO是指需要等待前面程序完成后,才能继续执行后面的程序。这意味着当程序发起一个IO请求时,它会阻塞当前线程的执行,直到IO操作完成。

特点:

  • 阻塞性:同步IO在读写数据时会阻塞程序的执行,导致程序无法充分利用CPU资源。
  • 顺序性:同步IO按照程序指定的顺序依次执行IO操作,前一个IO操作完成后,下一个IO操作才会开始。
  • 简单性:同步IO的实现相对简单,因为程序只需按照顺序执行IO操作,无需处理复杂的并发逻辑。

同步IO在IO操作期间会阻塞程序执行,导致CPU资源无法得到充分利用。
当IO操作时间较长时,程序会处于等待状态,造成CPU资源的浪费。

同步IO适用于一些简单的、不需要高并发处理的场景。例如,批处理作业、简单计算和查询程序等。
在这些场景中,同步IO的阻塞性和顺序性并不会对程序的性能产生太大影响。

异步IO

异步IO是指在进行数据读写操作时,程序无需等待IO操作完成。程序会发起IO操作,并立即返回一个标识符或回调函数,用于指示IO操作的状态或结果。

特点:

  • 非阻塞性:异步IO允许程序在发起IO请求后继续执行其他任务,无需等待IO操作完成。
  • 并发性:异步IO可以同时处理多个IO请求,提高了程序的并发处理能力。
  • 复杂性:异步IO的实现相对复杂,因为程序需要处理IO完成的通知和管理异步操作的状态。这通常涉及到事件循环、回调函数等概念。

异步IO可以充分利用CPU资源,因为程序可以在IO操作期间执行其他任务。
异步IO通过并发处理多个IO请求,提高了系统的整体性能和资源利用率。

异步IO适用于需要处理大量并发IO请求的场景。例如,Web服务器、数据库访问、网络编程等。
在这些场景中,异步IO的非阻塞性和并发性能够显著提高程序的执行效率和并发处理能力。

非阻塞IO的实现

对于C语言/C++来说,默认都是阻塞IO,如果要改为非阻塞IO,就要通过特定函数来进行实现;

fcntl()

在Linux系统中用于操作文件描述符的库函数。

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

参数:

  • fd: 文件描述符,是一个非负整数,表示要操作的文件。
  • cmd: 一个命令,用于指定要执行的操作。不同的命令需要不同数量的附加参数。
  • … /* arg */: 根据 cmd 命令的不同,可能需要一个或多个附加参数。这些参数通常是一个指向数据的指针。

常用命令

  • F_DUPFD: 复制文件描述符。
  • F_GETFD: 获取文件描述符标志。
  • F_SETFD: 设置文件描述符标志。
  • F_GETFL: 获取文件状态标志(如只读、非阻塞等)。
  • F_SETFL: 设置文件状态标志。
  • F_GETLK: 获取文件锁的状态。
  • F_SETLK: 设置文件锁(非阻塞)。
  • F_SETLKW: 设置文件锁(阻塞)。
#include<iostream>
#include<unistd.h>
#include<fcntl.h>

void SetNonBlock(int fd)
{
    int fl = ::fcntl(fd, F_GETFL);//获取fd当前文件描述符状态,默认为阻塞IO
    if(fl < 0) 
    {
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);//将其设置为非阻塞IO
}
int main()
{
    char buffer[1024];
    SetNonBlock(0);//设置成非阻塞的
    while (true)
    {
        //读取标准输入流的数据
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "Echo# " << buffer << std::endl;
        }
        else
        {
            //问题:我怎么知道是底层IO条件不就绪,还是读取错误了呢???
            // 底层IO条件就绪和读取错误采用的是同样返回值操作的
            if(errno == EWOULDBLOCK || errno == EAGAIN)
            {
                std::cout << "底层数据没有就绪, 下次在试试吧! 做做其他事情!" << std::endl;
                sleep(1);
                continue;
            }
            std::cout << "读取错误... s : " << s  << " errno: " << errno << std::endl;
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述

在阻塞IO中,read函数如果没有接收到输入的数据,那么就会一直阻塞,直到有数据的输入;
在这里插入图片描述
非阻塞IO中,如果没有输入数据,那么read函数也不会阻塞,会因为while循环进行轮询,不断的执行当前主函数,一旦read函数收到数据,那么就会打印对应数据。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐