零拷贝
● 零拷贝不是拷贝0次,而是减少不必要的拷贝次数,其目的就是为了提高IO效率
为什么要有零拷贝
● 在传统Io上,压根儿就不需要第二次和第三次copy操作,数据可以直接从读缓冲区传输到套接字缓存区,不需要在拷贝到用户空间去
传统的IO过程
● 将磁盘文件,读取到操作系统到内核缓冲区
● 将内核缓冲区的数据,copy到application用户程序的buffer缓冲区
● 将application应用程序buffer区里的数据,copy复制到socket网络发送缓冲区(这个是属于操作系统内核的缓冲区)
● 将socket的buffer区里的数据copy到网卡,由网卡进行网络传输
也就是说,传统Io进行网络传送要经过四次数据copy,实际上Io读写还需要进行Io中断,在进行与IO外围设备数据传输的时候,需要CPU响应中断来进行上下文切换,尽管在最新的系统中是引入了一个DMA技术来接管CPU的中断请求,但是在这四次拷贝中,有些是不必要的
DMA技术
● 本质上就是在主板上放一块独立的芯片,在进行内存与IO设备的数据传输的时候,我们不在通过CPU来控制数据传输,而直接通过DMA控制器来,可以认为这个芯片就是一个协助处理器
Linux的虚拟内存管理是基于mmap来拿实现,共享内存也可以通过mmap映射普通文件来实现,数据不需要来回复制,是最快的一种进程间通信的机制;
● 传统文件访问,两个进程同时读一个文件的同一页的情形,系统要将该页从磁盘读到高速缓冲区中,每个进程再执行一个内存期内的复制操作将数据从高速缓冲区读到自己的地址空间
● mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享
● Linux通过内存映像机制来提供用户程序对内存直接访问的能力。内存映像的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去。也就是说,用户空间和内核空间共享一块相同的内存。这样做的直观效果显而易见:内核在这块地址内存储变更的任何数据,用户可以立即发现和使用,根本无须数据拷贝。举个例子理解一下,使用mmap方式获取磁盘上的文件信息,只需要将磁盘上的数据拷贝至那块共享内存中去,用户进程可以直接获取到信息,而相对于传统的write/read IO系统调用, 必须先把数据从磁盘拷贝至到内核缓冲区中(页缓冲),然后再把数据拷贝至用户进程中。两者相比,mmap会少一次拷贝数据,这样带来的性能提升是巨大的
mmap的两种映射方式
● 基于文件的映射,适用于任何进程之间, 此时,需要打开或创建一个文件,然后再调用mmap()
● 匿名映射,一个匿名映射没有对应的文件,可以把它看成是一个内容总是被初始化为0的虚拟文件映射,比如在具有血缘关系的进程之间,如父子进程之间, 当一个进程调用mmap().之后又调用了fork(), 之后子进程会继承(拷贝)父进程映射后的空间,同时也继承了mmap()的返回地址,通过修改数据共享内存里的数据, 父子进程够可以感知到数据的变化,这样一来,父子进程就可以通过这块共享内存来实现进程间通信
mmap的优缺点
● 优点,可以很大的提升IO效率,省去了用户空间到内核空间复制的开销
● 缺点,mmap有时也不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘
sendfile零拷贝
● 简单理解就是,硬盘中的文件或者数据读取到操作系统内核缓冲区后,直接扔给网卡(socket buffer),发送网络数据;磁盘数据通过DMA技术拷贝到内核态buffer后,直接通过DMA拷贝到socket buffer,无需CPU拷贝(内核buffer到用户buffer,用户buffer再到socket buffer);除了减少数据拷贝外,因为整个读文件-网络发送只需要调用一个sendfile调用完成,整个过程只有两次上下文切换,因此也大大的提高了不少性能
mmap内存映射和sendfile零拷贝的对比
● 都是Linux内核提供、实现零拷贝的API
● sendfile是将读到内核空间的数据直接转到socket buffer,进行网络发送
● mmap(只支持文件)将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上
● Java-NIO中,对mmap和sendfile都有类似的实现
1、Java NIO,提供了一个 MappedByteBuffer 类可以用来实现内存映射,MappedByteBuffer只能通过调用FileChannel的map()取得,再没有其他方式
2、Java NIO对sendfile的支持就是FileChannel.transferTo()/transferFrom()
ExcelWriter excelWriter = EasyExcel.write(filePath, User.class).build();
// 设置FlushRows参数,表示写入2000条数据后执行一次内存刷盘操作
excelWriter.setFlushRows(2000);
for (int i = 0; i < userList.size(); i++) {
User user = userList.get(i);
List<String> data = Arrays.asList(user.getName(), user.getSex(), user.getAge());
excelWriter.write(data, new Sheet(1, i));
}
// 最后一定要调用finish方法,将数据刷盘并关闭流
excelWriter.finish();