mmap+write
一种零拷贝方式是使用 mmap+write 代替原来的 read+write 方式,减少了 1 次 CPU 拷贝操作 。
mmap 是 Linux 提供的一种内存映射文件方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址,mmap+write 的伪代码如下:
- tmp_buf = mmap(file_fd, len);
- write(socket_fd, tmp_buf, len);
从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程 。
然而内核读缓冲区(read buffer)仍需将数据拷贝到内核写缓冲区(socket buffer),大致的流程如下图所示:
基于 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 系统调用执行返回 。
因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存 。
mmap 的拷贝虽然减少了 1 次拷贝,提升了效率,但也存在一些隐藏的问题 。
当 mmap 一个文件时,如果这个文件被另一个进程所截获,那么 write 系统调用会因为访问非法地址被 SIGBUS 信号终止,SIGBUS 默认会杀死进程并产生一个 coredump,服务器可能因此被终止 。
Sendfile
Sendfile 系统调用在 Linux 内核版本 2.1 中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程 。
Sendfile 系统调用的引入,不仅减少了 CPU 拷贝的次数,还减少了上下文切换的次数,它的伪代码如下:
- sendfile(socket_fd, file_fd, len);
与 mmap 内存映射方式不同的是,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 系统调用执行返回 。
Sendfile 存在的问题是用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程 。
Sendfile+DMA gather copy
Linux 2.4 版本的内核对 Sendfile 系统调用进行修改,为 DMA 拷贝引入了 gather 操作 。
它将内核空间(kernel space)的读缓冲区(read buffer)中对应的数据描述信息(内存地址、地址偏移量)记录到相应的网络缓冲区( socket buffer)中,由 DMA 根据内存地址、地址偏移量将数据批量地从读缓冲区(read buffer)拷贝到网卡设备中 。
这样就省去了内核空间中仅剩的 1 次 CPU 拷贝操作,Sendfile 的伪代码如下:
推荐阅读
- 淘宝从百万到千万级并发的14次服务端架构演进之路
- 几百万扔进水?买二手房千万避开这几类房源
- 分布式、高并发、多线程,这些概念还傻傻分不清吗?
- Java 并发编程:如何保证共享变量的原子性?
- 硬核!如何模拟 5w+ 的并发用户?
- 格鲁吉亚中国茶王刘峻周诞辰150周年之际获百万赔偿
- 安徽省科技厅强化科技支撑推进精准扶贫
- PHP导出百万条数据方法
- 重金寻人 , 你是我们要找的百万英雄吗 内含福利
- 带你深入了解高并发架构
