当前位置: 首页 > 后端技术 > Java

操作系统的IO模型

时间:2023-04-01 17:16:02 Java

IO操作一般按照设备类型分为内存IO、网络IO、磁盘IO。其中内存IO的速度要比后两者快很多,计算机的性能瓶颈一般不在内存IO上。虽然可以通过购买专属带宽和高速网卡来增加网络IO,但是可以使用RAID磁盘阵列来增加磁盘IO的速度。但是,由于IO操作都是由系统内核调用完成的,而系统调用是通过CPU进行调度的,CPU的速度比IO操作要快很多,所以CPU宝贵的时间就会浪费在等待缓慢的IO操作。为了更好地协调CPU和慢速IO设备,减少CPU对IO调用的消耗,逐渐发展出各种IO模型。IO模型IO步骤I/O主要是:网络IO(本质上是socket文件读取),磁盘IO每次IO,对于一次IO访问,会先将数据复制到内核缓冲区,然后从内核缓冲区复制到应用程序的地址空间。需要经过两个阶段:第一步:先从文件加载数据到内核内存空间(buffer),等待数据准备完成,耗时较长;第二步:将数据从内核缓冲区拷贝到用户空间的进程内存中,阻塞/非阻塞和同步/异步IO模型总是离不开阻塞/非阻塞和同步/异步的概念。阻塞/非阻塞:阻塞和非阻塞是对调用线程状态的描述。如果调用者线程在一个IO过程中需要阻塞线程等待数据的到来,那么这个IO就被称为阻塞IO。Synchronous/Asynchronous:同步和异步是对调用者如何获取数据的描述。如果调用者主动查询和复制数据,那么就说这个IO是同步的。如果操作系统在数据准备好后(复制到用户缓存中)告诉调用者数据准备好了,那么就说IO是异步的。IO模型分类系统调用是由系统上运行的一个应用程序的进程发起的,对象是磁盘上的数据。要获取数据,需要I/O。整个过程就是应用程序等待获取磁盘数据。根据应用进程在整个过程中的不同状态,可以分为:同步阻塞型、同步非阻塞型、同步复用型、信号驱动型、异步。同步阻塞IO类比:老李去火车站买票,排了三天队,买了退票。消费:在车站吃喝玩乐睡了3天,别的什么都没做。同步阻塞IO模型是最简单的IO模型。用户线程在内核进行IO操作时被阻塞,数据读取完成后继续处理后续逻辑。步骤如下(以read()接口为例):read(file,tmp_buf,len);用户程序需要读取数据,调用read方法,将读取数据的指令交给CPU执行。CPU向DMA发送指令,告诉DMA从磁盘读取什么数据,然后返回,线程进入阻塞状态。DMA向磁盘控制器发送IO请求,告诉磁盘控制器读取什么数据,然后返回;磁盘控制器收到IO请求后,读取数据到磁盘缓存区,当磁盘缓存读取完成后,中断DMA;DMA接收到磁盘中断信号,将磁盘缓存区的数据读取到PageCache缓存区,然后中断CPU;CPU响应DMA中断信号,知道数据读取完成,然后将PageCache缓冲区中的数据读入用户缓存;用户程序从内存中读取数据后可以继续执行后续逻辑。同步阻塞IO优缺点优点:程序简单,进程/线程在阻塞等待数据的过程中挂掉,基本不占用CPU资源。缺点:每个连接都需要独立的进程/线程处理。当并发请求数很大时,为了维护程序,内存和线程切换的开销比较大。这种模式在实际生产中很少使用。同步非阻塞IO类比:老李去火车站买票,每隔12小时去火车站问有没有退票,三天后买票。消费:往返车站6次,路上6小时,其他时间有很多事情要做。非阻塞IO是指当调用者发起读取数据的应用时,如果内核数据没有准备好,会立即通知调用者,调用者线程不需要阻塞等待。以recvfrom方法为例。当调用者调用recvfrom读取数据时,如果缓冲区中没有数据,会直接返回EWOULDBLOCK错误,应用程序不会一直等待。当没有数据时,会立即返回错误标志,这也意味着应用程序要读取数据,需要不断调用recvfromrequest,直到读取到自己想要的数据。读取步骤如下:调用者调用recvfrom方法尝试获取数据;如果recvfrom方法返回EWOULDBLOCK错误,则转到步骤1;如果revifrom方法发现缓存中有数据,则转步骤3;CPU将PageCache缓存中的数据读入用户缓存;用户程序从内存中读取数据后可以继续执行后续逻辑。一种方法是在编程中为socket设置O_NONBLOCK。但是这种方法只对网络IO有效,对磁盘IO没有影响。因为本地文件IO默认是阻塞的,我们所说的网络IO阻塞是因为网络IO有无限阻塞的可能,除非本地文件被加锁,否则不可能无限阻塞,所以只有加锁。接下来,O_NONBLOCK就会起作用。而且在磁盘IO的时候,要么直接在kernelbuffer中返回数据,要么需要调用物理设备读取。这时,进程的其他任务需要等待。所以后续的IO多路复用和信号驱动IO对于文件IO也是没有意义的。IO多路复用模型IO多路复用也称为多通道IO就绪通知。这是进程提前通知内核的能力,让内核通知进程该进程指定的一个或多个IO条件已经准备就绪。使进程能够等待一系列事件。IO多路复用的实现方法主要有select、poll和epoll。Select/poll类比:老李去火车站买票,委托一个黄牛,然后每6小时打电话给黄牛查询。黄牛三天内买票,然后老李去火车站买票。费用:2次往返车站,路上2小时,100元黄牛费,17次电话查看这些fd的状态。当一个或多个fd就绪时,返回结果包括就绪和未就绪的fd。与select相比,poll解决了单个进程可以打开的文件描述符数量有限的问题。:select受FD_SIZE限制,如果修改,需要修改这个宏重新编译内核;而poll将需要关注的事件通过一个pollfd数组传递给内核,避开了文件描述符个数的限制。另外,select和poll共同的一大缺点是包含大量fds的数组在用户态和内核态地址空间之间作为一个整体进行复制,开销会随着fds数量的增加而线性增加。epoll老李去火车站买票,委托了黄牛。黄牛买了之后,通知老李去取,然后老李去火车站买票。成本:2次往返车站,路上2小时,100元黄牛费用,无需调用epoll是poll的改进:基于事件驱动的方式,避免了每次都扫描所有fds。epoll_wait只返回就绪的fds。epoll使用nmap内存映射技术,避免了内存拷贝的开销。epoll的fd个数的上限是操作系统的最大文件句柄数。这个数字一般和内存有关,通常比1024大很多。目前epoll是Linux2.6下最高效的IO多路复用方式,也是Nginx和Node的IO实现方式。freeBSD下,kqueue是另一种类似于epoll的IO多路复用方式。另外IO多路复用还有一个水平触发和边沿触发的概念:水平触发:当就绪的fd没有被用户进程处理时,下一次查询还是会返回,就是select和poll的触发方式。Edgetrigger:无论readyfd是否被处理,下次都不会返回。理论上性能更高,但实现起来相当复杂,任何意外丢失事件都会导致请求处理错误。epoll默认使用水平触发,可以通过相应的选项使用边缘触发。由于同步非阻塞方式需要不断的主动轮询,轮询占据了很大一部分进程,轮询会消耗大量的CPU时间,而且“后台”可能同时有多个任务,人们想到了循环多次查询任务的完成状态,只要有一个任务完成,就会处理。如果轮询不是进程的用户态,而是有人帮忙就好了。那么这就叫做“IO多路复用”。UNIX/Linux下的select、poll、epoll就是这样做的(epoll比poll和select效率更高,做的事情一样)。IO多路复用有两个特殊的系统调用select、poll和epoll函数。select调用在内核级别。selectpolling和non-blockingpolling的区别在于前者可以等待多个socket,同时监听多个IO口。当任何一个套接字的数据就绪时,它可以返回到可读状态,然后进程将进行recvform系统调用,将数据从内核复制到用户进程。当然,这个过程是被阻止的。调用select或poll后,进程将被阻塞。与阻塞IO不同的是,此时的select不会等到socket数据全部到达后再处理,而是调用用户进程处理部分数据。你怎么知道一部分数据已经到达?监控交给内核,内核负责数据到达的处理。也可以理解为“非阻塞”。I/O多路复用模型将使用select、poll和epoll函数。这两个函数也会阻塞进程,但是与阻塞I/O不同的是,这两个函数可以同时阻塞多个I/O操作。.并且可以同时检测多个读操作和多个写操作的I/O函数,直到有数据可读或可写时才真正调用I/O操作函数(注意不是所有数据都可读或可写)..用于多路复用,即轮询多个套接字。由于多路复用可以处理多个IO,因此带来了新的问题。多个IO之间的顺序变得不确定,当然也可以针对不同的数字。具体流程如下图所示:信号驱动模型类比:老李去火车站买票,给售票员留了电话。取票后,售票员给老李打电话,然后老李去火车站买票。费用:2次往返车站,路上2小时,免黄牛费100元,无需调用信号驱动IO模型,应用进程告诉??内核:数据报准备好后,发送我一个信号来捕获SIGIO信号,并调用我的信号处理程序来获取数据报。流程如下:打开socket信号驱动IO函数;系统调用sigaction执行信号处理函数(非阻塞,立即返回),告诉系统数据准备好后调用哪个函数;当数据准备好后,产生一个sigio信号,通过信号回调fetch数据通知应用读取。这种io方式有一个很大的问题:linux中的信号队列是有限制的,超过这个数量就无法读取数据。Linux信号处理:如果进程在用户态忙着做其他事情(比如计算两个矩阵的乘积),那么强行中断它,调用预先注册的信号处理函数。这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数突然闯入,就像中断处理程序一样,有很多事情是做不到的。所以,为了保险起见,一般都是把事件“注册”起来,放到队列中,然后再回到进程原来在干什么。事物。如果进程在内核态忙着做其他事情,比如以同步阻塞的方式读写磁盘,那么它就得暂停通知,等到内核态忙了,即将返回给用户状态,然后触发信号通知。如果现在进程挂起,比如无事休眠,那么唤醒进程,下次CPU空闲的时候,会调度到这个进程,并触发信号通知。异步API说起来容易做起来难,主要针对API实现者。Linux的异步IO(AIO)支持是在2.6.22引入的,现在还有很多系统调用不支持异步IO。Linux的异步IO本来就是为数据库设计的,所以通过异步IO进行的读写操作不会被缓存或缓冲,这样就无法利用操作系统的缓存和缓冲机制。很多人认为linux的O_NONBLOCK是一种异步方式,其实这就是上面说的同步非阻塞方式。需要指出的是,虽然Linux上的IOAPI略显粗糙,但是各个编程框架都有封装好的异步IO实现。操作系统做的工作更少,给用户留下更多的自由。这是UNIX的设计哲学,也是Linux上的编程框架蓬勃发展的原因之一。从前面IO模型的分类我们可以看出AIO的动机:同步阻塞模型需要在IO操作开始时阻塞应用。这意味着不可能同时重叠处理和IO操作。同步非阻塞模型允许处理和IO操作重叠,但这需要应用程序根据循环规则检查IO操作的状态。这就留下了异步非阻塞IO,它允许处理和IO操作重叠,包括IO操作完成的通知。异步IO打个比方:老李去火车站买票,给售票员留了电话。取票后,售票员打电话给老李,把票送到他家门口。费用:1趟往返车站,路上1小时,免黄牛费100元,不用打电话进程继续处理其他事情,是非阻塞状态。当内核准备好数据报时,内核将数据报复制到应用程序并返回定义在aio_read中的函数处理程序。很少有Linux系统支持它,Windows的IOCP就是模型。可以看出阻塞程度:阻塞IO>非阻塞IO>复用IO>信号驱动IO>异步IO,效率由低到高。欢迎关注鱼虎神的微信公众号参考文档IO与零拷贝异步IO、epoll、零拷贝IO概念及五种IO模型