码农有道 打算用一篇文章介绍I/O模型,在引入IO模型前,本文先对io等待时某一段数据的'经历'做一番解释。如图: 当某个进程/线程(后文将不加区分的只认为是进程)需要某段数据时,它只能在用户空间中属于它自己的内存中访问、修改,这段内存暂且称之为app buffer(用户缓冲区)。 假设需要的数据在磁盘上,那么进程首先得发起相关系统调用,通知内核去加载磁盘上的文件。但正常情况下,数据只能加载到内核的缓冲区,暂且称之为kernel buffer。数据加载到kernel buffer之后,还需将数据复制到app buffer。到了这里,进程就可以对数据进行访问、修改了。 现在有几个需要说明的问题。 为什么不能直接将数据加载到app buffer呢? 实际上是可以的,有些程序或者硬件为了提高效率和性能,可以实现内核旁路的功能,避过内核的参与,直接在存储设备和app buffer之间进行数据传输,例如RDMA技术就需要实现这样的内核旁路功能。 但是,最普通也是绝大多数的情况下,为了安全和稳定性,数据必须先拷入内核空间的kernel buffer,再复制到app buffer。 上面提到的数据几次拷贝过程,拷贝方式是一样的吗? 不一样。现在的存储设备(包括网卡)基本上都支持DMA操作。什么是DMA(direct memory access,直接内存访问)?简单地说,就是内存和设备之间的数据交互可以直接传输,不再需要计算机的CPU参与,而是通过硬件上的芯片(可以简单地认为是一个小cpu)进行控制。 假设,存储设备不支持DMA,那么数据在内存和存储设备之间的传输,必须通过计算机的CPU计算从哪个地址中获取数据、拷入到对方的哪些地址、拷入多少数据(多少个数据块、数据块在哪里)等等,仅仅完成一次数据传输,CPU都要做很多事情。而DMA就释放了计算机的CPU,让它可以去处理其他任务。 再说kernel buffer和app buffer之间的复制方式,这是两段内存空间的数据传输,只能由CPU来控制。 所以,在加载硬盘数据到kernel buffer的过程是DMA拷贝方式,而从kernel buffer到app buffer的过程是CPU参与的拷贝方式。 如果数据要通过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操作方式的拷贝。 如下图所示,是通过TCP连接传输数据时的过程。 网络数据一定要从kernel buffer复制到app buffer再复制到send buffer吗? 不是。如果进程不需要修改数据,就直接发送给TCP连接的另一端,可以不用从kernel buffer复制到app buffer,而是直接复制到send buffer。这就是零复制技术。 例如httpd不需要访问和修改任何信息时,将数据原原本本地复制到app buffer再原原本本地复制到send buffer然后传输出去,但实际上复制到app buffer的过程是可以省略的。使用零复制技术,就可以减少一次拷贝过程,提升效率。 当然,实现零复制技术的方法有多种,后面会有一篇专门总结零复制的文章 以下是以httpd进程处理文件类请求时比较完整的数据操作流程。 大致解释下:客户端发起对某个文件的请求,通过TCP连接,请求数据进入TCP 的recv buffer,再通过recv()函数将数据读入到app buffer,此时httpd工作进程对数据进行一番解析,知道请求的是某个文件,于是发起某个系统调用(例如要读取这个文件,发起read()),于是内核加载该文件,数据从磁盘复制到kernel buffer再复制到app buffer,此时httpd就要开始构建响应数据了,可能会对数据进行一番修改,例如在响应首部中加一个字段,最后将修改或未修改的数据复制(例如send()函数)到send buffer中,再通过TCP连接传输给客户端。 |
|