Linux——高级IO

目录

IO

五种IO模型

阻塞式IO

非阻塞式IO

信号驱动IO

多路转接

异步IO

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


IO

网络的知识我们已经介绍完了,网络通信的本质就是IO,一方要发送数据,还要接收数据,这就是一次IO,所以我们原来说过的IO至少是在一个机器上进行的,虽然向磁盘写入的效率相比于CPU是很慢的,但是网络通信中的IO效率要比这些更低的。

为了解决IO低效的问题,我们得先知道它为什么低效。以读取为例:

  • 当调用read/recv这样的拷贝函数时,如果缓冲区没有数据,那就会阻塞,说白了就是等。
  • 如果缓冲区中有数据,那就把缓冲区中的数据拷贝到上层。
  • 所以一次IO操作 就可以理解为:等待 + 数据拷贝

当要读取磁盘中的某个文件时,首先要打开该文件,打开文件就是为这个文件创建内核数据结构,此时可能并没有加载到内存,此时这个进程就只能阻塞等待,等操作系统把外设的数据换入到内存中,之后才能进行拷贝。

现在就可以知道,低效的IO就是:单位时间内,IO接口等待的比重高 。所以提高IO效率就是想办法在单位时间内,IO接口等待的比重降低


五种IO模型

先来说一下五种IO模型:

  • 阻塞式IO
  • 非阻塞式IO
  • 信号驱动IO
  • 多路转接
  • 异步IO

IO模型说完,我们也先来说一下IO的相关概念,还是以读取数据为例:

  1. 是谁读取数据?肯定是一个执行流在读取,也就是进程或者线程
  2. 从哪里读读取?从特定的文件描述符中读取。
  3. 要读取的数据放在哪里?放在了内核的缓冲区中。
  4. 读取后放到哪里?放到了用户缓冲区中。

之后我们都以读取为例来说明这五个IO模型。

阻塞式IO

阻塞式IO就是在内核缓冲区的数据准备好之前,系统调用会一直等待,改变进程的状态;数据准备好后,执行拷贝操作。

阻塞IO是最常见的IO模型,也是使用最多的,因为它简单。

  • 在等待的过程中,操作系统也在不停的检测 ,检测条件是否就绪,这个条件就是某个文件描述符上是否有数据,如果没有数据,操作系统就会把进程状态设置为非运行状态(例如S状态),并把这个进程放到对应的等待队列中。
  • 当条件就绪时,操作系统识别后,把该进程的状态调整为运行状态(R状态),放入运行队列。

阻塞式IO一定参与了此次IO,因为它不仅有等待的过程 ,还有拷贝的过程

非阻塞式IO

非阻塞式IO就是如果内核缓冲区还未将数据准备好,系统调用会直接返回 ,并且返回EWOULDBLOCK错误码 ,也就是在数据未准备好时,操作系统不将进程阻塞,让进程自行处理,此时进程可以先处理其他事,所以通常在检测条件就绪时采用循环的方式 ,这也叫做非阻塞轮询式

阻塞IO和非阻塞IO的区别在于,两种方式都进行了等待 ,但是等的方式不一样,最后两种方式都要从内核缓冲区拷贝到用户缓冲区

信号驱动IO

信号驱动IO就是当内核将数据准备好的时候,操作系统向目标进程发送SIGIO信号,将信号集中的位图结构的第29号位置由0置1,通知进程进行拷贝操作。

  • 信号的产生是异步的,因为信号在任何时刻都可能产生。
  • 信号驱动IO是同步,因为当底层数据就绪时,执行流需要停下正在做的事情,进行数据拷贝,所以当前执行流仍然需要参与IO过程。

如何区分一个IO过程是同步还是异步,其实就是看当前进程或线程是否亲自参与IO,如果参与了IO,那就是同步的,反之就是异步。

多路转接

多路转接也叫多路复用,能够同时检测多个文件描述符的状态,支持多路转接的操作系统都要提供一些接口,比如下面的select,它的工作就是等待,运行向其中添加多个描述符,一次就可以等待多个文件描述符。

  • 一次IO操作需要等待+数据拷贝,但是使用read/recvfrom这样的系统调用接口一次只能等待一个文件描述符,但是这样IO效率太低。
  • 所以系统提供了三组接口,分别是select、poll 和 epoll,他们的工作就是等待
  • 这些多路转接的接口一次性等待多个文件描述符,将等待的时间重叠,当数据就绪时就可以调用recvfrom等函数进行数据拷贝,就不需要再等了。

异步IO

异步IO会在内核数据拷贝完成后,通知应用程序,这是不同于信号驱动的。

  • 进行异步IO需要调用异步IO接口,比如aio_read,调用时预先给操作系统提供一块缓冲区,调用后会立刻返回。
  • 当数据就绪时不需要告知进程,直接包内核缓冲区的数据拷贝到用户缓冲区,之后通过某种特定的信号告知该进程。
    最后我们再来总结一下,这五种IO模型哪种的效率是最高的呢?前面也说过,只要等待的比重低,那么它的效率一定高,所以一定是多路转接这种方式的效率是最高的。

阻塞IO VS 非阻塞IO

系统中大部分的接口都是阻塞式的接口,我们之前使用的read或者是recvfrom,这些都是阻塞式的,所以我们下面就来谈一谈非阻塞IO。

我们之前使用系统调用打开文件时使用的open函数,函数的参数可以设置选项,其中就有打开方式,可以设置O_CREAT、O_RDONLY、O_WRONLY、O_APPEND 和 O_TRUNC等选项,还可以设置O_NONBLOCK 或 O_NDELAY选项,设置为非阻塞打开。

在套接字编程篇,设置socket也可以设置为非阻塞的,上面两个就是设置为字节流还是数据报。

所以在进行IO的时候,打开文件的时候就可以设置阻塞或非阻塞,但是我们不这样做,我们使用统一的方式来进行设置,使用 fcntl()函数。

一个文件的属性 中一定有这个文件是否是阻塞的,当使用系统调用时,操作系统要检查struct_file中的属性,如果是阻塞,那么就会直接挂起;如果当前设置非阻塞,操作系统不挂起,那就直接返回。

我们使用的函数就是fcntl。

cpp 复制代码
int fcntl(int fd, int cmd, ... /* arg */ );

参数:

  • fd:已经打开的文件描述符。
  • cmd:需要进行的操作。
  • ...:可变参数,传入的cmd值不同,后面追加的参数也不同。

fcntl函数常用的5种功能与其对应的cmd取值如下:

  • 复制一个现有的描述符 (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)

其中可以设置cmd为 F_GETFL 或 F_SETFL 来获取和设置文件读写标志位,这些大写字母就是使用位图方式,最后的可变参数可以使用按位或(|)来传参。

返回值:

  • 调用成功,则返回值取决于具体进行的操作。
  • 调用失败,则返回-1,同时错误码会被设置。

我们使用的标准输入本来就是一个阻塞式,当我们调用read,从0号文件描述符中读取数据,如果不输入,那就会阻塞住,原因就是底层数据不就绪,read需要阻塞等待。

下面我们就实现一个函数,向该函数传入指定的文件描述符,设置为非阻塞状态。

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>

// 对指定的文件描述符设置为非阻塞
bool SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
    if (fl < 0)
    {
        std::cout << "fcntl error" << std::endl;
        return false;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 要设置一个新的读写标志位,而且还要将非阻塞的选项传入
}
cpp 复制代码
#include <iostream>
#include <unistd.h>

int main()
{
    SetNonBlock(0); // 只要设置一次,0号文件描述符就是非阻塞的了
    char buffer[1024];
    while (true)
    {
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s - 1] = 0;
            std::cout << "echo# " << buffer << std::endl;
        }
        else
        {
            std::cout << "read \"error\": " << s << std::endl;
        }
        sleep(1);
    }
    return 0;
}

我们看到的现象就是,如果我们不输入,read的返回值一直是错误,所以非阻塞的时候是以出错的形式返回,告知上层数据没有就绪;如果数据就绪,那就正常读取。

但是如何甄别是真的出错了,还是没有数据就绪呢,这时就要使用cerrno这个库中的errno,出错了,返回错误码,并且errno被设置,标明出错原因,使用strerror就可以打印错误信息。

cpp 复制代码
int main()
{
    SetNonBlock(0); // 只要设置一次,0号文件描述符就是非阻塞的了
    char buffer[1024];
    while (true)
    {
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s - 1] = 0;
            std::cout << "echo# " << buffer << ", erron: " << errno << ", errorstring: " << strerror(errno) << std::endl;
        }
        else
        {
            std::cout << "erron: " << errno << ", errorstring: " << strerror(errno) << std::endl;
        }
        sleep(1);
    }
    return 0;
}

这里不管成功与否,errno都是11,原因就是如果数据就绪,errno没有被设置,如果想要看到errno为0就在循环开始设置errno为0即可。

所以errno被设置为11就不能叫出错,所以前面我们说过非阻塞如果数据未准备好返回的是EWOULDBLOCK。

cpp 复制代码
#define	EAGAIN		11	    /* Try again */
#define	EWOULDBLOCK	EAGAIN	/* Operation would block */

还有一种也不能叫做错误,就是返回的是以EINTR,这个就是当阻塞式读取的时候,进程收到一个信号,此时该进程就要被操作系统唤醒处理信号,处理完信号后就不是挂起状态了,因为没有再调用read,所以这个也要处理一下。

cpp 复制代码
int main()
{
    SetNonBlock(0); // 只要设置一次,0号文件描述符就是非阻塞的了
    char buffer[1024];
    while (true)
    {
        sleep(1);
        errno = 0;
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s - 1] = 0;
            //std::cout << "echo# " << buffer << std::endl;
            std::cout << "echo# " << buffer << ", erron: " << errno << ", errorstring: " << strerror(errno) << std::endl;
        }
        else
        {
            if (errno == EWOULDBLOCK || errno == EAGAIN)
            {
                std::cout << "当前0号fd数据没有就绪, 请再试一次" << std::endl;
                continue;
            }
            else if (errno == EINTR)
            {
                std::cout << "当前IO可能被信号中断, 请再试一次" << std::endl;
                continue;
            }
            else 
            {
                // 差错处理
            }
        }
    }
    return 0;
}

所以底层没有数据就绪的时候就是非阻塞式,如果有数据就依次读取。

相关推荐
结衣结衣.3 分钟前
python中的函数介绍
java·c语言·开发语言·前端·笔记·python·学习
茫茫人海一粒沙6 分钟前
Python 代码编写规范
开发语言·python
原野心存6 分钟前
java基础进阶知识点汇总(1)
java·开发语言
程序猿阿伟8 分钟前
《C++高效图形用户界面(GUI)开发:探索与实践》
开发语言·c++
暗恋 懒羊羊16 分钟前
Linux 生产者消费者模型
linux·开发语言·ubuntu
五味香33 分钟前
C++学习,信号处理
android·c语言·开发语言·c++·学习·算法·信号处理
梓䈑1 小时前
【C语言】自定义类型:结构体
c语言·开发语言·windows
安红豆.1 小时前
Linux基础入门 --13 DAY(SHELL脚本编程基础)
linux·运维·操作系统
..空空的人1 小时前
linux基础指令的认识
linux·运维·服务器