操作系统I/O模型
IO模型
I/O在计算机中指Input/Output
IOPS (Input/Output Per Second)即每秒的输入输出量(或读写次数),是衡量磁盘性能的主要指标之一。IOPS是指单位时间内系统能处理的I/O请求数量,一般以每秒处理的I/O请求数量为单位,I/O请求通常为读或写数据操作请求
IO流程
一次完整的I/O是用户空间的进程数据与内核空间的内核数据的报文的完整交换,但是由于内核空间与用户空间是严格隔离的,所以其数据交换过程中不能由用户空间的进程直接调用内核空间的内存数据,而是需要经历一次从内核空间中的内存数据copy到用户空间的进程内存当中,所以简单说I/O就是把数据从内核空间中的内存数据复制到用户空间中进程的内存当中
Linux 的 I/O:
- 磁盘I/O
- 网络I/O : 一切皆文件,本质为对socket文件的读写
磁盘I/O:
-磁盘I/O是进程向内核发起系统调用,请求磁盘上的某个资源比如是html文件或者图片,然后内核通过相应的驱动程序将目标文件加载到内核的内存空间,加载完成之后把数据从内核内存再复制给进程内存,如果是比较大的数据也需要等待时间
网络I/O:
网络通信就是网络协议栈到用户空间进程的IO就是网络IO
简单理解就是:网卡接收的数据到用户空间进程,或用户空间进程到网卡发送数据
IO概念:
发起请求的人是调用者,接受请求的人是被调用者
同步/异步:
关注被调用者消息通知机制,处于内核态
消息通知:
同步(sychronous):
- 等待被调用者返回消息,,一旦返回,则是最终结果。也就是调用函数,运行后等待返回结果
- 被调用者不提供事件的处理结果相关通知信息,要调用者主动询问是否完成
异步(asychronous):
- 不立即等带结果返回。被调用者通过状态、通知或回调机制,告诉调用者,它自己的运行状态
例:内核准备好数据后,是否提供通知进程复制机制,提供时为异步;不提供为同步
阻塞/非阻塞:
关注调用者在结果返回之前的所处的状态,处于进程态
阻塞(blocking):
- 被调用者的结果返回之前,调用者被挂起(进程睡眠,不消耗cpu资源),只能等待完成
非阻塞(noblocking):
- 被调用者运行后立即返回给用户一个状态值,结果返回之前,调用者不会被挂起(进程一直在线,但会频繁询问被调用者结果,一直占用cpu资源),可以去做别的事情
例:进程处理请求时,一个进程可以多次处理请求为非阻塞;一个进程只能处理单个请求时,为阻塞
组合模型:
- 同步+阻塞:等待结果时,必须等到结果返回,调用者也卡住
- 同步+非阻塞:等待结果时,不用等到结果返回,调用者不会卡住等待,但会频繁询问状态
- 异步+阻塞:等待结果时,不用等到结果返回,因为结果会自动返回,但调用者还是卡住等待
- 异步+非阻塞:等待结果时,不用等到结果返回,因为结果会自动返回,调用者不会卡住等待
网络I/O模型:
阻塞型、非阻塞型、复用型、信号驱动型、异步
前三种IO都算同步,后面才算异步
IO请求都是由两部分组成,内核态和进程态
阻塞IO和非阻塞型IO:
一次IO请求:
先决步骤:进程发起文件读请求,申请内核调用
- 第一步:等待数据,内核去读取磁盘的数据,把数据先读到内核内存中
- 第二步:复制数据,把内核内存中的数据,复制到进程内存
两步都卡住了叫阻塞型IO;第一步过去了,卡在第二步叫非阻塞型IO
阻塞解读:
用户线程通过系统调用read发起I/O读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作用户需要等待read将数据读取到buffer后,才继续处理接收的数据。整个I/O请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用CPU资源缺点:每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,apache的prefork使用的是这种模式
非阻塞解读:
用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行
“轮询”机制存在两个问题:如果有大量文件描述符都要等,那么就得一个一个的read。这会带来大量的Context-Switch(read是系统调用,每调用一次就得在用户态和核心态切换一次)。轮询的时间不好把握。这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU而已,是比较浪费CPU的方式,一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性
复用型IO(multiplexing): 同步非阻塞
多个进程不再与内核沟通,有一个代理程序负责接收进程的io请求,由此代理程序与内核进行io调用(阻塞在第一步),当内核把数据都复制到内核内存后,代理程序通知进程开始复制内核内存数据到进程内存。但进程还是属于阻塞型IO
3种io复用机制:
Apache prefork是此模式的select,work是poll模式
- select 最多帮助一个进程监控1024个IO请求
- poll 无限制
- epoll poll的升级版,无限制
I/O多路复用深度解读:
IO多路复用是一种机制,程序注册一组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次系统调用,占用时间会有增加
适用场景:
- 当客户端处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用
- 当一个客户端同时处理多个套接字时,此情况可能的但很少出现
- 当一个服务器既要处理监听套接字,又要处理已连接套接字,一般也要用到I/O复用
- 当一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用
- 当一个服务器要处理多个服务或多个协议,一般要使用I/O复用
信号驱动型IO: 异步阻塞
进程发起IO调用,通过系统调用sigaction,注册一个信号函数并立刻返回信号,进程可以继续发起IO请求(不会卡在第一步),当内核完成第一步以后,就给进程发送信号通知进程并回调之前注册的系统信号,开始完成第二步(阻塞在第二步)。在做第二步的时候,不能再接收I/O,完成以后可继续发起IO,直到收到下一个做第二步的通知
event-driven(事件驱动):
apache event是此模式
- epoll(linux上的) libevent包提供
- kqueue(BSD)
- /dev/poll(Solaris)
异步IO(简写AIO): 异步非阻塞
进程发起IO请求后,完全不用参与后面的事情,全部交给内核自己完成,内核做完以后通知进程即可
Linux系统提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv
优点:
- 异步I/O能充分利用DMA特性,让IO操作与计算重叠
缺点:
- 要实现真正的异步 I/O,操作系统需要做大量的工作
五种 IO 对比
I/O 的具体实现方式:
- select
- linux/win平台,最大并发1024,最早期使用
- poll
- linux平台,select升级版,但是没有并发限制
- 编译nginx时,可开启/关闭:--with/without-poll_module
- epoll
- Nginx服务器支持的最高性能的事件驱动库之一,是公认的非常优秀的事件驱动模型,它和select和poll有很大的区别,epoll是poll的升级版,但是与poll有很大的区别.
- epoll的处理方式是创建一个待处理的事件列表,然后把这个列表发给内核,返回的时候在去轮训检查这个表,以判断事件是否发生,epoll支持一个进程打开的最大事件描述符的上限是系统可以打开的文件的最大数,同时epoll库的I/O效率不随描述符数目增加而线性下降,因为它只会对内核上报的“活跃”的描述符进行操作
- rtsig
- 不是一个常用事件驱动,最大队列1024,不是很常用
- kqueue
- BSD平台,事件驱动模型,也是poll的变种,与epoll一样
- /dev/poll unix衍生平台(solaris等),事件驱动模型
- eventport
- iocp win平台,对应异步io模型
常用I/O复用机制比较:
select:
- select通知进程时,采用的遍历方式,所有的进程io请求从头到尾遍历一遍,看哪些是内核准备好数据了的
- io效率是每次都调用线性遍历,时间复杂度O(n)
- 并发数最大1024
poll:
- poll通知进程时,采用的遍历方式,所有的进程io请求从头到尾遍历一遍,看哪些是内核准备好数据了的
- io效率是每次都调用线性遍历,时间复杂度O(n)
- 并发数无上限,只受物理内存限制,物理内存多大,并发多大
epoll: - 每个进程的io请求都有一个注册过的信号,内核数据准备完成后直接回调注册的信号。相当于使用索引
- io效率是事件通知方式,每次fd就绪,系统回调函数进行调用,讲就绪的fs放在rdllist里面,时间复杂度O(1)
- 并发数无上限,只受物理内存限制,物理内存多大,并发多大
查看系统最大并发数: cat /proc/sys/fs/file-max


浙公网安备 33010602011771号