对常用I/O模型进行比较说明

1.网络 I/O 模型

1.1I/O 模型相关概念

同步/异步:关注的是消息通信机制,即调用者在等待一件事情的处理结果时,被调用者是否提供完成状态的通知。

  • 同步:synchronous,被调用者并不提供事件的处理结果相关的通知消息,需要调用者主动询问事情是否处理完成
  • 异步:asynchronous,被调用者通过状态、通知或回调机制主动通知调用者被调用者的运行状态。

image-20220623150939142

阻塞/非阻塞:关注调用者在等待结果返回之前所处的状态

  • 阻塞:blocking,指IO操作需要彻底完成后才返回到用户空间,调用结果返回之前,调用者被挂起,干不了别的事情。

  • 非阻塞:nonblocking,指IO操作被调用后立即返回给用户一个状态值,而无需等到IO操作彻底完成,在最终的调用结果返回之前,调用者不会被挂起,可以去做别的事情。

    image-20220623151037259

同步+阻塞:相当于人烧水,一直在旁边看着,不能做其他事情,自己看是否开没开。

同步+非阻塞:烧水时,人不盯着,可以去做其他事情,但是开没开还是需要自己看。

异步+阻塞:烧水器烧开水会报警通知,但是人不能去做其他事情,要盯着。

异步+非阻塞:烧水器烧开水会报警通知,人不用守在跟前,可以去做其他事情。

2.网络 I/O 模型

阻塞型、非阻塞型、复用型、信号驱动型、异步

2.1 阻塞型 I/O 模型(blocking IO)

**应用程序通过系统调用内核,让内核处理磁盘文件,不可能马上处理完,应用程序进程就要一直等(等待状态),阻塞时间包括第一阶段(内核把数据从网络中数据复制到内核空间,用时更长)和第二阶段(把内核空间数据复制到用户空间),用户空间收到数据后,才会去做别的事情。**一个用户占用一个进程,处理能力有限。

image-20220623151107252

阻塞IO模型是最简单的I/O模型,用户线程在内核进行IO操作时被阻塞
用户线程通过系统调用read发起I/O读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作
用户需要等待read将数据读取到buffer后,才继续处理接收的数据。整个I/O请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够

优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源

缺点:每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,apache 的preforck使用的是这种模式。

同步阻塞:程序向内核发送I/O请求后一直等待内核响应,如果内核处理请求的IO操作不能立即返回,则进程将一直等待并不再接受新的请求,并由进程轮训查看I/O是否完成,完成后进程将I/O结果返回给Client,在IO没有返回期间进程不能接受其他客户的请求,而且是有进程自己去查看I/O是否完成,这种方式简单,但是比较慢,用的比较少。

2.2 非阻塞型 I/O 模型 (nonblocking IO)

进程虽然未挂起,但是处理完任务,会不断地轮询去询问是否完成,这一阶段不阻塞,也不会去处理其他事情,反而增加i/o次数,第二阶段还是会阻塞。

image-20220623165955643

用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。即 “轮询”机制存在两个问题:如果有大量文件描述符都要等,那么就得一个一个的read。这会带来大量的Context Switch(read是系统调用,每调用一次就得在用户态和核心态切换一次)。轮询的时间不好把握。这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU而已,是比较浪费CPU的方式,一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

非阻塞:程序向内核发送请I/O求后一直等待内核响应,如果内核处理请求的IO操作不能立即返回IO结果,进
程将不再等待,而且继续处理其他请求,但是仍然需要进程隔一段时间就要查看内核I/O是否完成。

查看上图可知,在设置连接为非阻塞时,当应用进程系统调用 recvfrom 没有数据返回时,内核会立即返回一个 EWOULDBLOCK 错误,而不会一直阻塞到数据准备好。如上图在第四次调用时有一个数据报准备好了,所以这时数据会被复制到 应用进程缓冲区 ,于是 recvfrom 成功返回数据

当一个应用进程这样循环调用 recvfrom 时,称之为轮询 polling 。这么做往往会耗费大量CPU时间,实际使用很少

2.3 多路复用 I/O 型(I/O multiplexing)(优秀,高并发)

用户发起请求,交给代理程序select,在第一阶段(数据给内核之前)也是阻塞的,知识阻塞在select上,select与客户端打交道,通知给进程内核的处理情况。第二阶段(把数据从内核空间复制到应用程序缓冲区)也是阻塞的,这阶段是进程阻塞。减少用户和系统的之间的交互,让select和用户交互。单个process就可以同时处理多个网络连接的IO。

I/O multiplexing 主要包括:select,poll,epoll三种系统调用,select/poll/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。

它的基本原理就是select/poll/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

Apache prefork是此模式的select,work是poll模式。

IO多路复用(IO Multiplexing) :是一种机制,程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”

IO多路复用一般和NIO一起使用的。NIO和IO多路复用是相对独立的。NIO仅仅是指IO API总是能立刻返回,不会被Blocking;而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制这俩必须得一起用,可以只用IO多路复用 + BIO,这时还是当前线程被卡住。IO多路复用和NIO是要配合一起使用才有实际意义

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,就通知该进程

多个连接共用一个等待机制,本模型会阻塞进程,但是进程是阻塞在select或者poll这两个系统调用上,而不是阻塞在真正的IO操作上

用户首先将需要进行IO操作添加到select中,同时等待select系统调用返回。当数据到达时,IO被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视IO,以及调用select函数的额外操作,效率更差。并且阻塞了两次,但是第一次阻塞在select上时,select可以监控多个IO上是否已有IO操作准备就绪,即可达到在同一个线程内同时处理多个IO请求的目的。而不像阻塞IO那种,一次只能监控一个IO

虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只是注册自己需要的IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率

IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO模型,而非真正的异步IO

优缺点

  • 优点:可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源
  • 缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低,可能延迟更大,因为单个连接处理需要 2 次系统调用,占用时间会有增加

IO多路复用适用如下场合:

  • 当客户端处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用
  • 当一个客户端同时处理多个套接字时,此情况可能的但很少出现
  • 当一个服务器既要处理监听套接字,又要处理已连接套接字,一般也要用到I/O复用
  • 当一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用
  • 当一个服务器要处理多个服务或多个协议,一般要使用I/O复用

2.4 信号驱动式 I/O 模型 (signal-driven IO)

**第一阶段(数据从网路复制到内核空间,用时更长)不阻塞,进程可以去干其他事情,一个进程可以处理多个请求;第二阶段(数据从内核空间复制到用户空间)处于阻塞状态。**这样可以节约时间,提高性能。

信号驱动I/O的意思就是进程现在不用傻等着,也不用去轮询。而是让内核在数据就绪时,发送信号通知进程。

调用的步骤是,通过系统调用 sigaction ,并注册一个信号处理的回调函数,该调用会立即返回,然后主程序可以继续向下执行,当有I/O操作准备就绪,即内核数据就绪时,内核会为该进程产生一个 SIGIO信号,并回调注册的信号回调函数,这样就可以在信号回调函数中系统调用 recvfrom 获取数据,将用户进程所需要的数据从内核空间拷贝到用户空间

此模型的优势在于等待数据报到达期间进程不被阻塞。用户主程序可以继续执行,只要等待来自信号处理函数的通知。

在信号驱动式 I/O 模型中,应用程序使用套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞

当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。优点:线程并没有在等待数据时被阻塞,内核直接返回调用接收信号,不影响进程继续处理其他请求因此可以提高资源的利用率
缺点:信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知

异步阻塞:程序进程向内核发送IO调用后,不用等待内核响应,可以继续接受其他请求,内核收到进程请求后
进行的IO如果不能立即返回,就由内核等待结果,直到IO完成后内核再通知进程。

2.5 异步 I/O 模型 (asynchronous IO)(优秀)

第一阶段和第二阶段,进程都可以继续执行,彻底解放进程。

image-20220623170611593

异步I/O 与 信号驱动I/O最大区别在于,信号驱动是内核通知用户进程何时开始一个I/O操作,而异步I/O是由内核通知用户进程I/O操作何时完成,两者有本质区别,相当于不用去饭店场吃饭,直接点个外卖,把等待上菜的时间也给省了

相对于同步I/O,异步I/O不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。

信号驱动IO当内核通知触发信号处理程序时,信号处理程序还需要阻塞在从内核空间缓冲区拷贝数据到用户空间缓冲区这个阶段,而异步IO直接是在第二个阶段完成后,内核直接通知用户线程可以进行后续操作了

优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠

缺点:要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O,在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时以 IO 复用模型模式+多线程任务的架构基本可以满足需求

Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。

异步非阻塞:程序进程向内核发送IO调用后,不用等待内核响应,可以继续接受其他请求,内核调用的IO如果不能立即返回,内核会继续处理其他事物,直到IO完成后将结果通知给内核,内核在将IO完成的结果返回给进程,期间进程可以接受新的请求,内核也可以处理新的事物,因此相互不影响,可以实现较大的同时并实现较高的IO复用,因此异步非阻塞使用最多的一种通信方式  

3.五种 IO 对比

这五种 I/O 模型中,越往后,阻塞越少,理论上效率也是最优前四种属于同步 I/O,因为其中真正的 I/O操作(recvfrom)将阻塞进程/线程,只有异步 I/O 模型才与 POSIX 定义的异步 I/O 相匹配

4.常用I/O模型比较

image-20220623173917061

select是没有通知的,主动遍历的,进程越多,遍历时间越长,效率越低。一个进程最多同一时间能够处理1024个任务,epoll使用回调,主动通知的机制,不需要比遍历,处理任务数无上限。

Select:
POSIX所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理

缺点
单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义

FD_SETSIZE,再重新编译内核实现,但是这样也会造成效率的降低

单个进程可监视的fd数量被限制,默认是1024,修改此值需要重新编译内核

对socket是线性扫描,即采用轮询的方法,效率较低

select 采取了内存拷贝方法来实现内核将 FD 消息通知给用户空间,这样一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll:
本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态其没有最大连接数的限制,原因是它是基于链表来存储的

大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义

poll特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd

select是边缘触发即只通知一次
epoll:
在Linux 2.6内核中提出的select和poll的增强版本
支持水平触发LT和边缘触发ET,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次
使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
优点:
没有最大并发连接的限制:能打开的FD的上限远大于1024(1G的内存能监听约10万个端口),具体查看/proc/sys/fs/file-max,此值和系统内存大小相关
效率提升:非轮询的方式,不会随着FD数目的增加而效率下降;只有活跃可用的FD才会调用callback函数,即epoll最大的优点就在于它只管理“活跃”的连接,而跟连接总数无关
内存拷贝,利用mmap(Memory Mapping)加速与内核空间的消息传递;即epoll使用mmap减少复制开销

总结:

1、epoll只是一组API,比起select这种扫描全部的文件描述符,epoll只读取就绪的文件描述符,再加入基于事件的就绪通知机制,所以性能比较好
2、基于epoll的事件多路复用减少了进程间切换的次数,使得操作系统少做了相对于用户任务来说的无用功。
3、epoll比select等多路复用方式来说,减少了遍历循环及内存拷贝的工作量,因为活跃连接只占总并发连接的很小一部分。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值