3

linux 零拷贝相关方法梳理和代码调试

 8 months ago
source link: https://www.zoucz.com/blog/2023/12/31/93b6dc00-a7c3-11ee-95cb-3556e1632a5e/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

零拷贝(Zero-Copy)是一种可以在计算机系统中,通过减少 CPU 拷贝数据次数、绕开内核进行直接 IO 等方式,提高系统 IO 性能的一种技术。我要说话

通过 DMA 减少 CPU 拷贝次数

减少拷贝次数的原理主要是通过减少数据拷贝的次数,减少CPU的使用,提高数据传输的效率。在传统的数据传输过程中,数据通常需要在用户空间和内核空间之间进行至少两次的拷贝,这就会消耗大量的CPU资源。而零拷贝技术则是通过一些特殊的技术手段,尽可能地减少数据的拷贝次数,从而提高数据传输的效率。 我要说话

DMA(Direct Memory Access)

直接内存访问是一种可以让某些硬件子系统(例如高速磁盘驱动器、图形卡)在不涉及CPU的情况下,直接访问内存的技术,从而实现零拷贝。 我要说话

直接使用 CPU 的 IO

普通的 IO 方式,CPU 参与了全部 IO 的工作,并存在数据从内核态到用户态拷贝的过程。 我要说话

image.png

我要说话

CPU + DMA 的 IO

目前支持 DMA 的硬件包括:网卡、声卡、显卡、磁盘控制器等。 我要说话

image.png

我要说话

加上 DMA 设备后,CPU 参与的环节有所减少,但是整个 IO 过程扔存在内核态到用户态的拷贝过程我要说话

image.png

我要说话

操作系统提供了一些方法,能进一步减少 CPU 参与的 IO 环节,让数据不再从内核缓冲区的内存页拷贝到用户缓冲区,减少数据拷贝的次数。 我要说话

概念和适用场景

image.png

我要说话

该函数可以将一个文件或者其它对象映射进内存,通过对这块内存的读写操作,程序可以实现对文件或者其它对象的读写操作。这样就避免了数据在用户空间和内核空间之间的拷贝,从而减少了一次从内核态到用户态的拷贝过程。 我要说话

mmap适用的场景: 我要说话

  • 文件I/O性能要求较高的场景:mmap将文件映射到内存,可以避免数据在用户空间和内核空间之间的拷贝,从而提高文件I/O性能。
  • 大文件处理:对于大文件,使用mmap可以避免一次性将整个文件读入内存,减少内存占用,提高处理效率。
  • 多个进程共享内存的场景:mmap可以用于实现进程间通信(IPC),通过将一个文件映射到多个进程的内存空间,实现进程间数据共享。
  • 内存数据库:mmap可以用于实现内存数据库,将数据库文件映射到内存,提高数据访问速度。

Redis、Nginx、Lucene、LevelDB、SQLite 等应用中在一些场景下使用了 mmap 来优化 IO 效率。 我要说话

下面是两段示例代码,来对比使用 mmap 和普通 IO 的性能差异。 我要说话

构造测试数据,向磁盘中写入一个文件 test_data.txt,1GB 大小,每行 100~200 个字节的文本字符。 我要说话

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define FILE_SIZE (1024 * 1024 * 1024)
#define MIN_LINE_SIZE 100
#define MAX_LINE_SIZE 200

char random_char() {
int r = rand() % 62;
if (r < 10) {
return '0' + r;
} else if (r < 36) {
return 'A' + r - 10;
} else {
return 'a' + r - 36;
}
}

int main() {
int fd;
size_t written_bytes = 0;
srand(time(NULL));

// 创建并打开文件
fd = open("test_data.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd < 0) {
perror("open error");
exit(EXIT_FAILURE);
}

// 向文件写入数据,直到达到指定大小
while (written_bytes < FILE_SIZE) {
int line_size = MIN_LINE_SIZE + rand() % (MAX_LINE_SIZE - MIN_LINE_SIZE + 1);
char buffer[MAX_LINE_SIZE + 1]; // 多出来的一个字节用于存放换行符

for (int i = 0; i < line_size; ++i) {
buffer[i] = random_char();
}
buffer[line_size] = '\n';

ssize_t to_write = line_size + 1;
ssize_t remaining = FILE_SIZE - written_bytes;
if (to_write > remaining) {
to_write = remaining;
}

ssize_t written = write(fd, buffer, to_write);
if (written < 0) {
perror("write error");
exit(EXIT_FAILURE);
}

written_bytes += written;
}

// 关闭文件
close(fd);

return 0;
}

使用 mmap 读取文件并统计行数: 我要说话

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <string.h>

int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}

int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}

struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
return 1;
}

char *file_data = (char*)mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (file_data == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}

clock_t start = clock();

size_t line_count = 0;
char *next = NULL;
for (char *current = file_data; current - file_data < sb.st_size; current = next + 1) {
next = (char*)memchr(current, '\n', sb.st_size - (current - file_data));
if (next) {
line_count++;
} else {
break;
}
}

clock_t end = clock();

printf("Line count: %zu\n", line_count);
printf("Time taken: %lf ms\n", (double)(end - start) / CLOCKS_PER_SEC * 1000);

munmap(file_data, sb.st_size);
close(fd);

return 0;
}

使用普通IO读取文件并统计行数: 我要说话

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>

int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}

FILE *fp = fopen(argv[1], "r");
if (!fp) {
perror("fopen");
return 1;
}

clock_t start = clock();

// 对文件数据进行处理,这里逐行读取文件内容并计算行数
size_t line_count = 0;
char *line = NULL;
size_t line_length = 0;
ssize_t read_length;
while ((read_length = getline(&line, &line_length, fp)) != -1) {
line_count++;
}

clock_t end = clock();

printf("Line count: %zu\n", line_count);
printf("Time taken: %lf ms\n", (double)(end - start) / CLOCKS_PER_SEC * 1000);

free(line);
fclose(fp);

return 0;
}

测试结果: 我要说话

image.png我要说话

sendfile

概念和适用场景

image.png

我要说话

sendfile 相当于实现了 mmap + write 的功能。该函数可以在两个文件描述符之间直接传输数据,避免了数据在用户空间和内核空间之间的拷贝,从而减少了一次从内核态到用户态的拷贝。 我要说话

sendfile适用的场景: 我要说话

  • 文件服务器:在文件服务器中,常需要将磁盘上的文件发送给客户端。使用sendfile方法可以直接将数据从磁盘传输到套接字,避免了多余的数据拷贝操作,提高了文件传输的性能。
  • 代理服务器:代理服务器需要将客户端的请求转发给目标服务器,并将目标服务器的响应返回给客户端。使用sendfile方法可以在套接字之间直接传输数据,提高了代理服务器的性能。
  • Web服务器:Web服务器需要将磁盘上的静态文件发送给客户端。使用sendfile方法可以提高文件传输的性能,降低服务器的资源消耗。
  • 数据库服务器:数据库服务器需要将数据文件中的数据发送给客户端。使用sendfile方法可以提高数据传输的效率,降低服务器的资源消耗。

Nginx、Apache、Haproxy 等项目使用 sendfile 来提高文件传输性能。 我要说话

下面是两段测试代码,来测试 sendfile 和普通 IO 的性能差异,仍然使用上面创建的测试文件我要说话

使用 sendfile 读取文件并发送: 我要说话

#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}
int server_fd, client_fd, file_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
struct stat file_stat;
off_t offset = 0;
ssize_t sent_bytes = 0;
struct timeval start, end;

// 创建TCP套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 绑定地址和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(12345);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

// 监听连接
listen(server_fd, 1);

// 接受客户端连接
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);

// 打开要发送的文件
file_fd = open(argv[1], O_RDONLY);
fstat(file_fd, &file_stat);

// 使用sendfile发送文件
gettimeofday(&start, NULL);
while (((sent_bytes = sendfile(client_fd, file_fd, &offset, file_stat.st_size)) > 0) && (file_stat.st_size > 0)) {
file_stat.st_size -= sent_bytes;
}
gettimeofday(&end, NULL);

// 计算并输出耗时
long elapsed_ms = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000;
printf("sendfile: %ld ms\n", elapsed_ms);

// 关闭文件和套接字
close(file_fd);
close(client_fd);
close(server_fd);

return 0;
}

使用普通 IO 读取文件并发送:我要说话

#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

#define BUFFER_SIZE 8192

int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}
int server_fd, client_fd, file_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
struct stat file_stat;
char buffer[BUFFER_SIZE];
ssize_t read_bytes, sent_bytes;
struct timeval start, end;

// 创建TCP套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 绑定地址和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(12345);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

// 监听连接
listen(server_fd, 1);

// 接受客户端连接
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);

// 打开要发送的文件
file_fd = open(argv[1], O_RDONLY);
fstat(file_fd, &file_stat);

// 使用普通I/O发送文件
gettimeofday(&start, NULL);
while ((read_bytes = read(file_fd, buffer, BUFFER_SIZE)) > 0) {
sent_bytes = 0;
while (sent_bytes < read_bytes) {
ssize_t sent = send(client_fd, buffer + sent_bytes, read_bytes - sent_bytes, 0);
if (sent < 0) {
perror("send error");
exit(EXIT_FAILURE);
}
sent_bytes += sent;
}
}
gettimeofday(&end, NULL);

// 计算并输出耗时
long elapsed_ms = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000;
printf("regular IO: %ld ms\n", elapsed_ms);

// 关闭文件和套接字
close(file_fd);
close(client_fd);
close(server_fd);

return 0;
}

客户端: 我要说话

#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define BUFFER_SIZE 8192

int main() {
int client_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
ssize_t received_bytes;

// 创建TCP套接字
client_fd = socket(AF_INET, SOCK_STREAM, 0);

// 连接到服务器
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(12345);
connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

// 接收文件
while ((received_bytes = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) {
// 这里只是简单地将接收到的数据丢弃
// 在实际使用中,你可能需要将数据写入文件或进行其他处理
}

// 关闭套接字
close(client_fd);

return 0;
}

测试结果: 我要说话

sendfile: 221 ms
regular IO: 499 ms

splice

概念和适用场景

image.png

我要说话

和 sendfile 一样,该函数可以将数据从一个文件描述符移动到另一个文件描述符,而不需要将数据从内核态拷贝到用户态,减少了一次拷贝过程。 我要说话

和 sendfile 的差异

splice 和 sendfile 都是用于优化数据传输的 Linux 系统调用,它们之间有一定的联系,但也有一些区别。 我要说话

功能上的差异我要说话

  • sendfile 主要用于在两个文件描述符之间传输数据,特别是将文件数据发送到网络套接字。它可以在内核空间直接完成数据传输,避免了用户空间和内核空间之间的数据拷贝,从而提高了性能。然而,sendfile的一个限制是,它只能用于普通文件和套接字之间的数据传输。

  • splice 是一个更通用的数据传输系统调用,它可以在任意类型的文件描述符之间传输数据,包括普通文件、设备文件、套接字等。 splice 的工作原理是将数据从一个文件描述符移动到另一个文件描述符,而不需要将数据拷贝到用户空间。这样,它可以在内核空间完成数据传输,提高性能。与 sendfile 相比, splice 的优势在于它可以处理更多类型的文件描述符,更加灵活。

数据传输方式差异:我要说话

  • sendfile 在内核空间直接将数据从一个文件描述符传输到另一个文件描述符,不需要通过用户空间的缓冲区。sendfile适用于将磁盘上的文件发送到网络套接字的场景,例如文件服务器、Web服务器等。

  • splice 通过管道(pipe)在文件描述符之间传输数据。首先,它将数据从一个文件描述符移动到管道,然后将数据从管道移动到另一个文件描述符。这样,数据在内核空间完成传输,避免了用户空间和内核空间之间的数据拷贝。splice适用于各种类型的文件描述符之间的数据传输,例如设备文件、套接字等。

使用 splice 读取文件并发送 我要说话

#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

#define BUFFER_SIZE 8192

int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}
int server_fd, client_fd, file_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
struct stat file_stat;
int pipe_fds[2];
ssize_t splice_bytes;
struct timeval start, end;

// 创建TCP套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 绑定地址和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(12345);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

// 监听连接
listen(server_fd, 1);

// 接受客户端连接
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);

// 打开要发送的文件
file_fd = open(argv[1], O_RDONLY);
fstat(file_fd, &file_stat);

// 创建管道
pipe(pipe_fds);

// 使用splice发送文件
gettimeofday(&start, NULL);
while ((splice_bytes = splice(file_fd, NULL, pipe_fds[1], NULL, BUFFER_SIZE, SPLICE_F_MOVE)) > 0) {
splice(pipe_fds[0], NULL, client_fd, NULL, splice_bytes, SPLICE_F_MOVE);
}
gettimeofday(&end, NULL);

// 计算并输出耗时
long elapsed_ms = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000;
printf("splice: %ld ms\n", elapsed_ms);

// 关闭文件和套接字
close(file_fd);
close(client_fd);
close(server_fd);

return 0;
}

使用普通 IO 读取文件 我要说话

#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

#define BUFFER_SIZE 8192

int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}
int server_fd, client_fd, file_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
struct stat file_stat;
char buffer[BUFFER_SIZE];
ssize_t read_bytes, sent_bytes;
struct timeval start, end;

// 创建TCP套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 绑定地址和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(12345);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

// 监听连接
listen(server_fd, 1);

// 接受客户端连接
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);

// 打开要发送的文件
file_fd = open(argv[1], O_RDONLY);
fstat(file_fd, &file_stat);

// 使用普通I/O发送文件
gettimeofday(&start, NULL);
while ((read_bytes = read(file_fd, buffer, BUFFER_SIZE)) > 0) {
sent_bytes = 0;
while (sent_bytes < read_bytes) {
ssize_t sent = send(client_fd, buffer + sent_bytes, read_bytes - sent_bytes, 0);
if (sent < 0) {
perror("send error");
exit(EXIT_FAILURE);
}
sent_bytes += sent;
}
}
gettimeofday(&end, NULL);

// 计算并输出耗时
long elapsed_ms = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000;
printf("regular IO: %ld ms\n", elapsed_ms);

// 关闭文件和套接字
close(file_fd);
close(client_fd);
close(server_fd);

return 0;
}

测试结果: 我要说话

splice: 224 ms
regular IO: 473 ms

Direct IO

Direct IO 是一种在文件系统层面实现的绕过内核缓存的技术。Direct IO 仍然使用文件系统,但是在读写文件时,数据直接从磁盘传输到用户空间,而不经过内核缓存。 我要说话

需要注意的是,不经过内核缓存,对于单次的文件读写来说,性能反而会下降。在某些大文件高并发读写场景下,可以通过开启 Direct IO 配合 aio,减小 IO 过程中的 CPU 和内存开销,从而提升整体性能。我要说话

仍然使用前面创建的 test_data.txt 做测试。测试单次文件读写的性能,可以发现实际上 directio 比普通的 IO 速度是更慢的。 我要说话

使用 direct io 读取文件 我要说话

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>

#ifndef O_DIRECT
#define O_DIRECT 040000
#endif

#define BUF_SIZE 1024

int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}

int fd = open(argv[1], O_RDONLY | O_DIRECT);
if (fd < 0) {
perror("open");
return 1;
}

char *buf;
if (posix_memalign((void **)&buf, BUF_SIZE, BUF_SIZE) != 0) {
perror("posix_memalign");
close(fd);
return 1;
}

clock_t start = clock();

size_t total_bytes = 0;
ssize_t read_length;
while ((read_length = read(fd, buf, BUF_SIZE)) > 0) {
total_bytes += read_length;
}

clock_t end = clock();

if (read_length < 0) {
perror("read");
}

printf("Total bytes: %zu\n", total_bytes);
printf("Time taken: %lf ms\n", (double) (end - start) / CLOCKS_PER_SEC * 1000);

free(buf);
close(fd);

return 0;
}

使用普通 io 读取文件 我要说话

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>

#define BUF_SIZE 1024

int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <file>\n", argv[0]);
return 1;
}

FILE *fp = fopen(argv[1], "r");
if (!fp) {
perror("fopen");
return 1;
}

char buf[BUF_SIZE];

clock_t start = clock();

size_t total_bytes = 0;
ssize_t read_length;
while ((read_length = fread(buf, 1, BUF_SIZE, fp)) > 0) {
total_bytes += read_length;
}

clock_t end = clock();

printf("Total bytes: %zu\n", total_bytes);
printf("Time taken: %lf ms\n", (double) (end - start) / CLOCKS_PER_SEC * 1000);

fclose(fp);

return 0;
}

测试结果: 我要说话

Direct IO:
Total bytes: 1073741824
Time taken: 9720.000000 ms

regular IO:
Total bytes: 1073741824
Time taken: 250.000000 ms

其它 IO 优化相关技术

Linux的写时复制(Copy-on-Write)机制是指在进程创建子进程或者读取文件内容时,并不会立即将数据完全复制到新的地址空间中。而是通过共享同一段物理内存来提高效率。只有当其中一个进程对该部分数据进行修改时,才会真正发生复制操作。我要说话

这种技术可以避免大量无用的数据复制,节省了系统开销。我要说话

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
const size_t data_size = 4096;
int status;

// 父子进程都能访问到的内存
char shared_data[] = "Hello, copy-on-write!";

// 创建子进程
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
exit(EXIT_FAILURE);
}

if (pid == 0) { // 子进程
printf("Child process: initial data: %s\n", shared_data);

// 这将触发写时复制,因为我们试图修改一个只读的内存页
shared_data[0] = 'h';
printf("Child process: modified data: %s\n", shared_data);

// 退出子进程
exit(EXIT_SUCCESS);
} else { // 父进程
// 等待子进程结束
waitpid(pid, &status, 0);

// 注意,尽管子进程修改了数据,但父进程看到的数据仍然是原始数据
printf("Parent process: data: %s\n", shared_data);
}

return 0;
}

测试结果: 我要说话

Child process: initial data: Hello, copy-on-write!
Child process: modified data: hello, copy-on-write!
Parent process: data: Hello, copy-on-write!

RDMA(Remote Direct Memory Access)

RDMA( Remote Direct Memory Access )意为远程直接地址访问,通过RDMA,本端节点可以“直接”访问远端节点的内存。所谓直接,指的是可以像访问本地内存一样,绕过传统以太网复杂的TCP/IP网络协议栈读写远端内存,而这个过程对端是不感知的,而且这个读写过程的大部分工作是由硬件而不是软件完成的。 我要说话

需要专门的硬件(网卡)支持。 我要说话

参考: https://zhuanlan.zhihu.com/p/138874738 我要说话

DPDK(Data Plane Development Kit)

Data Plane Development Kit 是运行在用户空间上利用自身提供的数据平面库来收发数据包,绕过了 Linux 内核协议栈对数据包处理过程。在收到数据包时,经DPDK重载的网卡驱动不会通过中断通知CPU,而是直接将数据包存入内存,交付应用层软件通过DPDK提供的接口来直接处理,这样节省了大量的CPU中断时间和内存拷贝时间。 我要说话

参考: https://zhuanlan.zhihu.com/p/632045322 我要说话

SPDK(Storage Performance Development Kit),包含一套驱动程序,以及一整套端到端的存储参考架构。SPDK的目标是能够把硬件平台的计算、网络、存储(基于 NVME 的高读写性能 ssd 磁盘)的最新性能进展充分发挥出来。自芯片而上进行设计优化,SPDK 已展示出超高的性能指标。我要说话

它的高性能实际上来自于两项核心技术:第一个是用户态运行,第二个是轮询模式驱动。我要说话

参考: https://zhuanlan.zhihu.com/p/646710218我要说话

参考文档:我要说话

本文链接:https://www.zoucz.com/blog/2023/12/31/93b6dc00-a7c3-11ee-95cb-3556e1632a5e/我要说话

☞ 参与评论我要说话


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK