典型回答 要想理解零拷贝,首先要了解操作系统的IO流程,因为有内核态和用户态的区别,为了保证安全性和缓存,普通的读写流程如下:
(对于Java程序,还会多了一个堆外内存和堆内存之间的copy)
程序发起read()系统调用,要求从磁盘读取文件数据:
上下文切换:由用户态进入内核态 DMA拷贝:通过DMA技术将磁盘中的数据copy到内核缓冲区中 CPU拷贝:当DMA完成工作后,会发起一个中断通知CPU数据拷贝完成,然后CPU再将内核缓冲区中的数据copy到应用程序缓冲区中 上下文切换:read()调用返回,系统从内核态切换会用户态。内核唤醒对应线程,同时将用户态的数据返回给该线程空间 程序发起write()系统调用,要求将应用程序缓冲区里的数据通过socket发送出去:
上下文切换:从用户态切换到内核态。 CPU 拷贝:CPU将数据从应用程序缓冲区拷贝到内核的socket缓冲区(Socket Buffer)。 DMA 拷贝:DMA引擎将数据从socket缓冲区拷贝到网卡(NIC)缓冲区,准备进行网络传输。 上下文切换:write()调用返回,系统从内核态切换回用户态。 在这个过程中,如果不考虑用户态的内存拷贝和物理设备到驱动的数据拷贝,我们会发现,这其中会涉及4次数据拷贝。同时也会涉及到4次进程上下文的切换。所谓的零拷贝,作用就是通过各种方式,在特殊情况下,减少数据拷贝的次数/减少CPU参与数据拷贝的次数。
常见的零拷贝方式有mmap,sendfile,dma,directI/O等。
扩展知识 DMA(Direct Memory Access,直接内存访问) 正常的IO流程中,不管是物理设备之间的数据拷贝,如磁盘到内存,还是内存之间的数据拷贝,如用户态到内核态,都是需要CPU参与的,如下所示
如果是比较大的文件,这样无意义的copy显然会极大的浪费CPU的效率,所以就诞生了DMA,Direct Memory Access你翻译一下也能知道,就是让硬件设备能够直接与主内存进行数据读写。
DMA控制器是一个专门的硬件单元,可以看作是CPU的一个“搬运工助理”。
CPU下达指令:CPU对DMA控制器进行配置,告诉它数据的源地址、目标地址和要传输的数据量。 DMA接管工作:配置完成后,DMA控制器会直接接管后续工作。它直接与设备(如网卡、磁盘控制器)和内存交互,开始搬运数据。(在此期间,CPU可以被解放出来去执行其他计算任务,只需等待DMA的工作完成。) DMA发出中断:当整个数据块传输完毕后,DMA控制器向CPU发送一个中断信号,通知它“任务已完成”。 所以,通过DMA,减少了CPU在I/O操作中的开销。
mmap 上文我们说到,正常的read+write,都会经历至少四次数据拷贝的,其中就包括内核态到用户态的拷贝,它的作用是为了安全和缓存。如果我们能保证安全性,是否就让用户态和内核态共享一个缓冲区呢?这就是mmap的作用。
mmap,全称是memory map,翻译过来就是内存映射,顾名思义,就是将内核态和用户态的内存映射到一起,避免来回拷贝,实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read、write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。其函数签名如下:
1 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 一般来讲,mmap会代替read方法,模型如下图所示:
mmap + write 方式:
mmap(): 系统调用将内核缓冲区直接映射到用户进程的虚拟地址空间。这使得用户进程的指针可以直接指向这片内核缓冲区。 用户进程像操作普通内存一样操作这片映射区域。 write(): 数据从映射的内存区域(即内核缓冲区) -> (CPU拷贝到) Socket缓冲区 -> (DMA拷贝到) 网卡。 采用mmap + write的方式,内存拷贝的次数会变为3次(减少了一次CPU拷贝),上下文切换则依旧是4次。
...