五种IO模型详解( 二 )


当然 , 实现零复制技术的方法有多种 , 见我的另一篇结束零复制的文章:零复制(zero copy)技术 。
以下是以httpd进程处理文件类请求时比较完整的数据操作流程 。
五种IO模型详解文章插图
大致解释下:客户端发起对某个文件的请求 , 通过TCP连接 , 请求数据进入TCP的recv buffer , 再通过recv()函数将数据读入到app buffer , 此时httpd工作进程对数据进行一番解析 , 知道请求的是某个文件 , 于是发起read系统调用 , 于是内核加载该文件 , 数据从磁盘复制到kernel buffer再复制到app buffer , 此时httpd就要开始构建响应数据了 , 可能会对数据进行一番修改 , 例如在响应首部中加一个字段 , 最后将修改或未修改的数据复制(例如send()函数)到send buffer中 , 再通过TCP连接传输给客户端 。
2 I/O模型所谓的IO模型 , 描述的是出现I/O等待时进程的状态以及处理数据的方式 。 围绕着进程的状态、数据准备到kernel buffer再到app buffer的两个阶段展开 。 其中数据复制到kernel buffer的过程称为数据准备阶段 , 数据从kernel buffer复制到app buffer的过程称为数据复制阶段 。 请记住这两个概念 , 后面描述I/O模型时会一直用这两个概念 。
本文某些地方以httpd进程的TCP连接方式处理本地文件为例 , 请无视httpd是否真的实现了如此、那般的功能 , 也请无视TCP连接处理数据的细节 , 这里仅仅只是作为方便解释的示例而已 。
再次说明 , 从硬件设备到内存的数据传输过程是不需要CPU参与的 , 而内存间传输数据是需要内核线程占用CPU来参与的 。
2.1 Blocking I/O模型如图:
五种IO模型详解文章插图
假设客户端发起index.html的文件请求 , httpd需要将index.html的数据从磁盘中加载到自己的httpd app buffer中 , 然后复制到send buffer中发送出去 。
但是在httpd想要加载index.html时 , 它首先检查自己的app buffer中是否有index.html对应的数据 , 没有就发起系统调用让内核去加载数据 , 例如read() , 内核会先检查自己的kernel buffer中是否有index.html对应的数据 , 如果没有 , 则从磁盘中加载 , 然后将数据准备到kernel buffer , 再复制到app buffer中 , 最后被httpd进程处理 。
如果使用Blocking I/O模型:
(1).当设置为blocking i/o模型 , httpd从到都是被阻塞的 。
(2).只有当数据复制到app buffer完成后 , 或者发生了错误 , httpd才被唤醒处理它app buffer中的数据 。
(3).cpu会经过两次上下文切换:用户空间到内核空间再到用户空间 , 第一次是发起系统调用的切换 , 第二次是内核将数据拷贝到app buffer完成后的切换 。
(4).由于阶段的拷贝是不需要CPU参与的 , 所以在阶段准备数据的过程中 , cpu可以去处理其它进程的任务 。
(5).阶段的数据复制需要CPU参与 , 将httpd阻塞 。
(6).这是最省事、最简单的IO模式 。
如下图:
五种IO模型详解文章插图
2.2 Non-Blocking I/O模型(1).当设置为non-blocking时 , httpd第一次发起系统调用(如read())后 , 立即返回一个错误值EWOULDBLOCK , 而不是让httpd进入睡眠状态 。 UNP中也正是这么描述的 。
When we set a socket to be nonblocking, we are telling the kernel "when an I/O operation that I request cannot be completed without putting the process to sleep, do not put the process to sleep, but return an error instead.
(2).虽然read()立即返回了 , 但httpd还要不断地去发送read()检查内核:数据是否已经成功拷贝到kernel buffer了?这称为轮询(polling) 。 每次轮询时 , 只要内核没有把数据准备好 , read()就返回错误信息EWOULDBLOCK 。


推荐阅读