只看一次就能理解:IO 模型详解。

2024-07-01 22:58:04 作者:6kYzQ!yIEmp_M6UkZ
[[437724]] 开头

大家好,我是程序员田螺。 让我们一起学习IO模型吧。在开始本文之前,先问大家几个问题哈~

IO是什么?阻塞IO和非阻塞IO有什么区别?同步异步IO分别是两种不同的输入输出处理方式。在同步IO中,程序会等待数据被读取或写入完成之后才能继续执行下一步操作。而在异步IO中,程序会继续执行后续操作,不需要等待数据的读取或写入完成。这两种方式各有优势,可以根据具体的需求来选择适合的IO处理方式。IO多路复用是一种技术,允许一个进程监视多个输入/输出流。这意味着一个程序可以同时处理多个输入输出操作,提高了系统资源利用率和性能。select和epoll都是在编程中用于实现IO多路复用的机制。它们都可以监听多个IO事件,并在有事件发生时通知程序进行处理。它们与IO模型的关系在于,它们可以与不同的IO模型结合使用,如阻塞IO、非阻塞IO、IO多路复用等,来实现不同的IO处理方式。因此,select和epoll与IO模型有着密切的关联,可以用于构建各种不同的IO处理模式。IO模型有几种经典的呢?BIO、NIO和AIO有何不同之处呢?如果你可以很好地回答这些问题,那么恭喜你,你已经掌握了很好的IO技能!和田螺哥一起仔细阅读完这篇文章,然后再进行复习,加强记忆哦~如果你对这些问题有些困惑,也没关系,读完这篇文章之后就会明白了!IO是输入/输出的缩写形式,用来表示设备与计算机之间的数据交换。输入指的是数据从外部设备进入计算机,输出则是计算机向外部设备传送数据。在计算机领域,IO一般指代输入/输出操作或设备。IO是Input/Output的英文缩写,意思是输入/输出。我们经常听到很多关于磁盘输入输出和网络输入输出的知识。IO到底是什么?你是不是感到有些迷茫,似乎大致知道它是什么,又好像说不清楚。输入/输出(I/O)是指输入和输出,究竟是谁是输入呢?谁是发言者呢?如果IO脱离了主体,会让人感到困惑。在计算机领域中,我们经常谈论的输入输出(I/O)从计算机的角度来看,更直观地指的是计算机的主体进行的输入输出操作。大家还记得大学学习计算机组成原理时,学过冯.诺依曼结构吗?它将计算机分成了5个部分:运算器、控制器、存储器、输入设备、输出设备。输入设备是用来将数据和信息输入到计算机的设备,例如键盘和鼠标;输出设备则是计算机硬件系统的终端设备,用于显示计算机数据输出,例如显示器和打印机。比如,当你使用鼠标和键盘进行操作时,它会将你的指令数据传输给主机,主机经过计算后,将返回的数据信息输出到显示器。鼠标和显示器只是表面的输入输出设备,从计算机架构的角度来看,涉及到数据在计算机核心和其他设备之间的传输过程,这就是IO。磁盘IO是指从磁盘读取数据到内存中,这被视为一次输入操作;相应地,将内存中的数据写入磁盘被认为是输出操作。这就是输入输出的本质。当我们需要将内存中的数据写入磁盘时,主要涉及到的是操作系统的IO操作。可能的主体是一个应用程序,例如一个Java进程(假设网络传输的是二进制数据,Java进程可以将其写入磁盘)。操作系统负责管理计算机的资源和调度进程。我们电脑中的应用程序需要通过操作系统才能进行一些特殊操作,比如磁盘文件的读写以及内存的读写等。由于这些操作都具有一定的风险性,不能由应用程序随意执行,只能由底层操作系统来处理。换句话说,如果您的应用程序需要将数据写入磁盘,就需要使用操作系统提供的API来实现操作。用户空间是指操作系统中用于存储用户应用程序和数据的部分,与内核空间相对应。在用户空间中,用户可以运行和管理各种应用程序,但不能直接访问操作系统的核心功能。内核空间是操作系统中用于运行内核和系统进程的一部分内存区域。它是操作系统的核心部分,用于管理系统资源和执行关键任务。内核空间通常是受保护的,只有特权进程和内核能够访问。以32位操作系统为例,每个进程都能够分配4G(即2的32次方)的内存空间。可访问的4G内存空间分成了两部分,一部分是供用户使用的空间,另一部分是供内核使用的空间。操作系统内核访问的区域被称为内核空间,这是一个受保护的内存区域。而用户空间则是用户应用程序所能访问的内存区域。我们的应用程序在用户空间运行,它没有实际的I/O过程,真正的I/O是由操作系统执行的。应用程序的IO操作通常可以分为两种动作:IO调用和IO执行。进程(应用程序的运行状态)发起IO调用,而IO执行是由操作系统内核完成的工作。所谓的IO是指应用程序触发操作系统IO功能的一次操作,也就是所谓的IO调用。

操作系统进行一次IO过程时,

应用程序发起的IO操作包含两个阶段: IO调用:应用程序进程向操作系统内核发起调用。{ n } 输入/输出执行:操作系统内核执行输入/输出操作。操作系统内核进行IO操作包括两个主要过程:\n准备数据阶段:内核等待I/O设备准备好数据;\n拷贝数据阶段:将数据从内核缓冲区拷贝到用户进程缓冲区。\n实际上,IO操作就是将进程的内部数据传输到外部设备,或者将外部设备的数据传输到进程内部。外部设备通常是指硬盘和用于插槽通信的网卡。一个完整的输入输出过程包括以下步骤:

应用程序进程向操作系统发出IO调用请求

操作系统准备数据,将IO外部设备的数据加载到内核缓冲区

操作系统拷贝数据,即将内核缓冲区的数据复制到用户进程缓冲区

阻塞IO模型

我们已经掌握了IO的概念,那什么是阻塞IO呢?假设一个应用程序的进程发出IO调用,但是如果内核的数据还没有准备好,那么应用程序进程将会一直被阻塞,直到内核数据准备就绪并且从内核拷贝到用户空间后,该IO操作才会返回成功。这种IO操作被称之为阻塞IO。阻塞IO的一个经典应用是阻塞socket和Java的BIO。阻塞IO的不足之处在于:如果内核数据一直没有准备好,那用户进程将会持续阻塞,导致性能浪费。可以考虑使用非阻塞IO进行优化。非阻塞IO模型允许内核在数据准备就绪之前向用户进程返回错误信息,从而避免等待,并通过轮询再次请求。以下是非阻塞I/O的过程图:

非阻塞I/O的流程如下:

应用进程从操作系统内核发起recvfrom读取数据。操作系统内核数据尚未准备就绪,将立即返回EWOULDBLOCK错误码。应用程序进程会不断地进行轮询调用,以继续向操作系统内核发起recvfrom读取数据。内核数据已准备就绪,现在将其从内核缓冲区复制到用户空间。 调用已经成功完成,并返回成功提示。NIO是非阻塞IO模型的简称,全称Non-Blocking IO。与阻塞IO相比,非阻塞IO虽然有很大的性能提升,但仍然存在性能问题,就是频繁轮询会导致频繁的系统调用,同样会消耗大量的CPU资源。可以尝试采用IO复用模型来解决这个问题。多路复用模型既然有效,NIO轮询无效会导致CPU资源浪费。我们可以等待内核数据准备好,然后主动通知应用进程进行系统调用,这样不是更好吗?在此之前,让我们先回顾一下文件描述符(File Descriptor)是什么。它是计算机科学中的一个术语,形式上是一个非负整数。当程序打开已经存在的文件或者创建新文件时,内核会向进程返回一个文件描述符。IO复用模型的核心思想是:系统提供了一系列函数(比如我们熟悉的select、poll、epoll函数),它们能够同时监控多个文件描述符的操作。一旦其中任何一个返回内核数据已经准备就绪,应用进程就可以发起recvfrom系统调用。使用select函数,应用进程可以同时监控多个文件描述符(fd)。一旦 select 函数监控的文件描述符中有任何一个数据状态准备就绪,select 函数就会返回可读状态。这时应用进程可以发起 recvfrom 请求来读取数据。在非阻塞IO模型(NIO)中,需要进行N(N≥1)次系统调用来轮询,但是利用select的IO多路复用模型,只需要进行一次调用就可以了,极大地优化了性能。然而,select存在一些缺点:\n监听的I/O最大连接数受限,在Linux系统上通常为1024。select函数返回后,通过遍历fdset来找到就绪的描述符fd。只知道发生了I/O事件,但不清楚是哪些流,因此需要遍历所有流。由于存在连接数限制,后来又提出了poll。poll解决了连接数限制的问题,相较于select有了改进。然而,无论是select还是poll,都需要通过循环遍历文件描述符来获取已经准备就绪的socket。当大量客户端同时连接时,可能只有很少数量处于就绪状态,而且随着监视描述符数量的增加,效率也会呈线性下降。为此,经典的多路复用模型epoll应运而生。epoll是多路复用机制中的一种,旨在解决select和poll存在的一些问题,并采用事件驱动模型实现。其流程包括使用epoll_ctl()来注册文件描述符fd,当某个fd就绪时,内核会通过回调机制快速激活该fd,从而通知进程调用epoll_wait()。这里取消了遍历文件描述符的繁琐操作,而是采用了监听事件回调的机制。这就是epoll的优势所在。让我们一起总结一下select、poll和epoll的不同之处。\n底层数据结构:\n- select使用数组\n- poll使用链表\n- epoll使用红黑树和双链表\n获取就绪的fd:\n- select需要遍历\n- poll需要遍历\n- epoll通过事件回调\n事件复杂度:\n- select是O(n)\n- poll是O(n)\n- epoll是O(1)\n最大连接数:\n- select是1024\n- poll没有限制\n- epoll没有限制\nfd数据拷贝:\n- 每次调用select都需要拷贝数据需要将文件描述符(fd)的数据从用户空间复制到内核空间 。每次调用poll时,都需要将文件描述符的数据从用户空间复制到内核空间 。而使用内存映射(mmap)时,则无需频繁地将文件描述符的数据从用户空间复制到内核空间 。

epoll明显地优化了IO的执行效率,但在进程调用epoll_wait()时,仍可能会被阻塞。可以这样吗:我不必时常询问你数据是否已经准备好,我只需要在发送请求后,当数据准备好了你通知我即可,这就是信号驱动IO模型的由来。可以不要每次都问我数据准备好了吗,我们可以用信号驱动I/O模型,当我发出请求后,数据准备好了就通知我。信号驱动I/O模型是一种I/O模型,不再需要通过主动询问的方式确认数据是否准备就绪,而是通过向内核发送一个信号(使用sigaction建立一个SIGIO信号),然后应用程序可以去做其他事情,而不必被阻塞。一旦核心数据准备好,就会发送SIGIO信号通知应用程序进程,表明数据已经准备好可以读取。当应用程序的用户进程接收到信号后,立即执行recvfrom操作以读取数据。

采用信号驱动的IO模型,一旦应用进程发出信号,立即返回,不会阻塞进程。它已经具有了异步操作的感觉。然而当您仔细观察上面的流程图时,会发现在数据复制到应用缓冲时,应用进程仍然处于阻塞状态。从另一个角度来看,无论是BIO、NIO还是信号驱动,当数据从内核复制到应用缓冲时,都会发生阻塞。还有其他方法可以改进吗?AIO (真正的非阻塞I/O)!IO 模型中的异步IO(AIO)在前面提到的BIO、NIO和信号驱动中,数据从内核复制到应用缓冲时都是阻塞的,因此不能被视为真正的异步。AIO实现了IO全流程的非阻塞,即应用程序进程发出系统调用后立即返回,但返回的不是处理结果,而是表示提交成功的意思。等内核数据已就绪,将数据复制到用户进程的缓冲区,并发送信号通知用户进程IO操作已完成。

流程如下:优化

异步IO的思路很简单,只需要发送一次请求给内核,即可完成数据状态询问和数据拷贝等操作,且无需阻塞等待结果。在日常开发中常常遇到类似的业务场景:

例如发起一笔批量转账,但是批量转账的处理时间较长。这时,后端可以先通知前端转账提交成功,等批量转账处理完成后再通知前端处理结果。阻塞、非阻塞、同步、异步的I/O分类:\nIO模型\n- 阻塞I/O模型: 同步阻塞\n- 非阻塞I/O模型: 同步非阻塞\n- I/O多路复用模型: 同步阻塞\n- 信号驱动I/O模型: 同步非阻塞\n- 异步IO(AIO)模型: 异步非阻塞\n通俗例子理解BIO、NIO、AIO\n- 同步阻塞(blocking-IO)简称BIO\n- 同步非阻塞(non-blocking-IO)简称NIO\n- 异步非阻塞(asynchronous-non-blocking-IO)简称AIO\n生活例子:\n小明去吃同仁四季的椰子鸡。在那里排队等了一个小时,然后才开始享用火锅。小红也去同仁四季尝椰子鸡,她发现要等很长时间,于是她去商场逛逛。每次逛完一圈,她就跑回餐馆看看是不是该轮到她了。小红也去了同仁四季的椰子鸡店。她发现需要等很长时间,于是决定去商场逛一逛。每隔一段时间就回来看看,是否轮到她了。最终,她既逛了一番商场,又品尝到了椰子鸡。小华去吃椰子鸡时,由于他是高级会员,店长说可以去商场随便逛逛,等有位置时会马上给他打电话。小华再也不用无聊地坐着等待,也不必时不时地跑回来看是否已经到了。最终,他也品尝到了美味的椰子鸡。本文来自微信公众号“捡田螺的小男孩”,可通过扫描下方二维码关注。如需转载本文,请联系《捡田螺的小男孩》的公众号。

 

在线咨询 拨打电话

电话

02088888888

微信二维码

微信二维码