从读写IO说起
用户进程向 CPU 发起 read 系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。
CPU 在接收到指令以后对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区。
数据准备完成以后,磁盘向 CPU 发起 I/O 中断。
CPU 收到 I/O 中断以后将磁盘缓冲区中的数据拷贝到内核缓冲区,然后再从内核缓冲区拷贝到用户缓冲区。
用户进程由内核态切换回用户态,解除阻塞状态,然后等待 CPU 的下一个执行时间钟。
内存映射
我们通常说的内存容量指的就是物理内存,即主存。能直接访问主存的只有内核,那么当进程到访问物理内存的时候怎么办呢?Linux给每个进程提供了一个独立的虚拟地址空间,并且这个地址空间是连续的,这样进程就可以很方便的访问内存,更准确的说是访问虚拟内存。
既然每个进程都有这么大的地址空间那么所有的虚拟内存加起来就要远远超过实际的物理内存所以并不是所有的虚拟内存都会分配物理内存只有实际使用的虚拟内存才会分配(Java堆内存溢出?内存锁定?)并且分配后物理内存是通过内存映射来管理的。
理解一:内存映射即将虚拟内存地址映射到物理内存,为了完成映射内核为每个进程维护了一张页表记录映射关系。而当进程访问虚拟地址在页表中查不到时系统会产生一个缺页异常,进入内核空间分配物理内存 -- 更新页表 -- 返回用户空间 -- 恢复进程。
在传统的I/O中 读写操作都是通过系统调用的标准I/O函数read()/ write()实现的。在java调用read方法以后首先将当前的用户态(?)转为内核态然后由操作系统的内核代码拷贝到内核缓冲区,最后再从内核拷贝到用户态的缓存,这样完成一次操作。
理解二:内存映射和标准的io操作一样需要从磁盘文件中获取数据,但它并不需要从内核态的缓冲区拷贝到用户态的缓冲区,而是直接将进程中一部分私有地址空间区域与磁盘文件建立起映射关系,通过缺页,把磁盘文件的内容直接拷贝到进程的缓冲区。相对标准io操作,这里有两个最主要的优势,第一,性能上,由于直接内存映射少一次内存拷贝,因此会比标准io操作要快,文件越大,优势越明显;第二,直接内存映射可以加载普通方式无法访问的大文件,这里的大文件指,例如1G的文件,如果是标准io操作,我们需要将1G的文件直接加载进内存,才可以访问,明显这是不可能的,但通过虚拟内存的映射方式以及操作系统层面上发起的页面请求,便可将所需数据加载到程序内存中
零拷贝
1. 用户态直接I/O
2. 较少数据拷贝次数
使用mmap+write代替原来的 read+write 方式,减少一次CPU拷贝操作。这里的mmap的目的是将内核缓冲区的地址与用户空间的缓冲区进行映射,从而实现了内核到应用之间的共享。
基于 mmap + write 系统调用的零拷贝方式,整个拷贝过程会发生 4 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝,用户程序读写数据的流程如下:
用户进程通过 mmap() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
将用户进程的内核空间的读缓冲区(read buffer)与用户空间的缓存区(user buffer)进行内存地址映射。
CPU利用DMA控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
上下文从内核态(kernel space)切换回用户态(user space),mmap 系统调用执行返回。
用户进程通过 write() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
CPU将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
CPU利用DMA控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
上下文从内核态(kernel space)切换回用户态(user space),write 系统调用执行返回
sendfile系统调用,数据可以直接在内核空间内部进行I/O传输从而省去在用户空间和内核空间的来回拷贝
基于 sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝,用户程序读写数据的流程如下:
用户进程通过 sendfile() 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
CPU 将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回
3. 写时复制技术
Java零拷贝实现
在Java NIO中的通道Channel就相当于操作系统的内核空间缓冲区,而缓冲区对应的相当于操作系统的用户空间的用户缓冲区。
MappedByteBuffer 是NIO是基于内存映射mmap这种零拷贝方式提供的一种实现。FileChannel定义了一个map()方法它可以把文件从position开始的size大小的区域映射为内存映射文件
DirectByteBuffer 自身是Java堆内的它背后真正承载数据的buffer是在堆外。
RMQ的零拷贝实践