零拷贝的方式以及理解
- DMA
- mmap
- sendfile
- sendfile + DMA scatter / Gather
- direct I/O
上一篇: 什么是零拷贝
DMA
正常的IO流程中,不管是物理设备之间的数据拷贝(比如:磁盘到内存),还是内存之间的数据拷贝(比如:用户态到内核态),都是需要CPU参与的。
看图:
如果是比较大的文件,这样没有任何意义的copy显然会极大的浪费CPU的效率,所以就诞生了DMA。
DMA的全称是Direct Memory Access , 顾名思义,DMA的作用就是之间将IO设备的数据拷贝到内核缓冲区中。
使用DMA的好处就是IO设备到内核之间的数据拷贝不需要CPU的参与,CPU只需要给DMA发送copy指令即可,提高了处理器的利用率。
看图:
mmap
正常的 read + write ,都会经历至少四次的数据拷贝,其中就包括内核态到用户态的copy,它的作用是为了安全和缓存。如果我们保证安全性,是否就让用户态和内核态共享一个缓冲区呢?这就是mmap的作用。
mmap , 全称是memory map , 翻译过来就是内存映射,顾名思义,就是将内核态和用户态的内存映射到一起。避免来回copy,实现这样的映射关系以后,进程就可以采用指针的方式读写操作这一段内存,而这个时间系统会自动会写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必调用 read 、 write 等系统调用函数。相反,内核空间对这段区域的修改也之间反映到用户空间,从而可以实现不同进程间的文件共享。其函数签名如下:
void *mmap(void *addr,size_t length,int port,int flags, int fd, off_t_offset);
一般来讲,mmap会替代read方法,模型我已画出来:
如果这个时候系统进行IO的话,采用mmap + write 的方式,内存拷贝的次数会编程三次,上下文切换则依然是4次。
需要注意的是,mmap采用基于缺页异常的懒加载模式。通过mmap申请1000G内存的话可能仅仅只占用100Mb的虚拟内存空间,甚至没有分配实际的物理内存空间,只有当真正访问的时候,才会通过缺失页中断 的方式分配内存,但mmap不是银弹,有以下原因:
1.mmap使用时必须实现指定好内存映射的大小,因此mmap并不适合变长文件;
2.因为mmap在文件更新后会通过OS自动将脏页面回写到disk中,所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快;
3.因为mmap必须要在内存中找到一块连续的地址块,如果在32-bits 的操作系统上的话,虚拟内存总大小也就2Gb左右(32位操作系统的地址空间最大为4Gb,除去1Gb系统,用户能使用的内存最多为3Gb左右(windows内核比较大,一般用户只剩下2Gb可用)。),此时就很难对4Gb大小的文件完全进行mmap,所以对于超大文件来讲,mmap并不适合。
sendfile
如果只是传输数据,并不对数据进行任何处理,譬如将服务器存储的静态文件(html、js)发送客户端用于浏览器渲染,在这种场景下,如果依然进行这么多数据拷贝和上下文切换,简直就是丧心病狂的有木有!!!所以我们就可以通过sendfile的方式,只做文件传输,而不通过用户态进行干预。
看图:
此时我们发现,数据拷贝变成了3次,上下文切换减少到2次。
虽然这个时候已经优化了不少,但是我们还有一个问题,为什么内核要拷贝两次呢???why???(page cache -> scket cache),能不能省略这个步骤呢?答案是当然可以啦!
sendfile + DMA scatter / Gather
DMA gather 是Linux2.4引入的功能,他可以读page cache 中的数据描述信息(内存地址和偏移量)记录到 scket cache 中,由DMA根据这些将数据从读缓存区copy到网卡,相比之前减少了一次CPU拷贝的过程。
看图:
direct I/O
之前的mmap可以让用户态和内核态共用一个内存空间来减少拷贝,其实还有一种方式,就是硬件数据不经过内核态的空间,直接到用户态的内存中,这种方式就是 Direct I/O。换句话说, Direct I/O不会经过内核态,而是用户态和设备的直接交互,用户态的写入就是直接写入到磁盘,不会再经过操作系统刷盘处理。
这样确实拷贝次数减少,读取速度会变快,但是因为操作系统不再负责缓存之类的管理,这就必须交由程序自己去做,譬如,MySQL就是自己通过 Direct I/O完成的,同时Mysql也有一套自己的缓存系统。
同时,虽然 Direct I/O可以直接将文件写入到磁盘,但是文件相关的元信息还是要通过fsync缓存到内核空间中。