Swift51.com
麦子学院 头像
麦子学院  2017-01-17 19:18

Linux学习之常见的5种IO 模型

回复:0  查看:2639  

本文和大家分享的主要是linux中常见的5IO 模型相关内容,一起来看看吧,希望对大家学习linux有所帮助。

   IO模型
  用一幅图表示所支持的I/O 模型
Linux学习之常见的5种IO 模型
纵向维度是阻塞( Blocking 非阻塞( Non-blocking ;横向维度是 同步 异步 。总结起来是四种模型  同步阻塞、同步非阻塞;异步阻塞、异步非阻塞  。《Unix 网络编程》中划分出了 第五种 模型 ——“ 信号驱动式 IO” 其实属于异步阻塞类型,这种模型的通知方式有多种多样后面展开说明。
   同步/异步、阻塞/非阻塞
  从内核角度看I/O 操作分为两步:用户层 API 调用;内核层完成系统调用(发起 I/O 请求)。所以 异步 / 同步 的是指 API 调用; 阻塞 / 非阻塞 是指内核完成 I/O 调用的模式。用一幅图表示更加明显
Linux学习之常见的5种IO 模型
同步是指函数完成之前会一直等待;  阻塞  是指系统调用的时候进程会被设置为Sleep 状态直到等待的事件发生(比如有新的数据)。明白这一点之后再看这五种模型相信就会清晰很多,我们挨个分析:
   同步阻塞
  这种模型最为常见,用户空间调用API (  read    write ) 会转化成一个 I/O 请求,一直等到 I/O 请求完成 API 调用才会完成。这意味着: 在 API 调用期间用户程序是同步的的;这个 API 调用会导致系统以阻塞的模式执行 I/O ,如果此时没有数据则一直 等待 (放弃 CPU 主动挂起 ——Sleep 状态) (注意,对于硬盘来说是不会出现阻塞的,无论是什么时候读它总是有数据。常见的阻塞设备是终端、网卡之类的)。
Linux学习之常见的5种IO 模型
  read  为例子,它由三个参数组成,第一个函数是文件描述符;第二个是  应用缓冲  ;第三个参数是需要读取的字节数。经过系统调用会以阻塞模式执行I/O I/O 模块读取数据后会放入到 PageCache 中;最后一步是把数据从 PageCache 复制到  应用缓冲  。如果I/O 请求无法得到满足 —— 没有数据,则主动让出 CPU 直到  有数据  (注意,即便系统调用让出CPU 也未必真的就让出。 read 函数是同步的,所以 CPU 还是会被用户空间代码占用)。
   同步非阻塞
  这种模式通过调用  read    write  的时候指定  O_NONBLOCK  参数。和同步阻塞 模式的区别在于系统调用的时候它是以非阻塞的方式执行,无论是否有数据都会立即返回。 以  read  为例,如果成功读取到数据它返回读取到的字节数;如果此时没有数据则返回-1 ,同时设置 errno EAGAIN (或者 EWOULDBLOCK ,二者相同)。所以这种模式下我们一般会用一个 循环 不停的尝试读取数据,处理数据。
   异步阻塞
  同步模型最主要的问题是占用CPU , 阻塞 I/O 会主动让出 CPU 但是用户空间的系统调用还是不会返回依然耗费 CPU ;非阻塞 I/O 必须不停的 轮询 不断尝试读取数据(会耗费更多 CPU 更加低效)。如果仔细分析同步模型霸占 CPU 的原因不难得出结论 —— 都是在等待数据到来。异步模式正是意识到这一点所以把 I/O 读取细化为 订阅 I/O 事件,实际 I/O 读写,在 订阅 I/O 事件 事件部分会主动让出 CPU 直到事件发生 。异步模式下的 I/O 函数和同步模式下的 I/O 函数是一样的(都是  read    write  )唯一的区别是异步模式  必有数据  而同步模式则未必。 常见的异步阻塞函数包括  select    poll    epoll  ,这些函数的用法需要花费相当大的篇幅介绍而这篇文章我想集中精力介绍“I/O 模型 。以  select  为例我们看一下大致原理
Linux学习之常见的5种IO 模型
异步模式下我们的API 调用分为两步,第一步是通过  select  订阅读写事件  这个函数会主动让出CPU 直到事件发生(设置为 Sleep 状态,等待事件发生) ; select 一旦返回就证明可以开始读了所以第二部是通过  read  读取数据必有数据 )
   异步阻塞模型之信号驱动
  完美主义者 看了上面的  select  之后会有点不爽—— 我还要 等待 读写事件(即便  select  会主动让出CPU ),能不能有读写事件的时候主动通知我啊?。借助 信号 机制我们可以实现这个,但是这并不完美而且有点弄巧成拙的意思。 具体用法:通过  fcntl  函数设置一个  F_GETFL|O_ASYNC    曾经信号驱动I/O 也叫 异步 I/O” 所以才有  O_ASYNC  的说法),当有I/O 时间的时候操作系统会触发  SIGIO  信号。在程序里只需要绑定  SIGIO  信号的处理函数就可以了。但是这里有个问题——  信号处理函数由哪个进程执行呢?  ,答案是:属主 进程。操作系统只负责参数信号而实际的信号处理函数必须由用户空间的进程实现。(这就是设置  F_SETOWN  为当前进程PID 的原因) 信号驱动性能要比  select    poll  高(避免文件描述符的复制)但是缺点是致命的——* Linux 中信号队列是有限制的如果操过这个数字问题就完全无法读取数据。
   异步非阻塞
  这种模型是最省事 的模型,系统调用完成之后就只要坐等数据就可以了。是不是特别爽?其实不然,问题出在实现上。 Linux 上的 AIO 两个实现版本, POSIX 的实现最烂(蓝色巨人的锅)性能很差而且是基于 事件驱动 还会出现 信号队列不足 的问题 ( 所以它就偷偷的创建线程,导致线程也不可控了 ) ;一个是 Linux 自己实现的( redhat 贡献) Native AIO Native AIO 主要涉及到的两个函数 io_submit  设置需要I/O 动作(读、写,数据大小,应用缓冲区等);  io_getevents  等待I/O 动作完成。没错,即便你的整个 I/O 行为是非阻塞的还是需要有一个办法知道数据是否读取 / 写入成功。
Linux学习之常见的5种IO 模型
注意图中,内核不再为I/O 分配 PageCache ,所有的数据必须有用户自己读取到应用缓冲中维护。所以 AIO 一定是和 直接 I/O” 配合使用。 AIO 针对网卡设备的意义不大,首先它的实现本质上和 epoll 差不多;其次它在 Linux 中的作用更多的是用于磁盘 I/O (异步非阻塞可以不用多线程就造成大量的 I/O 请求便于 I/O 模块 合并 优化会提高整体 I/O 的吞吐率 —— 而且对 CPU 开销比较少)。 在 Nginx 中用了一个技巧,可以实现 AIO epoll 联动, AIO 读取到数据后触发 epoll 发送数据。(这个特性是非常尴尬的,如果是磁盘文件完全可以用 sendfile 搞定)。
   Direct I/OBuffered I/O
  Linux 在进行 I/O 操作的时候会先把数据放到 PageCache 中然后通过 内存映射 的方式返回给应用程序,这样做的好处是可以预读数据也能在多个进程读取相同数据的时候起到 Cache 的作用。应用程序不能直接使用 PageCache 中的数据,通常是复制到一块 用户空间 的内存中再使用。
  ·Direct I/O 是指数据不落在 PageCache ,直接从设备读取到数据后放到用户空间中
  ·Buffered I/O 是指数据竞购 PageCache
  同步I/O 只能使用 Buffered I/O ;异步阻塞 I/O 可以 Buffered I/O 也可以使用 Direct I/O ;异步非阻塞 I/O 只能使用 Direct I/O
   Zero Copy
  考虑从磁盘读取文件经过网卡发送出去,会有  四次内存复制  1. DMA 会复制磁盘数据到内核空间, 2.  应用程序复制内核空间的数据到用户空间; 3.  应用程序用户空间的数据复制到 Socket 缓冲(内核空间); 4.  协议栈把数据复制到网卡的中发送。 简单来说 Zero Copy 就是 节省这个过程中的内存复制次数  。有几种做法:
  ·Direct I/O 直接把磁盘数据复制到内核空间; 但是 Direct I/O 没有办法直接把数据放到网卡中 —— 必须要经过协议栈 。所以可以节省一次内存复制;
  ·sendfile ,磁盘数据通过 DMA 读取到内核空间后直接交给 TCP/IP 协议栈;真正的不需要内存复制;
  除此之外还可以利用  splice    mmap  做一些优化,根据不同的设备需要采用不同的方式此处不再展开。
   最后的礼物
  我是一个 Java 程序员,出于职业的良知(公众账号里遍地的 Python C Erlang 都让我遗忘了自己的本性)最后我决定回归本性看一下 Java AIO 。  JDK7.0 引入了 AIO ,但是  JavaAIO其实内部是用Epoll实现  (自己去看LinuxAsynchronousChannelProvider.java 文件吧)。所以不要以为 Java AIO 会比 NIO 快多少多多,压根没有什么性能变化。
来源:公众号