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访问数据会先被拷贝到内核的缓冲区中然后才会从内核的缓冲区拷贝到应用程序的地址空间。需要经历两个阶段
- 第一步将数据从文件先加载至内核内存空间缓冲区等待数据准备完成时间较长
- 第二步将数据从内核缓冲区复制到用户空间的进程的内存中时间较短
阻塞/非阻塞和同步/异步
IO模型总是离不开阻塞/非阻塞、同步/异步这些概念。
- 阻塞/非阻塞阻塞和非阻塞是对调用方线程状态的描述如果一次IO过程中调用方线程需要阻塞线程等待数据的到达那么说这次IO是阻塞式IO。
- 同步/异步同步和异步是对调用方获取数据方式的描述如果调用方主动去查询并复制数据那么称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错误不会让应用一直等待中。在没有数据的时候会即刻返回错误标识那也意味着如果应用要读取数据就需要不断的调用recvfrom请求直到读取到它数据要的数据为止。其读取步骤如下所示
- 调用方调用recvfrom方法尝试获取数据
- 如果recvfrom方法返回EWOULDBLOCK错误执行步骤1如果revifrom方法发现缓存区有数据那么执行步骤3
- CPU将PageCache缓存区中的数据读取到用户缓存中
- 用户程序从内存中读取到数据可以继续执行后续逻辑。
种方式在编程中对socket设置O_NONBLOCK即可。但此方式仅仅针对网络IO有效对磁盘IO并没有作用。因为本地文件IO默认是阻塞我们所说的网络IO的阻塞是因为网路IO有无限阻塞的可能而本地文件除非是被锁住否则是不可能无限阻塞的因此只有锁这种情况下O_NONBLOCK才会有作用。而且磁盘IO时要么数据在内核缓冲区中直接可以返回要么需要调用物理设备去读取这时候进程的其他工作都需要等待。因此后续的IO复用和信号驱动IO对文件IO也是没有意义的。
IO复用模型
IO复用也叫多路IO就绪通知。这是一种进程预先告知内核的能力让内核发现进程指定的一个或多个IO条件就绪了就通知进程。使得一个进程能在一连串的事件上等待。IO复用的实现方式目前主要有select、poll和epoll。
select/poll
类比老李去火车站买票委托黄牛然后每隔6小时电话黄牛询问黄牛三天内买到票然后老李去火车站交钱领票。耗费往返车站2次路上2小时黄牛手续费100元打电话17次
select和poll的原理基本相同
- 注册待侦听的fd(这里的fd创建时最好使用非阻塞)
- 每次调用都去检查这些fd的状态当有一个或者多个fd就绪的时候返回
- 返回结果中包括已就绪和未就绪的fd
相比selectpoll解决了单个进程能够打开的文件描述符数量有限制这个问题select受限于FD_SIZE的限制如果修改则需要修改这个宏重新编译内核而poll通过一个pollfd数组向内核传递需要关注的事件避开了文件描述符数量限制。
此外select和poll共同具有的一个很大的缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间开销会随着fd数量增多而线性增大。
epoll
老李去火车站买票委托黄牛黄牛买到后即通知老李去领然后老李去火车站交钱领票。耗费往返车站2次路上2小时黄牛手续费100元无需打电话
epoll是poll的一种改进
- 基于事件驱动的方式避免了每次都要把所有fd都扫描一遍。
- epoll_wait只返回就绪的fd。
- epoll使用nmap内存映射技术避免了内存复制的开销。
- epoll的fd数量上限是操作系统的最大文件句柄数目,这个数目一般和内存有关通常远大于1024。
目前epoll是Linux2.6下最高效的IO复用方式也是Nginx、Node的IO实现方式。而在freeBSD下kqueue是另一种类似于epoll的IO复用方式。
此外对于IO复用还有一个水平触发和边缘触发的概念
- 水平触发当就绪的fd未被用户进程处理后下一次查询依旧会返回这是select和poll的触发方式。
- 边缘触发无论就绪的fd是否被处理下一次不再返回。理论上性能更高但是实现相当复杂并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发通过相应选项可以使用边缘触发。
由于同步非阻塞方式需要不断主动轮询轮询占据了很大一部分过程轮询会消耗大量的CPU时间而 “后台” 可能有多个任务在同时进行人们就想到了循环查询多个任务的完成状态只要有任何一个任务完成就去处理它。如果轮询不是进程的用户态而是有人帮忙就好了。那么这就是所谓的 “IO 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的epoll 比 poll、select 效率高做的事情是一样的。
IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的select轮询相对非阻塞的轮询的区别在于—前者可以等待多个socket能实现同时对多个IO端口进行监听当其中任何一个socket的数据准好了就能返回进行可读然后进程再进行recvform系统调用将数据由内核拷贝到用户进程当然这个过程是阻塞的。select或poll调用之后会阻塞进程与blocking IO阻塞不同在于此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢监视的事情交给了内核内核负责数据到达的处理。也可以理解为"非阻塞"吧。
I/O复用模型会用到select、poll、epoll函数这几个函数也会使进程阻塞但是和阻塞I/O所不同的的这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作多个写操作的I/O函数进行检测直到有数据可读或可写时注意不是全部数据可读或可写才真正调用I/O操作函数。
对于多路复用也就是轮询多个socket。多路复用既然可以处理多个IO也就带来了新的问题多个IO之间的顺序变得不确定了当然也可以针对不同的编号。具体流程如下图所示
信号驱动模型
类比老李去火车站买票给售票员留下电话有票后售票员电话通知老李然后老李去火车站交钱领票。耗费往返车站2次路上2小时免黄牛费100元无需打电话
信号驱动IO模型应用进程告诉内核当数据报准备好的时候给我发送一个信号对SIGIO信号进行捕捉并且调用我的信号处理函数来获取数据报。流程如下
- 开启套接字信号驱动IO功能
- 系统调用sigaction执行信号处理函数非阻塞立刻返回告诉系统数据就绪式调用哪个函数
- 数据就绪生成sigio信号通过信号回调通知应用来读取数据。
此种io方式存在的一个很大的问题Linux中信号队列是有限制的如果超过这个数字问题就无法读取数据。
Linux信号的处理如果这个进程正在用户态忙着做别的事例如在计算两个矩阵的乘积那就强行打断之调用事先注册的信号处理函数这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的因此跟中断处理程序一样有很多事情是不能做的因此保险起见一般是把事件 “登记” 一下放进队列然后返回该进程原来在做的事。
如果这个进程正在内核态忙着做别的事例如以同步阻塞方式读写磁盘那就只好把这个通知挂起来了等到内核态的事情忙完了快要回到用户态的时候再触发信号通知。
如果这个进程现在被挂起了例如无事可做 sleep 了那就把这个进程唤醒下次有 CPU 空闲的时候就会调度到这个进程触发信号通知。
异步 API 说来轻巧做来难这主要是对 API 的实现者而言的。Linux 的异步 IOAIO支持是 2.6.22 才引入的还有很多系统调用不支持异步 IO。Linux 的异步 IO 最初是为数据库设计的因此通过异步 IO 的读写操作不会被缓存或缓冲这就无法利用操作系统的缓存与缓冲机制。
很多人把 Linux 的 O_NONBLOCK 认为是异步方式但事实上这是前面讲的同步非阻塞方式。需要指出的是虽然 Linux 上的 IO API 略显粗糙但每种编程框架都有封装好的异步 IO 实现。操作系统少做事把更多的自由留给用户正是 UNIX 的设计哲学也是 Linux 上编程框架百花齐放的一个原因。
从前面 IO 模型的分类中我们可以看出 AIO 的动机
- 同步阻塞模型需要在 IO 操作开始时阻塞应用程序。这意味着不可能同时重叠进行处理和 IO 操作。
- 同步非阻塞模型允许处理和 IO 操作重叠进行但是这需要应用程序根据重现的规则来检查 IO 操作的状态。
- 这样就剩下异步非阻塞 IO 了它允许处理和 IO 操作重叠进行包括 IO 操作完成的通知。
异步IO
类比老李去火车站买票给售票员留下电话有票后售票员电话通知老李并快递送票上门。耗费往返车站1次路上1小时免黄牛费100元无需打电话
当应用程序调用aio_read时内核一方面去取数据报内容返回另一方面将程序控制权还给应用进程应用进程继续处理其他事情是一种非阻塞的状态。
当内核中有数据报就绪时由内核将数据报拷贝到应用程序中返回aio_read中定义好的函数处理程序。
很少有Linux系统支持Windows的IOCP就是该模型。可以看出阻塞程度阻塞IO>非阻塞IO>多路转接IO>信号驱动IO>异步IO效率是由低到高的。