目录

IO

五种IO模型

阻塞式IO

非阻塞式IO

信号驱动IO

多路转接

异步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。

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需要阻塞等待。

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

#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); // 要设置一个新的读写标志位,而且还要将非阻塞的选项传入
}

#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就可以打印错误信息。

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。

#define	EAGAIN		11	    /* Try again */
#define	EWOULDBLOCK	EAGAIN	/* Operation would block */

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

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;
}

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

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部