【linux】五种IO模型与非阻塞IO

一、IO的概念

前面我们说过其实IO就是拷贝数据。
先说一下读取的接口:

当系统调用read/recv的时候会有两种情况
①没有数据,阻塞等待。
②有数据,read/recv拷贝完成后返回。

阻塞的本质就是等待资源(缓冲区)就绪。而且写数据也需要的等待(发送缓冲区被写满)。

由此得出IO不仅仅是拷贝数据:
IO = 等待资源就绪 + 拷贝数据

而我们说的IO效率低并不是拷贝的效率低,而是等的时间长。
所以有一个概念叫做高效IO,它的本质就是减少等待的时间(等待的比重)。

二、IO的五种模型

2.1 概念

先举个例子:

现在有几个人在钓鱼:
张三下勾后就一直死盯着鱼鳔,什么都不做,等待着鱼上钩。
李四下勾后一会看看书一会看看鱼鳔一会玩玩手机。
王五在钓竿上挂了个铃铛,下勾后就做自己的事情,铃铛响了头也不抬就钓上了鱼。
赵六有很多鱼竿,全部下勾后就一直遍历看是否有鱼上钩。
田七自己不钓,让别人钓,钓完后通知田七即可,田七最后直接获得鱼。

作为旁观者,我们认为只要一个人等待的时间少那么他的钓鱼效率就高。
由此判断赵六的效率最高,因为他的鱼竿多,鱼上钩的概率大,等待的时间就少。

把上述场景类比到计算机:

张三:阻塞IO
李四:非阻塞IO
王五:信号驱动IO
赵六:多路转接/复用
田七:异步IO
这几个人就相当于进程,田七雇佣的人就是操作系统,鱼就是数据,鱼塘就是内核空间,鱼鳔就是数据就绪的事件,鱼竿就是文件描述符,钓鱼的整个动作就是read/recv调用

  • 阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。
  • 非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。

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

  • 信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。
  • IO多路转接: 虽然看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

进程受阻于select调用,select只负责等(无拷贝能力),当文件描述符就绪时(select返回时),用其他的IO类接口完成拷贝。

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

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

2.2 对比五种IO

  • 阻塞IO、非阻塞IO、信号驱动IO的对比

阻塞IO、非阻塞IO、信号驱动IO它们三个在IO上效率上没有区别(只有一个鱼竿)。
那在其他方面呢?在其他方面 非阻塞IO、信号驱动IO可以做更多的事情。
而阻塞IO和非阻塞IO"钓鱼"是一样的,不同的是等待的方式

  • 阻塞IO与非阻塞IO的对比

阻塞IO当数据资源没有准备好的时候会把进程放到等待队列中挂起,得到结果后才能返回。
而非阻塞IO当数据资源没有准备好的时候会直接返回(得知了数据资源没准备好)。

  • 信号驱动IO有没有等待?

等了,只不过等待的方式不一样。

  • 同步IO与异步IO

除了异步IO,其他几种IO都是进程自己参与了IO的过程(钓 + 等),所以称为同步IO
而因为田七没有参与IO的任何一个阶段,所以称作异步IO
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果。
异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

  • 为什么多路复用IO高效?

因为减少了等待的比重

三、非阻塞IO

打开文件时默认都是以阻塞的方式打开的,如果要以非阻塞的方式打开某个文件,需要在使用open函数打开文件时携带O_NONBLOCKO_NDELAY选项,此时就能够以非阻塞的方式打开文件。

3.1 fcntl文件描述符控制

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

int fcntl(int fd, int cmd, ... /* arg */ );

fcntl函数的作用是对文件描述符进行控制操作。它可以实现文件锁定、非阻塞I/O、修改文件状态标志等功能。

参数说明:

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)。

  • 具体实现非阻塞流程

先调用fcntl函数获取该文件描述符对应的文件状态标记(这是一个位图),此时调用fcntl函数时传入的cmd值为F_GETFL
获取到的文件状态标记上添加非阻塞标记O_NONBLOCK设置回去。

void setNoBlock(int fd) 
{
    
     
	 int fl = fcntl(fd, F_GETFL); 
	 if (fl < 0) 
	 {
    
     
	 	perror("fcntl");
	 	return; 
	 }
	 fcntl(fd, F_SETFL, fl | O_NONBLOCK); 
}

3.2 以非阻塞轮询方式读取标准输入

先来看看阻塞式输入的情况:

int main()
{
    
    
	char buf[1024];
	while(1)
    {
    
    
        std::cout << "[input]# ";
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof buf - 1);
        if(s > 0)
        {
    
    
            // 正常读取
            buf[s] = '\0';
            std::cout << "[echo]# " << buf << std::endl;
        }
        else if(s == 0)
        {
    
    
            // 输入完了
            std::cout << "read end" << std::endl;
            break;
        }
        else
        {
    
    
            // -1
        }
    }
	return 0;
}

在这里插入图片描述

可以看到如果我们没输入,它就会阻塞等待。
输入[Ctrl + d]就表示输入结束:
在这里插入图片描述


接下来看看非阻塞

bool setNonBlock(int fd)
{
    
    
    int fl = fcntl(fd, F_GETFL);
	if (fl < 0)
    {
    
    
		std::cerr << "fcntl: " << strerror(errno) << std::endl;
		return false;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
	return true;
}

int main()
{
    
    
    setNonBlock(0);
	char buf[1024];
	while(1)
    {
    
    
        std::cout << "[input]# ";
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof buf - 1);
        if(s > 0)
        {
    
    
            // 正常读取
            buf[s] = '\0';
            std::cout << "[echo]# " << buf << std::endl;
        }
        else if(s == 0)
        {
    
    
            // 输入完了
            std::cout << "read end" << std::endl;
            break;
        }
        else
        {
    
    
            // -1
        }
        sleep(1);
    }
	return 0;
}

在这里插入图片描述
可以看到一个现象就是我输入我的,它打印它的。

所以我们可以在不输入的时候执行其他任务

typedef std::function<void()> func_t;

void TaskA()
{
    
    
    std::cout << "TaskA" << std::endl;
}

void TaskB()
{
    
    
    std::cout << "TaskB" << std::endl;
}

void TaskC()
{
    
    
    std::cout << "TaskC" << std::endl;
}

void ExecOther(std::vector<func_t>& v)
{
    
    
    for(auto& func : v)
    {
    
    
        func();
    }
}

int main()
{
    
    
    std::vector<func_t> cbs;// 回调方法
    cbs.push_back(TaskA);
    cbs.push_back(TaskB);
    cbs.push_back(TaskC);
    setNonBlock(0);
	char buf[1024];
	while(1)
    {
    
    
        std::cout << "[input]# ";
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof buf - 1);
        if(s > 0)
        {
    
    
            // 正常读取
            buf[s] = '\0';
            std::cout << "[echo]# " << buf << std::endl;
        }
        else if(s == 0)
        {
    
    
            // 输入完了
            std::cout << "read end" << std::endl;
            break;
        }
        else
        {
    
    
            // -1
        }
        // 执行其他任务
        ExecOther(cbs);
        sleep(1);
    }
	return 0;
}

在这里插入图片描述

  • 当read返回值是-1时如何区分是错误还是底层没有数据?

观察上面的代码,read出错和底层没有数据都会返回-1,那么怎么区分它们呢?
通过错误码

else
{
    
    
    // -1
    std::cout << "errno: " << strerror(errno) << std::endl;
}

在这里插入图片描述
表示资源没有准备好。、

当read函数以非阻塞方式读取标准输入时,当底层数据不就绪时,read函数是以出错的形式返回的,此时的错误码会被设置为EAGAINEWOULDBLOCK

此外,调用read函数在读取到数据之前可能会被其他信号中断,此时read函数也会以出错的形式返回,此时的错误码会被设置为EINTR,此时应该重新执行read函数进行数据的读取

int main()
{
    
    
    std::vector<func_t> cbs;// 回调方法
    cbs.push_back(TaskA);
    cbs.push_back(TaskB);
    cbs.push_back(TaskC);
    setNonBlock(0);
	char buf[1024];
	while(1)
    {
    
    
        std::cout << "[input]# ";
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof buf - 1);
        if(s > 0)
        {
    
    
            // 正常读取
            buf[s] = '\0';
            std::cout << "[echo]# " << buf << std::endl;
        }
        else if(s == 0)
        {
    
    
            // 输入完了
            std::cout << "read end" << std::endl;
            break;
        }
        else
        {
    
    
            // -1
            if(errno == EAGAIN)
            {
    
    
                // 底层没有数据
                // 执行其他任务
                ExecOther(cbs);
            }
            else if(errno == EINTR)
            {
    
    
                // 被信号中断
                continue;
            }
            else
            {
    
    
                std::cout << "errno: " << strerror(errno) << std::endl;
                break;
            }
        }
        sleep(1);
    }
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_66314292/article/details/131860820