当前位置: 首页 > 科技观察

让我们从头到尾看一遍I-O模型

时间:2023-03-18 14:03:41 科技观察

转载本文请联系yes的练级指导公众号。你好,我是。在上一篇文章中,我们了解了socket通信的内幕,也了解了网络I/O中确实存在很多阻塞点。随着阻塞I/O的用户越来越多,我们只能通过增加线程的方式来处理更多的请求。线程不仅会占用内存资源,过多的线程竞争会导致上下文切换频繁,开销巨大。所以,blockingI/O已经不能满足需求了,于是后面的大佬不断优化进化,提出了多种I/O模型。在UNIX系统下,一共有五种I/O模型。今天我们就来看看他们吧!但是在介绍I/O模型之前,我们需要先了解一下前置知识。内核态和用户态我们的电脑可能会同时运行很多程序,而这些程序来自不同的公司。谁也不知道电脑上运行的某个程序会不会发疯,做一些奇怪的操作,比如定时清理内存。因此,CPU将非特权指令和特权指令划分开来,进行权限控制。有些危险的指令不会对普通程序开放,只会对操作系统等特权程序开放。你可以这样理解,我们的代码不能调用那些可能造成“危险”的操作,但是操作系统的内核代码可以调用。这些“危险”操作是指:内存分配与回收、磁盘文件读写、网络数据读写等等。如果我们要进行这些操作,只能调用操作系统开放的API,也称为系统调用。这就好比我们去行政大厅办业务,那些敏感的操作都是官方人员给我们处理的(系统调用),所以道理是一样的,目的就是防止我们(普通程序)乱来.这里又多了两个名词:用户空间和内核空间。我们普通程序的代码运行在用户空间,而操作系统的代码运行在内核空间,用户空间不能直接访问内核空间。当一个进程运行在用户空间时,它就处于用户态,当它运行在内核空间时,它就处于内核态。用户空间的程序在进行系统调用,即调用操作系统内核提供的API时,会切换上下文,切换到内核态,也就是常说的落入内核态。那么为什么一开始就介绍这个知识点呢?因为当程序请求获取网络数据时,需要经过两次拷贝:程序需要等待数据从网卡拷贝到内核空间。因为用户程序无法访问内核空间,内核不得不将数据拷贝到用户空间,这样用户空间的程序才能访问数据。介绍这么多,就是让大家明白为什么要有两份,系统调用是有开销的,最好不要频繁调用。那么我们今天讲的I/O模型的差距就是这个copy的实现方式不一样!今天我们就以read调用,即读取网络数据为例,对I/O模型进行扩展。离开!同步阻塞I/O当用户程序的线程调用read获取网络数据时,数据首先要可用,即网卡首先要从客户端接收到数据,然后需要将数据复制到内核,然后再复制到用户空间,整个进程用户线程被阻塞。假设没有客户端发送数据,用户线程就会被阻塞等待,直到有数据。就算有数据,两份的进程也得阻塞等待。所以这被称为同步阻塞I/O模型。它的优点很明显,简单。调用read之后,就不管它了,直到数据到达并准备好进行处理。劣势也很明显。一个线程对应一个连接,一直被占用。即使网卡没有数据过来,也会阻塞同步等待。我们都知道线程是比较重的资源,有点浪费。所以我们不希望它像这样等待。于是就有了同步非阻塞I/O。同步非阻塞I/O从图中我们可以清楚的看出,同步非阻塞I/O是在同步阻塞I/O的基础上进行优化的:当没有数据的时候,可以不再傻傻地阻塞等待,而是直接返回一个错误,告诉我们还没有准备好的数据!这里需要注意的是,在从内核拷贝到用户空间的步骤中,用户线程仍然会被阻塞。这种模型比同步阻塞I/O更灵活。比如调用read时没有数据,线程可以先做其他事情,然后继续调用read看有没有数据。但是如果你的线程是取数据然后处理数据,而不做其他逻辑,那么这个模型就有点问题了。这意味着你在不断地进行系统调用。如果你的服务器需要处理大量的连接,那么你就需要大量的线程不断调用,上下文切换频繁,CPU就会忙死,做无用功,忙死。那么该怎么办?于是就有了I/O多路复用。从图中看,I/O多路复用似乎和上面的同步非阻塞I/O有些相似。其实不一样,线程模型不一样。既然同步非阻塞I/O在连接太多的情况下频繁调用太浪费了,那就请个专家吧。这个专员的工作是管理多个连接,并帮助检查连接上的数据是否就绪。换句话说,您可以只使用一个线程来检查数据是否已准备好用于多个连接。具体到代码上,这个委托是select,我们可以把需要监听的连接注册到select,select会监听自己管理的连接是否有数据就绪,如果有就可以通知其他线程去读取数据。这次读取和之前一样,还是会阻塞用户线程。这样就可以用少量的线程来监听多个连接,减少线程数,减少内存消耗,减少上下文切换次数,很舒服。到现在为止,您大概已经了解什么是I/O多路复用了。所谓多通道是指多个连接,多路复用是指能够用一个线程监听这么多的连接。看到这里,再想一想,还有什么可以优化的?虽然信号驱动I/O上的选择没有被阻塞,但它必须随时检查是否有任何数据准备就绪。那可能吗?让内核告诉我们数据到了而不是轮询?信号驱动I/O可以实现这个功能。内核告诉我们数据准备好了,然后用户线程去读(还是block)。它听起来比I/O多路复用好吗?那为什么很少听到信号驱动I/O呢?为什么市面上都是I/O多路复用而不是信号驱动?因为我们的应用程序通常使用TCP协议,而TCP协议的socket可以产生七种信号事件。也就是说,不仅数据准备好时会发出信号,其他事件也会发出信号,而且这个信号是同一个信号,所以我们的应用程序没有办法区分是什么事件产生了这个信号。那是麻木!所以我们的应用基本上不会使用信号来驱动I/O,但是如果你的应用使用的是UDP协议,那也没关系,因为UDP没有那么多事件。所以从这个角度来看,信号驱动I/O对我们来说并不太好。异步I/O信号驱动I/O对TCP不是很友好,但是思路是正确的:它是异步发展的,但不是完全异步的,因为它后面的读还是会阻塞用户线程,所以考虑半异步。所以,我们要考虑怎么让它完全异步,也就是省去读这一步的阻塞。其实思路很清晰:让内核直接把数据拷贝到用户空间,然后通知用户线程,实现真正的非阻塞I/O!所以异步I/O其实就是用户线程调用aio_read,然后包括从内核拷贝数据到用户空间的步骤,所有的操作都是由内核来完成的。内核运行完成后,调用之前设置的回调。这时候用户线程就可以用已经复制到用户控件的数据继续进行后续的操作了。整个过程中,用户线程没有任何阻塞点,是真正的非阻塞I/O。那么问题又来了:为什么要使用I/O多路复用而不是异步I/O呢?因为Linux对异步I/O的支持不够,你可以认为它没有完全实现,所以不能使用异步I/O。O.可能有人会说不对。Tomcat已经实现了AIO实现类。事实上,你使用的这些组件或一些类库似乎支持AIO(异步I/O)。其实底层实现是通过epoll来模拟的。的。Windows实现了真正的AIO,但是我们的服务器一般部署在Linux上,所以主流还是I/O多路复用。最后,到这里,您一定已经了解了五种I/O模型是如何演变的。在下一篇文章中,我将谈谈网络I/O经常伴随的几个容易混淆的概念:同步、异步、阻塞和非阻塞。参考:https://time.geekbang.org/column/article/100307https://zhuanlan.zhihu.com/p/266950886我是,从一点点到十亿位,下篇见~