零拷贝技术

传统文件读写有三个步骤:把文件内容读入到内存中、修改内存中的内容、把内存的数据写入到文件中,page cache页缓存是读写文件时的中间层,内核使用页缓存文件的数据块关联起来,所以应用程序读写文件时,实际操作的是页缓存。

mmap

mmapmemory map内存映射,是一种内存映射文件的方法,即将一个文件其它对象映射到进程的地址空间,实现文件磁盘地址进程虚拟地址空间一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read、write等系统调用函数。内存映射服务的地址空间处在堆栈之间的空余部分。

读写文件都需要经过页缓存,mmap映射的正是文件的页缓存,而非磁盘中的文件本身,Linux内核并不会主动把mmap映射的页缓存同步到磁盘,而是需要用户主动触发,同步mmap映射的内存到磁盘有4个时机:

  • 调用msync函数主动进行数据同步主动
  • 调用munmap函数对文件进行解除映射关系时(主动
  • 进程退出时(被动
  • 系统关机时(被动

基于磁盘的读写单位是block,一般大小为4KB,而基于内存的读写单位是地址,即CPU进行一次磁盘读写操作涉及的数据量至少4KB,但进行一次内存操作涉及的数据量是基于地址的,64位操作系统通常是64bitmmap下进程可采用指针的方式进行读写操作。

mmap应用程序提供的内存访问接口内存地址连续的,但对应磁盘文件block可以不是地址连续的;mmap提供的内存空间虚拟内存,而不是物理内存,因此可分配远远大于物理内存大小的虚拟内存,如16G内存主机分配1000G的mmap内存空间;mmap负责映射文件逻辑上一段连续的数据映射为连续内存,物理上可以不连续存储,而这里的文件可以是磁盘文件以及设备;mmap由操作系统负责管理,对同一个文件地址的映射将被所有线程共享操作系统确保线程安全以及线程可见性

内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址mmap库函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// start: 映射区的开始地址
// length: 映射区的长度
// prot: 期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
// PROT_EXEC: 页内容可以被执行
// PROT_READ: 页内容可以被读取
// PROT_WRITE: 页可以被写入
// PROT_NONE: 页不可访问
// flags: 指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
// MAP_FIXED: 使用指定的映射起始地址,若由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。若指定起始地址不可用,操作将会失败,且起始地址必须落在页的边界上
// MAP_SHARED: 与其它所有映射该对象的进程共享映射空间,对共享区的写入相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新
// MAP_PRIVATE: 建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。该标志和MAP_SHARED是互斥的
// MAP_DENYWRITE: 该志被忽略
// MAP_EXECUTABLE: 同上
// MAP_NORESERVE: 不要为该射保留交换空间,当交换空间被保留,对映射区修改的可能会得到保证,当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号
// MAP_LOCKED: 锁定映射区的页面,从而防止页面被交换出内存
// MAP_GROWSDOWN: 用于堆栈,告诉内核VM系统,映射区可以向下扩展
// MAP_ANONYMOUS: 匿名映射,映射区不与任何文件关联
// MAP_ANON: MAP_ANONYMOUS的别称,不再被使用
// MAP_FILE: 兼容标志,被忽略
// MAP_32BIT: 将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略,当前该志只在x86-64平台上得到支持
// MAP_POPULATE: 为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞
// MAP_NONBLOCK: 仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口
// fd: 有效的文件描述符,若MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
// offset: 被映射对象内容的起点
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

mmap是一种零拷贝技术,,其 I/O 模型如下:

mmap也有其缺陷,在相关场景下的性能存在缺陷:

  • 由于mmap使用时必须实现指定好内存映射的大小,因此mmap并不适合变长文件
  • 更新文件的操作很多mmap避免两态拷贝的优势就被冲没了,最终还是落在了大量的脏页回写及由此引发的随机I/O,故在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快;
  • 读/写小文件,如16K以下的文件,mmap与通过read系统调用相比有着更高的开销与延迟;同时mmap的刷盘由系统全权控制,但在小数据量的情况下由应用本身手动控制更好
  • mmap受限于操作系统内存大小,如32bits操作系统上,虚拟内存总大小也就2GB,但由于mmap必须要在内存中找到一块连续的地址块,此时就无法对4GB大小的文件完全进行mmap,该情况下必须分多块分别进行mmap,但此时地址内存地址已经不再连续,使用mmap的意义大打折扣,且引入了额外的复杂性;

mmap的适用场景实际上非常受限,在如下场合下可选择使用mmap机制:

  • 多个线程只读的方式同时访问一个文件,因为mmap机制下多线程共享了同一物理内存空间,因此节约了内存。如多个进程可能依赖于同一个动态链接库,利用mmap可实现内存仅仅加载一份动态链接库,多个进程共享此动态链接库。
  • mmap非常适合用于进程间通信,对同一文件对应的mmap分配的物理内存天然多线程共享,并可依赖于操作系统的同步原语
  • mmap虽然比sendfile等机制多了一次CPU全程参与的内存拷贝,但用户空间与内核空间并不需要数据拷贝,因此在正确使用情况下并不比sendfile效率差;

mmap Java实现

Java中原生读写方式大概可以被分为三种:普通IOFileChannel文件通道mmap内存映射FileWriterFileReader存在于java.io包中属于普通IOFileChannel存在于java.nio包中,也是Java最常用的文件操作类,mmap则是由FileChannel调用map方法衍生出来的一种特殊读写文件的方式

sendfile