【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;
}
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
### 回答1: Linux IO 模型是指 Linux 操作系统中的 IO 处理机制。它的目的是解决多个程序同时使用 IO 设备时的资源竞争问题,以及提供一种高效的 IO 处理方式。 Linux IO 模型主要分为三种:阻塞 IO非阻塞 IOIO 多路复用。 阻塞 IO 指的是当程序进行 IO 操作时,会被挂起直到 IO 操作完成,这种方式简单易用,但是对于高并发环境不太适用。 非阻塞 IO 指的是程序进行 IO 操作时,如果无法立即完成,会立即返回一个错误码,程序可以通过循环不断地进行 IO 操作来实现轮询的效果。非阻塞 IO 可以提高程序的响应速度,但是会增加程序的复杂度。 IO 多路复用指的是程序可以同时监听多个 IO 设备,一旦有 IO 事件发生,就会立即执行相应的操作。IO 多路复用可以提高程序的效率,但是需要程序员手动编写代码来实现。 Linux IO 模型还有其他的实现方式,比如信号驱动 IO 和异步 IO 等。但是这些方式的使用比较复杂,一般不常用。 ### 回答2: LinuxIO模型是指操作系统在处理输入输出(IO)时的工作方式和机制。Linux支持多种IO模型,包括阻塞IO非阻塞IOIO多路复用和异步IO。 1. 阻塞IO(Blocking IO):当应用程序发起IO操作时,会一直阻塞等待IO操作完成才会返回结果。在阻塞IO模式下,内核会一直等待IO完成,期间CPU处于空闲状态,无法处理其他任务。 2. 非阻塞IO(Non-blocking IO):非阻塞IO模式下,应用程序通过设置IO文件描述符为非阻塞模式,并不断地轮询IO操作的状态。如果IO操作没有立即完成,应用程序不会等待,而是继续执行其他任务。这种模式下,CPU利用率较高,但需要消耗大量的轮询时间。 3. IO多路复用(IO Multiplexing):IO多路复用指的是通过select、poll、epoll等系统调用,能够同时监听多个IO事件。当任意一个IO事件准备就绪时,操作系统会通知应用程序进行IO操作。IO多路复用模型能够支持同时处理多个IO事件,提高了系统的整体性能。 4. 异步IO(Asynchronous IO):异步IO模式下,应用程序发起IO操作后立即返回,而不需要等待IO操作的完成。当IO操作完成后,操作系统会通知应用程序进行结果获取。异步IO模型能够在等待IO操作完成的同时进行其他任务,减少了等待时间,提高了系统的并发性能。 不同的IO模型适用于不同的场景和需求。阻塞IO适用于简单的应用程序,非阻塞IO适用于需要同时处理多个IO事件的高负载情况,IO多路复用适用于需要同时监听多个IO事件的场景,异步IO适用于需要高并发处理IO操作的应用程序。 ### 回答3: LinuxIO模型主要包括阻塞IO非阻塞IO、多路复用IO和异步IO。 1. 阻塞IO模型:当用户进程发起IO操作时,如果操作不能立即完成,则进程会被阻塞,直到操作完成或出现错误。这种模型简单直观,适用于处理短时间内并发IO操作较少的情况,但会造成进程资源浪费。 2. 非阻塞IO模型:用户进程发起IO操作之后,会立即返回,不会被阻塞。进程可以通过轮询或者信号来检查IO操作是否完成,从而执行其他任务。这种模型适用于需要处理多个IO事件并希望不阻塞进程的情况。 3. 多路复用IO模型:通过使用select、poll或epoll等系统调用,将多个IO操作集中在一个系统调用中同时等待,从而有效提高IO效率。当有其中任何一个IO就绪时,进程被通知进行处理,可以同时处理多个IO事件,减少了轮询的开销。 4. 异步IO模型:用户进程发起IO操作后,不需要等待操作完成或者被通知,而是继续执行其他任务。当IO操作完成后,系统会通知进程进行处理。这种模型适用于需要处理多个IO事件且IO操作较耗时的情况。 总结来说,LinuxIO模型提供了多种选择,可以根据应用程序的需求和设计特点来选择适合的模型,以提高IO效率和系统性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

命由己造~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值