零拷贝(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 环节,让数据不再从内核缓冲区的内存页拷贝到用户缓冲区,减少数据拷贝的次数。

mmap

概念和适用场景

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 个字节的文本字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#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 读取文件并统计行数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#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读取文件并统计行数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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 读取文件并发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#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 读取文件并发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#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;
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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;
}

测试结果:

1
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 读取文件并发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#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 读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#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;
}

测试结果:

1
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 读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#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 读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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;
}

测试结果:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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;
}

测试结果:

1
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

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

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

参考: https://zhuanlan.zhihu.com/p/646710218

参考文档:

☞ 参与评论