I/O模型与多路复用

I/O模型与多路复用

本文转载自这里,并加上了自身理解,感觉更透彻了

同步、异步、阻塞、非阻塞

同步 & 异步

同步与异步是针对多个事件(线程/进程)来说的。
同步和异步关注的是消息通信机制

  • 如果事件A需要等待事件B的完成才能完成,这种串行执行机制可以说是同步的,这是一种可靠的任务序列,要么都成功,要么都失败。
  • 如果事件B的执行不需要依赖事件A的完成结果,这种并行的执行机制可以说是异步的。事件B不确定事件A是否真正完成,所以是不可靠的任务序列。

同步异步可以理解为多个事件的执行方式和执行时机如何,是串行等待还是并行执行。同步中依赖事件等待被依赖事件的完成,然后触发自身开始执行,异步
中依赖事件不需要等待被依赖事件,可以和被依赖事件并行执行,被依赖事件执行完成后,可以通过回调、通知等方式告知依赖事件。

阻塞 & 非阻塞

阻塞与非阻塞是针对单一事件(线程/进程)来说的。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 对于阻塞,如果一个事件在发起一个调用之后,在调用结果返回之前,该事件会被一直挂起,处于等待状态。
  • 对于非阻塞,如果一个事件在发起调用以后,无论该调用当前是否得到结果,都会立刻返回,不会阻塞当前事件。

阻塞与非阻塞可以理解为单个事件在发起其他调用以后,自身的状态如何,是苦苦等待还是继续干自己的事情。非阻塞虽然能提高CPU利用率,但是也带来了系统线程切换的成本,需要在CPU执行时间和系统切换成本之间好好估量一下。

同步阻塞

应用程序执行系统调用,应用程序会一直阻塞,直到系统调用完成。应用程序处于不再消费CPU而只是简单等待响应的状态。当响应返回时,数据被移动到用户空间的缓冲区,应用程序解除阻塞。

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。(数据准备阶段,和数据拷贝阶段)

此处输入图片的描述

同步非阻塞

设备以非阻塞形式打开,I/O操作不会立即完成,read操作可能会返回一个错误代码。应用程序可以执行其他操作,但需要请求多次I/O操作,直到数据可用。

此处输入图片的描述

同步非阻塞形式实际上是效率低下的,因为:

  • 应用程序需要在不同的任务之间切换。异步非阻塞是你只需要执行当前任务,系统调用会主动通知你,不用频繁切换。
  • 数据在内核中变为可用到调用read返回数据之间存在时间间隔,会造成整体数据吞吐量降低

所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

异步非阻塞

应用程序的其他处理任务与I/O任务重叠进行。读请求会立即返回,说明请求已经成功发起(成不成功另说),应用程序不被阻塞,继续执行其它处理操作。当read响应到达,将数据拷贝到用户空间,产生信号或者执行一个基于线程回调函数完成I/O处理。应用程序不用在多个任务之间切换。

此处输入图片的描述

非阻塞I/O和异步I/O区别在于,在非阻塞I/O中,虽然进程大部分时间不会被block,但是需要不停的去主动check,并且当数据准备完成以后,也需要应用程序主动调用recvfrom将数据拷贝到用户空间;异步I/O则不同,就像是应用程序将整个I/O操作交给了内核完成,然后由内核发信号通知。期间应用程序不需要主动去检查I/O操作状态,也不需要主动从内核空间拷贝数据到用户空间。
相当于应用程序将IO完全托管给了内核,自己则去做其他的事了。

非阻塞I/O看起来是non-blocking的,但是只是在内核数据没准备好时,当数据准备完成,recvfrom需要从内核空间拷贝到用户空间,这个时候其实是被block住的。
而异步I/O是当进程发起I/O操作后,再不用主动去请求,直到内核数据准备好并发出信号通知,整个过程完全没有block。

没有异步阻塞,都异步了都不管了,还阻塞个P啊

几种常用I/O模型

BIO(同步阻塞)

总结:一个连接一个线程
同步并阻塞,服务器实现模式为 一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

服务器需要监听端口号,客户端通过IP和端口与服务器简历TCP连接,以同步阻塞的方式传输数据。服务端设计一般都是 客户端-线程模型,新来一个客户端连接请求,就新建一个线程处理连接和数据传输

当客户端连接较多时就会大大消耗服务器的资源,线程数量可能超过最大承受量

伪异步I/O (带线程池的同步阻塞)

与BIO类似,只是将客户端-线程的模式换成了线程池,可以灵活设置线程池的大小。但这只是对BIO的一种优化手段,并没有解决线程连接的阻塞问题。

NIO (异步非阻塞)

总结:一个请求一个线程,连接注册在多路复用器上,再启动线程,不停询问IO操作

同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。

利用selector多路复用器轮询为每一个用户创建连接,这样就不用阻塞用户线程,也不用每个线程忙等待。只使用一个线程轮询I/O事件,比较适合高并发,高负载的网络应用,充分利用系统资源快速处理请求返回响应消息,

适合:连接又多 IO时间又短

AIO (异步非阻塞,NIO升级版)

总结:一个有效请求一个线程。

异步非阻塞,需要操作系统内核线程支持,一个用户线程发起一个请求后就可以继续执行,内核线程执行完系统调用后会根据回调函数完成处理工作。

适合: I/O任务较长(不想等待浪费时间)

I/O多路复用

速记:挨个访问,准备好了的再建立连接。

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。

此处输入图片的描述
这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。

本质:(NIO)同步非阻塞I/O

优势:能处理更多的连接。系统开销小,不需要创建和维护额外线程或进程。

适合:处理多个客户端接入请求时

过程:
通过把多个I/O的阻塞复用到同一个select阻塞上,一个进程监视多个描述符(Socket),一旦某个描述符就位, 能够通知程序进行读写操作。因为多路复用本质上是同步I/O,都需要应用程序在读写事件就绪后自己负责读写。

应用场景
服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
需要同时处理多种网络协议的套接字
一个服务器处理多个服务或协议
目前支持多路复用的系统调用有select, poll, epoll。

select, poll, epoll(三者都是用于轮询)的区别

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select

实现:监视三种文件描述符(writefds、readfds、和exceptfds)
优点:良好跨平台支持
缺点:单个进程 能够监视的 文件描述符的数量存在最大限制

poll

实现:pollfd的指针实现,包括了要监视的event和发生的event
优点:没有最大数量限制,poll返回后,需要轮询pollfd来获取就绪的描述符

epoll 增强版本

优点:更加灵活,没有描述符限制。

epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。
虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

未完持续

Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2019 Jae's blog All Rights Reserved.

UV : | PV :