五种IO模型详解

架构师-网络文章汇总
1 基础在引入IO模型前 , 先对io等待时某一段数据的"经历"做一番解释 。 如图:
五种IO模型详解文章插图
当某个程序或已存在的进程/线程(后文将不加区分的只认为是进程)需要某段数据时 , 它只能在用户空间中属于它自己的内存中访问、修改 , 这段内存暂且称之为app buffer 。 假设需要的数据在磁盘上 , 那么进程首先得发起相关系统调用 , 通知内核去加载磁盘上的文件 。 但正常情况下 , 数据只能加载到内核的缓冲区 , 暂且称之为kernel buffer 。 数据加载到kernel buffer之后 , 还需将数据复制到app buffer 。 到了这里 , 进程就可以对数据进行访问、修改了 。
现在有几个需要说明的问题 。
(1).为什么不能直接将数据加载到app buffer呢?
实际上是可以的 , 有些程序或者硬件为了提高效率和性能 , 可以实现内核旁路的功能 , 避过内核的参与 , 直接在存储设备和app buffer之间进行数据传输 , 例如RDMA技术就需要实现这样的内核旁路功能 。
但是 , 最普通也是绝大多数的情况下 , 为了安全和稳定性 , 数据必须先拷入内核空间的kernel buffer , 再复制到app buffer , 以防止进程串进内核空间进行破坏 。
(2).上面提到的数据几次拷贝过程 , 拷贝方式是一样的吗?
不一样 。 现在的存储设备(包括网卡)基本上都支持DMA操作 。 什么是DMA(direct memory access , 直接内存访问)?简单地说 , 就是内存和设备之间的数据交互可以直接传输 , 不再需要计算机的CPU参与 , 而是通过硬件上的芯片(可以简单地认为是一个小cpu)进行控制 。
假设 , 存储设备不支持DMA , 那么数据在内存和存储设备之间的传输 , 必须由内核线程占用CPU去完成数据拷贝(比如网卡不支持DMA时 , 内核负责将数据从网卡拷贝到kernel buffer) 。 而DMA就释放了计算机的CPU , 让它可以去处理其他任务 , DMA也释放了从用户进程切换到内核的过程 , 从而避免了用户进程在这个拷贝阶段被阻塞 。
再说kernel buffer和app buffer之间的复制方式 , 这是两段内存空间的数据传输 , 只能由内核占用CPU来完成拷贝 。
所以 , 在加载硬盘数据到kernel buffer的过程是DMA拷贝方式 , 而从kernel buffer到app buffer的过程是CPU参与的拷贝方式 。
(3).如果数据要通过TCP连接传输出去要怎么办?
例如 , web服务对客户端的响应数据 , 需要通过TCP连接传输给客户端 。
TCP/IP协议栈维护着两个缓冲区:send buffer和recv buffer , 它们合称为socket buffer 。 需要通过TCP连接传输出去的数据 , 需要先复制到send buffer , 再复制给网卡通过网络传输出去 。 如果通过TCP连接接收到数据 , 数据首先通过网卡进入recv buffer , 再被复制到用户空间的app buffer 。
同样 , 在数据复制到send buffer或从recv buffer复制到app buffer时 , 是内核占用CPU来完成的数据拷贝 。 从send buffer复制到网卡或从网卡复制到recv buffer时 , 是DMA方式的拷贝 , 这个阶段不需要切换到内核 , 也不需要计算机自身的CPU 。
如下图所示 , 是通过TCP连接传输数据时的过程 。
五种IO模型详解文章插图
(4).网络数据一定要从kernel buffer复制到app buffer再复制到send buffer吗?
不是 。 如果进程不需要修改数据 , 就直接发送给TCP连接的另一端 , 可以不用从kernel buffer复制到app buffer , 而是直接复制到send buffer 。 这就是零复制技术 。
例如 , 如果httpd进程不需要访问和修改任何数据 , 那么将数据原原本本地复制到app buffer再原原本本地复制到send buffer然后传输出去的过程中 , 从kernel buffer复制到app buffer的过程是可以省略的 。 使用零复制技术 , 就可以减少一次拷贝过程 , 提升效率 。
【五种IO模型详解】当然 , 实现零复制技术的方法有多种 , 见我的另一篇结束零复制的文章:零复制(zero copy)技术 。
以下是以httpd进程处理文件类请求时比较完整的数据操作流程 。
五种IO模型详解文章插图
大致解释下:客户端发起对某个文件的请求 , 通过TCP连接 , 请求数据进入TCP的recv buffer , 再通过recv()函数将数据读入到app buffer , 此时httpd工作进程对数据进行一番解析 , 知道请求的是某个文件 , 于是发起read系统调用 , 于是内核加载该文件 , 数据从磁盘复制到kernel buffer再复制到app buffer , 此时httpd就要开始构建响应数据了 , 可能会对数据进行一番修改 , 例如在响应首部中加一个字段 , 最后将修改或未修改的数据复制(例如send()函数)到send buffer中 , 再通过TCP连接传输给客户端 。