Linux 零拷贝技术-mmap 和 sendfile
零拷贝
- 零拷贝是网络编程的关键, 很多性能优化都离不开。像kafka、nginx、tomcat中都使用了零拷贝技术。
- 一般来说从数据角度分析: 在零拷贝机制中, 整个数据在内存中只有一份数据;非零拷贝机制中 , 内核缓冲区 , 用户缓冲区 , Socket 缓冲区 , 各有一份数据;
- 零拷贝是什么:指的是没有CPU 拷贝, 都是 DMA ( 直接内存访问 ) 拷贝;
- 零拷贝性能优势: 没有复制数据带来的内存开销 , 没有 CPU 拷贝 , 直接节省了大量 CPU 计算资源 ;
一般场景
1、普通io读取本地文件,网络发送:
File file = new File("index.html");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int)file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
2、具体步骤:
下面说说他们的步骤:
read调用有两次内存拷贝
(0)发生一次用户态到内核态的上下文切换。
(1)第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取index.html文件,并将数据放入到内核缓冲区。
(2)发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。
write调用有两次内存拷贝
(3)发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。
(4)第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
(5)write 方法返回,再次从内核态切换到用户态。
从本地磁盘读取数据,通过网络发送出去,用户态和内核态之间需要发生4次切换;数据从磁盘取出来之后,一共要经过4次拷贝。
注意:读写文件100M文件,不是分配100M内存,而是分配64k内存循环读写,否则直接分配100M,其他应用就容易被挂起。
send是写内存缓冲区成功,不是写到网络成功,网络可能一直发送不完。所以下次send的时候可能失败,因为buffer满,没发送出去.
使用mmap(内存映射)实现零拷贝
把磁盘文件映射到内存中,然后把映射到内存的数据通过Socket发送出去。
buf = mmap(file, len);
write(sockfd, buf, len);
mmap(内存映射): 直接将磁盘文件数基于DMA引擎拷贝据映射到内核缓冲区,同时用户缓冲区是跟内核缓冲区共享一块映射数据,建立映射后,不需要从内核缓冲区拷贝到用户缓冲区。可减少一次拷贝。总共是4次切换,3次拷贝。
使用 sendFile 实现零拷贝
Linux提供sendfile技术。Kafka中,transferFrom和transferTo方法。
只要2次切换,2次拷贝。
- 用户态切换到内核态,DMA 引擎从文件拷贝到内核缓冲区,同时从内环缓冲区拷贝一些offset和length数据到socket缓冲区
- 从内核态切换到用户态,从内核缓冲区直接把数据拷贝到网络协议引擎里去,同时从Socket缓冲区拷贝一些offset和length信息到网络协议引擎里去
offset和length量几乎可以忽略。
零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
各种框架使用零拷贝案例
1、nginx作为静态服务器时,采用sendfile()方式可以减少上下文切换和系统拷贝从而提高系统性能;但是nginx作为反向代理服务器时,就没有效果了,因为sendfile()的作用是发送文件,也就是接收数据的一段是文件句柄,发送数据的那一端是socket。而在做反向代理服务器的时候,两端都是socket,此时无法使用sendfile(),也就不存在性能提升这一说了。
linux 使用sendfile(https://linuxcpp.0voice.com/?id=23881)
#include <sys/sendfile.h>
#include <fcntl.h>
#include <unistd.h>
#include <netinet/in.h>
#define BUF_SIZE 4096
int main(int argc, char *argv[]) {
int fd, sock_fd;
off_t offset = 0;
struct sockaddr_in serv_addr;
// 创建TCP socket
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
return 1;
}
// 设置目标地址和端口号
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8888);
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("inet_pton");
return 1;
}
// 连接到服务器
if (connect(sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("connect");
return 1;
}
// 打开本地文件并读取内容
if ((fd = open("example.txt", O_RDONLY)) == -1) {
perror("open");
return 1;
}
// 使用sendfile函数将文件内容发送到socket
ssize_t sent_bytes;
char buf[BUF_SIZE];
while ((sent_bytes = sendfile(sock_fd, fd, &offset, BUF_SIZE)) > 0) {
printf("%ld bytes sent\n", sent_bytes);
}
close(fd);
close(sock_fd);
return 0;
}
小结
mmap与sendFile区别
mmap 用于文件共享,很少用于socket操作,sendfile用于发送文件.
mmap 适合小数据量读写,sendFile 适合大文件传输。
mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 2 次上下文切换,最少 2 次数据拷贝。
sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
mmap和共享内存的区别:
mmap是共享一个文件,共享内存是共享一段内存。mmap还可以写回到file.
mmap缺点:
mmap 每次读入都是1页即4k,所以少于4k会造成大量内存碎片. 但是通过read,write也是这样的。
mmap适用场景,是取代read,write 文件.
使用mmap+write方式
优点:即使频繁调用,使用小文件块传输,效率也很高
缺点:不能很好的利用DMA方式,会比sendfile多消耗CPU资源,内存安全性控制复杂,需要避免JVM Crash问题
使用sendfile方式
优点:可以利用DMA方式,消耗CPU资源少,大块文件传输效率高,无内存安全问题
缺点:小块文件效率低于mmap方式,只能是BIO方式传输,不能使用NIO
rocketMQ 在消费消息时,使用了 mmap,因为小块数据传输比sendFile好。kafka 使用了 sendFile。
资料:
https://www.cnblogs.com/hongdada/p/16926179.html
https://www.bilibili.com/video/BV1514y1h7Ve
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2020-09-24 C++11 随机数 random