企业实战|Linux 五种IO模型详细图解

一、IO 简述 IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间<–>内核空间、内核空间<–>设备空间(磁盘、网络等)。
企业实战|Linux 五种IO模型详细图解
文章图片

IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。
对于一个输入(读取到内存)操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。
所以,对于一个网络输入(读取网络资源)的操作,通常包括两个部分

  1. 等待网络数据到达网卡 --> 读取到内核空间 (这个过程就是准备数据)
  2. 从内核缓冲区复制数据到进程空间
  3. 然后具体的用户函数,把结果返回。
IO 操作发生时一般涉及两个对象,一个是调用这个IO的 process (or thread) ,另一个就是系统内核 (kernel)
二、IO 模型介绍 一共有五种 IO 模型
  1. 阻塞 IO
  2. 非阻塞 IO
  3. 多路复用 IO
  4. 信号驱动 IO
  5. 异步 IO
其中,前面4种是同步IO,最后一个是异步IO
2.1 阻塞 IO 阻塞I/O模型示意图:
企业实战|Linux 五种IO模型详细图解
文章图片

read为例:
(1)进程发起read,进行recvfrom系统调用;
(2)内核开始第一阶段,准备数据(从磁盘拷贝到缓冲区),进程请求的数据并不是一下就能准备好;准备数据是要消耗时间的;
(3)与此同时,进程阻塞(进程是自己选择阻塞与否),等待数据ing;
(4)直到数据从内核拷贝到了用户空间,内核返回结果,进程解除阻塞。
也就是说,内核准备数据和数据从内核拷贝到进程内存地址这两个过程都是阻塞的。
总结:阻塞IO,就是指当调用读取数据的函数(比如 read),这个函数不立马返回结果,而是阻塞当前进程,直到数据被复制到用户进程空间或者是超时出错才解除阻塞,并返回结果。
通俗的举例就是:
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。
缺点: 实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 recv(1024) 的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
2.2 non-block(非阻塞I/O模型) 当对一个non-blocking socket执行读操作时,流程是这个样子:
企业实战|Linux 五种IO模型详细图解
文章图片

(1)当用户进程发出read操作时,如果kernel中的数据还没有准备好;
(2)那么它并不会block用户进程,而是立刻返回一个error,从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果;
(3)用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call;
(4)那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO的特点是用户进程在内核准备数据的阶段需要不断的主动询问数据好了没有。
通俗的举例就是:
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
耗费:往返车站6次,路上6小时,其他时间做了好多事。
总结:在非阻塞 IO 中,用户在调用 read 方法后,进程没有被阻塞。内核会立马返回数据给进程,这个数据可能是error提示,也可能是最终的结果。如果要得到最终的结果,就需要用户进程主动的多次调用 read 方法。
缺点: 轮询调用 read 是很费CPU资源的,所以一般我们会在代码中加入 time.sleep(2)。但是加入了 sleep 以后,任务的执行时间长了,因为我们不确定,任务多久执行完成。
2.3 I/O多路复用 I/O多路复用实际上就是用select , poll, epoll监听多个io对象,当io对象有变化(有数据)的时候就通知用户进程。好处就是单个进程可以处理多个socket。当然具体区别我们后面再讨论,现在先来看下I/O多路复用的流程:
企业实战|Linux 五种IO模型详细图解
文章图片

(1)当用户进程调用了select,那么整个进程会被block;
? (2)而同时,kernel会“监视”所有select负责的socket;
(3)当任何一个socket中的数据准备好了,select就会返回;
(4)这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用多线程 + 阻塞 IO的web server性能更好,可能延迟还更大。
select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为非阻塞,但是,如上图所示,整个用户的process其实是一直被阻塞的。只不过process是被select这个函数阻塞,而不是被socket IO给阻塞。

典型应用:Java NIO、Nginx(epoll、poll、select)
专一进程解决多个进程IO的阻塞问题、性能好、Reactor模式
实现、开发应用难度较大
适用高并发服务应用开发、一个进程/线程响应多个请求
select/poll和select/epoll两种模式的区别:
1.select/poll
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
2.epoll
【企业实战|Linux 五种IO模型详细图解】老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话
2.4 信号驱动式IO模型 企业实战|Linux 五种IO模型详细图解
文章图片

信号驱动式IO就是指进程预先告知内核、向内核注册一个信号处理函数、然后用户进程返回不阻塞、当内核数据就绪时会发送一个信号给进程、用户进程便在信号处理函数中调用IO读取数据、从图中明白实际IO内核拷贝到用户进程的过程还是阻塞的、信号驱动式IO并没有实现真正的异步、因为通知到进程之后、依然是由进程来完成IO操作
通俗的举例就是:
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话
这和后面的异步IO模型很容易混淆、需要理解IO交互并结合五种IO模型的比较阅读
在信号驱动式IO模型中、依然不符合POSIX描述的异步IO、只能算是半异步、并且实际中并不常用、
回调机制、实现、开发应用难度大
2.5 异步IO模型 企业实战|Linux 五种IO模型详细图解
文章图片

用户进程发起aio_read(POSIX异步IO函数aio_或者lio_开头)操作之后、给内核传递描述符、缓冲区指针、缓冲区大小和read相同的三个参数以及文件偏移(与lseek类似)、告诉内核当整个操作完成时、如何通知我们、立刻就可以开始去做其它的事、而另一方面、从内核的角度、当它受到一个aio_read之后、首先它会立刻返回、所以不会对用户进程产生任何阻塞、然后、内核会等待数据准备完成、然后将数据拷贝到用户内存、当这一切都完成之后、内核会给用户进程发送一个信号、告诉它aio_read操作完成了
通俗的举例就是:
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话
异步IO的工作机制是:告知内核启动某个操作、并让内核在整个操作完成后通知我们、这种模型与信号驱动的IO区别在于、信号驱动IO是由内核通知我们何时可以启动一个IO操作、这个IO操作由用户自定义的信号函数来实现、而异步IO模型是由内核告知我们IO操作何时完成、
在异步IO模型中、真正实现了POSIX描述的异步IO、是五种IO模型中唯一的异步模型
典型应用:Java 7 AIO、高性能服务器应用
不阻塞、数据一步到位、Proactor模式
需要操作系统的底层支持、LINUX 2.5 版本内核首现、2.6 版本产品的内核标准特性
回调机制、实现、开发应用难度大
非常适合高性能高并发应用
三、五种IO模型的比较 企业实战|Linux 五种IO模型详细图解
文章图片

阻塞IO和非阻塞IO的区别在哪?
前面的介绍中其实已经很明确的说明了这两者的区别、调用阻塞会一直阻塞住对应的进程直到操作完成、而非阻塞IO在内核还没准备数据的情况下会立刻返回、阻塞和非阻塞关注的是进程在等待调用结果时的状态、阻塞是指调用结果返回之前、当前进程会被挂起、调用进程只有在得到结果才会返回、非阻塞调用指不能立刻得到结果、该调用不会阻塞当前进程、
同步IO和异步IO区别在哪?
同异步IO的根本区别在于、同步IO主动的调用recvfrom来将数据拷贝到用户内存、而异步则完全不同、它就像是用户进程将整个IO操作交给了他人(内核)完成、然后他人做完后发信号通知、在此期间、用户进程不需要去检查IO操作的状态、也不需要主动的去拷贝数据
信号驱动式IO和异步IO的区别?
信号驱动IO与异步IO的区别在于启用异步IO意味着通知内核启动某个IO操作、并让内核在整个操作(包括数据从内核复制到用户缓冲区)完成时通知我们、也就是说、异步IO是由内核通知我们IO操作何时完成、即实际的IO操作也是异步的、信号驱动IO是由内核通知我们何时可以启动一个IO操作、这个IO操作由用户自定义的信号函数来实现

    推荐阅读