还在为慢速数据传输苦恼?Linux 零复制技能来帮你!
前语
程序员的终极寻求是什么?当体系流量大增,用户体会却丝滑仍旧?没错!但是,在很多文件传输、数据传递的场景中,传统的“数据转移”却拖慢了功能。为了处理这一痛点,Linux 推出了 零仿制 技能,让数据高效传输简直无需 CPU 操心。今日,我就用最浅显的言语解说零仿制的作业原理、常见完结办法和实践运用,彻底帮你搞懂这项技能!
1、传统仿制:数据转移的“旧时代”
为了了解零仿制,咱们先看看传统数据传输的作业办法。幻想一下,咱们需求把一个大文件从硬盘读取后发送到网络上。这听起来很简略,但实践上,传统的数据传输触及多个进程并占用很多 CPU 资源。
1.1 一个典型的文件传输进程(没有 DMA 技能):
假定咱们要将一个大文件从硬盘读取后发送到网络。以下是传统仿制办法的具体进程:
- 读取数据到内核缓冲区:运用
read()
体系调用,数据从硬盘读取到内核缓冲区。此刻,CPU 需求协谐和履行相关指令来完结这一步。 - 仿制数据到用户缓冲区:数据从内核缓冲区被仿制到用户空间的缓冲区。这一步由
read()
调用触发,CPU 彻底担任这次数据仿制。 - 写入数据到内核缓冲区:经过
write()
体系调用,数据从用户缓冲区被再次仿制回内核缓冲区。CPU 再次介入并担任数据仿制。 - 传输数据到网卡:终究,内核缓冲区的数据被传输到网卡,发送到网络。假如没有 DMA 技能,CPU 需求仿制数据至网卡。
1.2 来看个图,更直观念:
1.3 数据传输的“四次仿制”
在这个进程中,数据在体系中阅历了四次仿制:
- 硬盘 -> 内核缓冲区(CPU 参加,担任数据读取和传输)
- 内核缓冲区 -> 用户缓冲区(
read()
调用触发,CPU 担任仿制) - 用户缓冲区 -> 内核缓冲区(
write()
调用触发,CPU 担任仿制) - 内核缓冲区 -> 网卡(终究发送数据,CPU 参加传输)
1.4 功能瓶颈剖析
这种传统仿制办法的问题清楚明了:
- CPU 资源占用高:每次
read()
和write()
调用都需求 CPU 进行屡次数据仿制,严峻占用 CPU 资源,影响其他使命的履行。 - 内存占用:当数据量较大时,内存运用量显着添加,或许导致体系功能下降。
- 上下文切换开支:每次
read()
和write()
调用触及用户态和内核态的切换,加剧了 CPU 的担负。
这些问题在处理大文件或高频率传输时尤为显着,CPU 被逼充任“转移工”,功能因而遭到严峻约束。那么, 有没有一种办法能够削减 CPU 的“转移”作业?此刻,DMA(Direct Memory Access,直接内存拜访)技能上台了。
2、DMA:零仿制的序幕
DMA(Direct Memory Access,直接内存拜访) 是一种让数据在硬盘和内存之间直接传输的技能,不需求 CPU 逐字节参加。简略来说,DMA 是 CPU 的“好辅佐”,削减了它的作业量。
2.1 DMA 怎样帮 CPU?
在传统的数据传输中,CPU 需求亲身把数据从硬盘搬到内存,再送到网络,这很耗费 CPU 资源。而 DMA 的呈现让 CPU 能够少干活:
- 硬盘到内核缓冲区:由 DMA 完结,CPU 只需求下指令,DMA 就主动将数据仿制至内核缓冲区。
- 内核缓冲区到网卡:DMA 也能处理这部分,把数据直接送到网卡,CPU 只需监督全体流程。
有了 DMA,CPU 只需求说一句:“嘿,DMA,把数据从硬盘搬到内存去!” 然后 DMA 操控器就会接过这活,主动把数据从硬盘传到内核缓冲区,CPU 只需求在旁边监督一下。
2.2 有了 DMA , 再来看看数据传输的进程:
为了更好地了解 DMA 在整个数据转移中的人物,咱们用图来阐明:
阐明:
- DMA 担任硬盘到内核缓冲区和内核到网卡的传输。
- CPU 仍需处理内核和用户缓冲区之间的数据传输。
2.3 哪些进程仍需 CPU 参加?
尽管 DMA 能帮 CPU 分管一些使命,但它并不能全权署理一切数据仿制作业。CPU 仍是得担任以下两件事:
- 内核缓冲区到用户缓冲区:数据需求被 CPU 仿制到用户空间供程序运用。
- 用户缓冲区回到内核缓冲区:程序处理完数据后,CPU 还得把数据拷回内核,预备进行后续传输。
就像请了一个辅佐,但有些详尽活儿还得自己干。所以,在高并发或大文件传输时,CPU 仍旧会由于这些仿制使命感到压力。
2.4 总结一下
总结来说,DMA 的确减轻了 CPU 在数据传输中的担负,让数据从硬盘传输到内核缓冲区和内核缓冲区到网卡时简直无需 CPU 的参加。但是,DMA 无法彻底处理数据在内核和用户空间之间的仿制问题。CPU 仍然需求进行两次数据转移,特别是在高并发和大文件传输场景下,这个约束变得尤为杰出。
3、零仿制:让数据“直达”
因而,为了进一步削减 CPU 的参加,进步传输功率,Linux 推出了 零仿制 技能。这项技能的中心方针是:让数据在内核空间内直接流通,防止在用户空间的冗余仿制,然后最大极限削减 CPU 的内存仿制操作,进步体系功能。
接下来,咱们来具体看看 Linux 中的几种首要零仿制完结办法:
留意:Linux 中零仿制技能的完结需求硬件支撑 DMA。
3.1 sendfile:最早的零仿制办法
sendfile
是最早在 Linux 中引进的零仿制办法,专为文件传输规划。
3.2 sendfile 的作业流程
- DMA(直接内存拜访)直接将文件数据加载到内核缓冲区。
- 数据从内核缓冲区直接进入网络协议栈中的 socket 内核缓冲区。
- 数据经过网络协议栈处理后,经过网卡直接发往网络。
经过 sendfile
,整个传输进程 CPU 只需求一次数据仿制,削减了 CPU 的运用。
3.3 简略图解:
sendfile
图解阐明:
- 从硬盘读取数据:文件数据经过 DMA 从硬盘读取,直接加载到内核缓冲区,这个进程不需求 CPU 的参加。
- 仿制数据至网络协议栈的 socket 缓冲区:数据不进入用户空间,而是从内核缓冲区直接进入网络协议栈中的 socket 缓冲区,在这儿经过必要的协议处理(如 TCP/IP 封装)。
- 数据经过网卡发送:数据终究经过网卡直接发往网络。
3.4 sendfile 接口阐明
sendfile
函数界说如下:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd
:方针文件描绘符,一般是 socket 描绘符,用于网络发送。in_fd
:源文件描绘符,一般是从硬盘读取的文件。offset
:偏移量指针,用于指定从文件的哪个方位开端读取。假如为NULL
,则从当时偏移方位开端读取。count
:要传输的字节数。
回来值是实践传输的字节数,犯错时回来 -1
,并设置 errno
来指示过错原因。
3.5 简略代码示例
#include <sys/sendfile.h>
int main() {
int input_fd = open("input.txt", O_RDONLY);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
sendfile(client_fd, input_fd, NULL, 1024);
close(input_fd);
close(client_fd);
close(server_fd);
return 0;
}
这个比方展现了怎样运用 sendfile
将本地文件发送到一个经过网络连接的客户端。只需求调用 sendfile
,数据就能从 input_fd
直接传输到 output_fd
。
3.6 适用场景
sendfile
首要用于将文件数据直接传输到网络,十分合适需求高效传输大文件的状况,例如文件服务器、流媒体传输、备份体系等。
在传统的数据传输办法中,数据需求经过多个进程:
- 首要,数据从硬盘读取到内核空间。
- 然后,数据从内核空间仿制到用户空间。
- 最终,数据从用户空间再仿制回内核,送到网卡发出去。
总结来说,sendfile 能够让数据传输愈加高效,削减 CPU 的干涉,特别合适简略的大文件传输场景。但是,假如遇到更杂乱的传输需求,比方要在多个不同类型的文件描绘符之间移动数据,splice 则供给了一种愈加灵敏的办法。接下来咱们来看看 splice 是怎样完结这一点的。
4. splice : 管道式零仿制
splice
是 Linux 中另一种完结零仿制的数据传输体系调用,专为在不同类型的文件描绘符之间高效地移动数据而规划,适用于在内核中直接传输数据,削减不必要的仿制。
4.1 splice 的作业流程
- 从文件读取数据:运用
splice
体系调用将数据从输入文件描绘符(例如硬盘文件)读取,数据直接经过 DMA(直接内存拜访)进入内核缓冲区。 - 传输到网络 socket:随后,
splice
持续将内核缓冲区中的数据直接传输到方针网络 socket 的文件描绘符中。
整个进程在内核空间内完结,防止了数据从内核空间到用户空间的往复仿制,大大削减了 CPU 的参加,进步了体系功能。
4.2 简略图解:
和 sendfile 图解相似,仅仅接口不一样。
splice
图解阐明:
数据经过 splice
从文件描绘符传输到网络 socket。数据首要经过 DMA 进入内核缓冲区,然后直接传输到网络 socket,整个进程防止了用户空间的介入,明显削减了 CPU 的仿制作业。
4.3 splice 接口阐明
splice
函数的界说如下:
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
fd_in
:源文件描绘符,数据从这儿读取。off_in
:指向源偏移量的指针,假如为NULL
,则运用当时偏移量。fd_out
:方针文件描绘符,数据将被写入这儿。off_out
:指向方针偏移量的指针,假如为NULL
,则运用当时偏移量。len
:要传输的字节数。flags
:操控行为的标志,例如SPLICE_F_MOVE
、SPLICE_F_MORE
等。
回来值是实践传输的字节数,犯错时回来 -1
,并设置 errno
来指示过错原因。
4.4 简略代码示例
int main() {
int input_fd = open("input.txt", O_RDONLY);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
splice(input_fd, NULL, client_fd, NULL, 1024, SPLICE_F_MORE);
close(input_fd);
close(client_fd);
close(server_fd);
return 0;
}
这个比方展现了怎样运用 splice
将本地文件直接发送到网络 socket,以完结高效的数据传输。
4.5 适用场景
splice
适用于在文件描绘符之间进行高效、直接的数据传输,例如从文件到网络 socket 的传输,或在文件、管道和 socket 之间传递数据。在这种状况下,数据在内核空间内完结传输,无需进入用户空间,然后明显削减仿制次数和 CPU 的参加。别的 splice 特别合适需求灵敏数据活动和削减 CPU 担负的场景,例如日志处理、实时数据流处理等。
4.6 sendfile 与 splice 的差异
尽管 sendfile 和 splice 都是 Linux 供给的零仿制技能,用于高效地在内核空间传输数据,但它们在运用场景和功能上存在一些明显差异:
数据活动办法:
- sendfile:直接将文件中的数据从内核缓冲区传输到 socket 缓冲区,合适文件到网络的传输。合适需求简略高效的文件到网络的传输场景。
- splice:更灵敏,能够在任意文件描绘符之间进行数据传输,包含文件、管道、socket 等。因而,splice 能够在文件、管道和 socket 之间完结更杂乱的数据流通。
适用场景:
- sendfile:首要用于文件到网络的传输,十分合适文件服务器、流媒体等需求高效传输文件的场景。
- splice:更合适杂乱的数据活动场景,例如在文件、管道和网络之间需求多步传输或灵敏操控数据流向的状况。
灵敏性:
- sendfile:用于直接、高效地将文件发送到网络,尽管操作单一,但功能十分高效。
- splice:能够结合管道运用,完结更杂乱的数据流向操控,例如先经过管道对数据进行处理,再发送到方针方位。
5. mmap + write:映射式零仿制
除了以上两种办法,mmap
+ write
也是一种常见的零仿制完结办法。这种办法首要是经过内存映射来削减数据仿制的进程。
5.1 mmap + write 的作业流程
- 运用
mmap
体系调用将文件映射到进程的虚拟地址空间中,这样数据就能够直接在内核空间和用户空间同享,而不需求额定的仿制操作。 - 运用
write
体系调用将映射的内存区域直接写入到方针文件描绘符中(比方网络 socket),完结数据传输。
这种办法削减了数据仿制,进步了功率,合适需求灵敏操作数据后再发送的场景。经过这种办法,数据不需求显式地从内核空间仿制到用户空间,而是经过映射的办法同享,然后削减了不必要的仿制。
5.2 简略图解:
mmap + write
图解阐明:
- 运用
mmap
将文件数据映射到进程的虚拟地址空间,防止显式的数据仿制。 - 经过
write
直接将映射的内存区域数据发送到方针文件描绘符(如网络 socket)。
5.3 mmap 接口阐明
mmap
函数的界说如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr
:指定映射内存的开端地址,一般为NULL
由体系决议。length
:要映射的内存区域的巨细。prot
:映射区域的维护标志,例如PROT_READ
、PROT_WRITE
。flags
:影响映射的特点,例如MAP_SHARED
、MAP_PRIVATE
。fd
:文件描绘符,指向需求映射的文件。offset
:文件中的偏移量,表明从文件的哪个方位开端映射。
回来值为映射内存区域的指针,犯错时回来 MAP_FAILED
,并设置 errno
。
5.4 简略代码示例
int main() {
int input_fd = open("input.txt", O_RDONLY);
struct stat file_stat;
fstat(input_fd, &file_stat);
char *mapped = mmap(NULL, file_stat.st_size, PROT_READ, MAP_PRIVATE, input_fd, 0);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
write(client_fd, mapped, file_stat.st_size);
munmap(mapped, file_stat.st_size);
close(input_fd);
close(client_fd);
close(server_fd);
return 0;
}
这个比方展现了怎样运用 mmap
将文件映射到内存,然后经过 write
将数据发送到网络连接的客户端。
5.5 适用场景
mmap
+ write
适用于需求对文件数据进行灵敏操作的场景,例如需求在发送数据前进行修正或部分处理。与 sendfile
比较,mmap
+ write
供给了更大的灵敏性,由于它答应在用户态拜访数据内容,这关于需求对文件进行预处理的运用场景十分有用,例如紧缩、加密或许数据转化等。
但是,这种办法也带来了更多的开支,由于数据需求在用户态和内核态之间进行交互,这会添加体系调用的本钱。因而,mmap
+ write
更合适那些需求在数据传输前进行一些自界说处理的状况,而不太合适朴实的大文件高效传输。
6. tee:数据仿制的零仿制办法
tee
是 Linux 中的一种零仿制办法,它能够把一个管道中的数据仿制到另一个管道,一同保存原管道中的数据。这意味着数据能够一同被发送到多个方针,而不影响本来的数据流,十分合适日志记载和实时数据剖析等需求把相同的数据送往不同当地的场景。
6.1 tee 的作业流程
- 数据仿制到另一个管道:
tee
体系调用能够将一个管道中的数据仿制到另一个管道,而不改动原有的数据。这意味着数据能够在内核空间中被一同用于不同的意图,而无需经过用户空间的仿制。
6.2 tee 接口阐明
tee
函数的界说如下:
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
fd_in
:源管道文件描绘符,数据从这儿读取。fd_out
:方针管道文件描绘符,数据将被写入这儿。len
:要仿制的字节数。flags
:操控行为的标志,例如SPLICE_F_NONBLOCK
等。
回来值是实践仿制的字节数,犯错时回来 -1
,并设置 errno
来指示过错原因。
6.3 简略代码示例
int main() {
int pipe_fd[2];
pipe(pipe_fd);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
listen(server_fd, 3);
int client_fd = accept(server_fd, NULL, NULL);
// 运用 tee 仿制数据
tee(pipe_fd[0], pipe_fd[1], 1024, 0);
splice(pipe_fd[0], NULL, client_fd, NULL, 1024, SPLICE_F_MORE);
close(pipe_fd[0]);
close(pipe_fd[1]);
close(client_fd);
close(server_fd);
return 0;
}
这个比方展现了怎样运用 tee
将管道中的数据仿制,并经过 splice
将数据发送到网络 socket,然后完结高效的数据传输和仿制。
6.4 适用场景
tee
十分合适需求将数据一同发送到多个方针的场景,比方实时数据处理、日志记载等。 经过 tee
,能够在内核空间内完结多方针数据仿制,进步体系功能,削减 CPU 担负。
总结比照:
下面我将 Linux 的几种零仿制办法做了总结,便利我们比照学习:
办法 | 描绘 | 零仿制类型 | CPU 参加度 | 适用场景 |
---|---|---|---|---|
sendfile | 直接将文件数据发送到套接字,无需仿制到用户空间。 | 彻底零仿制 | 很少,数据直接传输。 | 文件服务器、视频流传输等大文件场景。 |
splice | 在内核空间内高效地在文件描绘符之间传输数据。 | 彻底零仿制 | 很少,彻底在内核内。 | 文件、管道与 socket 之间的杂乱传输场景。 |
mmap + write | 将文件映射到内存并运用 write 发送数据,灵敏处理数据 | 部分零仿制 | 中等,需求映射和写入。 | 数据需求处理或修正的场景,如紧缩加密。 |
tee | 将管道中的数据仿制到另一个管道,无需耗费原始数据。 | 彻底零仿制 | 很少,数据仿制在内核。 | 日志处理、实时数据监控等多方针场景。 |
最终:
期望这篇文章让你对 Linux 的零仿制技能有了更全面、更明晰的了解!这些技能看起来或许有些杂乱,但一旦把握后,你会发现它们十分简略, 并且在实践项目中十分有用。
假如觉得这篇文章对你有协助,记住给我点个在看和赞 👍,并共享给有需求的小伙伴吧!也欢迎我们来重视我大众号 「跟着小康学编程」
重视我能学到什么?
-
这儿共享 Linux C、C++、Go 开发、计算机基础知识 和 编程面试干货等,内容浅显易懂,让技能学习变得轻松风趣。
-
不管您是备战面试,仍是想进步编程技能,这儿都致力于供给有用、风趣、有深度的技能共享。快来重视,让咱们一同生长!
怎样重视我的大众号?
十分简略!扫描下方二维码即可一键重视。
此外,小康最近创建了一个技能沟通群,专门用来评论技能问题和回答读者的疑问。在阅读文章时,假如有不了解的知识点,欢迎我们参加沟通群发问。我会极力为我们回答。等待与我们共同进步!