sendfile、aio和directIO
# sendfile、aio和directio
# 零拷贝的种类
Sendfile:
- 在 Linux 和其他 Unix 系统中,
sendfile()
系统调用可以直接将文件内容从文件描述符发送到网络套接字,而无需在用户空间中进行数据拷贝。这对于文件传输和网络服务非常有效。
Mmap:
mmap()
系统调用可以将文件映射到进程的虚拟内存空间,从而允许应用程序直接访问文件内容,而无需使用传统的读写调用。这使得文件的访问速度更快,并且可以与其他零拷贝机制结合使用。
Receive-Files:
- 与
sendfile()
类似,某些系统(如 Windows)也提供类似的机制来接收文件,避免了在用户空间的拷贝。
Scatter-Gather I/O:
- 通过一次系统调用,允许将数据分散到多个缓冲区中,或从多个缓冲区聚集数据。这减少了多次系统调用的开销,适用于网络通信和文件处理。
Kernel-Bypass:
- 一些高级技术(如 DPDK、RDMA)允许应用程序绕过操作系统内核直接与网络硬件交互,从而实现更高效的数据传输。
Direct I/O:
- 允许应用程序直接对磁盘进行 I/O 操作,避免操作系统的缓存。这在需要高性能文件系统的场景中非常有用。
# sendfile零拷贝
零拷贝(zero-copy)是一种数据传输技术,它可以在不涉及 CPU 的数据拷贝的情况下将数据从一个存储区域传输到另一个存储区域。这种技术通常用于优化高性能网络应用程序和文件系统,因为它可以减少 CPU 的负担并提高数据传输速度。
# sendfile
linux 里查看sendfile
函数的定义man sendfile
:
read
+send
系统调用发送文件经过的拷贝过程:
read
系统调用产生一次上下文切换:从用户态切换到内核态;- DMA 执行拷贝,把文件内容拷贝到内核缓冲区 Page Cache;
- CPU 把文件内容从 Page Cache 拷贝到用户缓冲区;
read
系统调用返回,从内核态切换到用户态;write
系统调用从用户态切换至内核态;- CPU 把文件内容从用户缓冲区拷贝至 Socket 发送缓存区;
write
系统调用返回,从内核态切换到用户态;- DMA 执行拷贝,把文件内容从 Socket 发送缓存区拷贝至网卡。
sendfile
系统调用发送文件的过程:
sendfile
系统调用产生一次上下文切换:从用户态切换到内核态;- DMA 执行拷贝,把文件内容拷贝到内核缓冲区 Page Cache;
sendfile
系统调用返回,从内核态切换到用户态;- CPU 把文件内容从 Page Cache 拷贝至 Socket 发送缓存区;
- DMA 执行拷贝,把文件内容从 Socket 发送缓存区拷贝至网卡。
使用 DMA 技术可以进一步减少 CPU 拷贝:
sendfile
系统调用产生一次上下文切换:从用户态切换到内核态;- DMA 执行拷贝,把文件内容拷贝到内核缓冲区 Page Cache;
sendfile
系统调用返回,从内核态切换到用户态;- DMA 执行拷贝,把文件内容从 Page Cache 拷贝至网卡。
# sendfile的优缺点
优点
- 高性能:由于直接在内核中传输数据,避免了用户空间与内核空间之间的多次上下文切换,减少了 CPU 的负担。例如 Kafka 和 nginx 就使用 sendfile 做性能优化。
- 减少内存使用率:不要将文件数据拷贝到用户空间,降低了内存的占用,适合处理大文件。
缺点
- 灵活性限制:只能处理特定的文件描述符(如 socke 和文件),不支持一般的 I/O 操作。
- 不能对数据处理:由于数据的整个阶段都在内核中进行,不会拷贝到用户空间,程序不能对数据进行二次处理。
- 错误处理复杂:由于数据传输在内核中进行,调试和错误处理可能相对复杂。
# Go 程序实现零拷贝
在 Go 中实现零拷贝的方法主要涉及以下两个方面:
文件传输
Go 语言的标准库中提供了一些与文件传输相关的函数,例如
io.Copy()
,可以将一个Reader
中的数据复制到一个Writer
中。这个过程中,数据会在内存中进行复制,不符合零拷贝的要求。为了实现零拷贝的文件传输,可以使用
os.File
中的ReadFrom()
和WriteTo()
方法。这两个方法可以直接将文件中的数据读取到一个Writer
中,或者将一个Reader
中的数据写入到文件中,而不需要将数据先复制到内存中。例如,下面的代码演示了如何使用
ReadFrom()
和WriteTo()
实现文件复制:goCopy codesrc, err := os.Open("srcfile") if err != nil { // handle error } defer src.Close() dst, err := os.Create("dstfile") if err != nil { // handle error } defer dst.Close() _, err = dst.ReadFrom(src) if err != nil { // handle error }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16网络传输
在 Go 中实现零拷贝的网络传输可以使用
net
包中的一些函数和结构体。例如,
net
包中提供了net.TCPConn
结构体,它包含了一个与 TCP 连接相关的文件描述符(fd
),可以使用这个fd
实现零拷贝的网络传输。具体来说,可以使用
syscall
包中的sendfile()
函数,将一个文件描述符中的数据直接传输到另一个文件描述符中,避免了中间的内存复制过程。例如,下面的代码演示了如何使用
sendfile()
实现网络文件传输:goCopy codesrc, err := os.Open("srcfile") if err != nil { // handle error } defer src.Close() conn, err := net.Dial("tcp", "example.com:80") if err != nil { // handle error } defer conn.Close() // get the file descriptor of the connection tcpConn, ok := conn.(*net.TCPConn) if !ok { // handle error } fd, err := tcpConn.FileDescriptor() if err != nil { // handle error } // transfer the file descriptor to the sendfile function n, err := syscall.Sendfile(int(fd), int(src.Fd()), nil, 1024) if err != nil { // handle error }
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
# mmap实现零拷贝
mmap
是一种共享内存的方法,将文件或其他对象(如公共物理内存)映射到进程的虚拟内存地址空间。这种内存映射使得文件的内容可以像数组一样直接访问,提供不同进程之间的内存共享和高效的文件 I/O 操作。
# mmap
的原理
虚拟内存:每个进程在 Linux 中都有自己的虚拟地址空间。
mmap
允许将文件或设备的内容映射到这个虚拟地址空间的一部分。页管理:Linux 内核使用分页管理内存。
mmap
将文件的部分或全部内容映射到内存页中。读取或写入这些映射的地址实际上是对文件的操作。内存共享:多个进程可以通过
mmap
共享同一个文件。这使得进程之间能够方便地共享数据。延迟加载:文件的内容并不会立即加载到内存中。只有在访问映射的地址时,内核才会从文件中读取数据(惰性加载)。
文件修改:如果使用
MAP_SHARED
标志映射文件,修改映射区域会直接影响文件内容;如果使用MAP_PRIVATE
,则会在修改时创建一个私有副本。
# 使用示例
下面是一个简单的示例,演示如何使用 mmap
:
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDWR);
if (fd == -1) {
perror("open");
return 1;
}
// 获取文件大小
off_t size = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
// 映射文件到内存
char *map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 访问和修改映射区域
printf("File content: %s\n", map);
map[0] = 'H'; // 修改文件内容
// 解除映射
if (munmap(map, size) == -1) {
perror("munmap");
}
close(fd);
return 0;
}
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
这个示例打开一个文件,将其内容映射到内存中,并修改文件的第一个字符。通过 mmap
,可以高效地处理文件内容,而不需要使用传统的读写操作。
# 应用场景
mmap
在 Linux 中有多种应用场景,主要包括以下几个方面:
文件 I/O 优化
大文件处理:通过内存映射,可以直接访问大文件的部分内容,而无需将整个文件读入内存,节省内存和时间。
高效读取:对于频繁访问的文件,
mmap
可以减少系统调用的开销,提高性能。
进程间通信 (IPC)
- 共享内存:多个进程可以通过
mmap
映射同一文件或设备,实现数据的共享和通信。使用MAP_SHARED
可以保证数据的一致性。例如 nginx 中就用到了mmap
实现master
进程与worker
进程中间的进程间通信。
- 共享内存:多个进程可以通过
动态库加载
- 库文件映射:操作系统可以通过
mmap
映射共享库文件到进程的地址空间,从而实现动态链接和加载,节省内存使用。
- 库文件映射:操作系统可以通过
虚拟内存管理
- 分页文件:操作系统可以使用
mmap
将交换空间映射到进程的地址空间,管理虚拟内存。
- 分页文件:操作系统可以使用
存储设备访问
- 设备文件映射:可以将设备文件(如
/dev/mem
)映射到内存,以便直接访问硬件设备。
- 设备文件映射:可以将设备文件(如
文件系统实现
- 文件系统缓存:一些文件系统(如 FUSE)使用
mmap
提供对文件的直接访问,提高了性能。
- 文件系统缓存:一些文件系统(如 FUSE)使用
这些应用场景显示了 mmap
的灵活性和高效性,使其在处理大数据、进程间通信和系统资源管理等方面发挥了重要作用。
# mmap的优缺点
优点
- 高效性:减少了文件 I/O 的系统调用次数,通过内存直接访问文件内容,提高了性能。
- 易用性:可以像数组一样访问文件数据,简化了编程模型。
- 共享内存:支持多个进程共享同一映射区域,方便进程间通信。
- 延迟加载:只在访问时加载数据,节省内存。
缺点
- 复杂性:错误处理和映射管理可能较为复杂,尤其是在多进程环境中。
- 资源限制:映射的内存区域受限于系统的虚拟内存大小,可能导致映射失败。
- 数据一致性:使用
MAP_SHARED
时,多个进程可能导致数据不一致,需要额外同步机制。 - 不适合小文件:对于小文件,使用
mmap
可能引入额外开销,不如传统读写操作简单高效。
# aio
相对于同步 IO,异步 IO(Asychronous IO) 不是顺序执行。用户进程进行aio_read
系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket
数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO 两个阶段,进程都是非阻塞的。
# 原理
- 第一阶段:
- 用户进程调用
aio_read
,创建信号回调函数 - 内核等待数据就绪
- 用户进程无需阻塞,可以做任何事情
- 用户进程调用
- 第二阶段:
- 内核数据就绪
- 内核数据拷贝到用户缓冲区
- 拷贝完成,内核递交信号触发
aio_read
中的回调函数 - 用户进程处理数据
# aio的优缺点
优点:
- 高性能:用户进程不需要被阻塞着,可以干其他事,提高了性能
- 非阻塞:不再像非阻塞 IO 那样需要轮询来检查 IO 是否就绪,而是由系统信号来通知
缺点:
- IO 效率低时容易系统崩溃:在高并发场景下,因为 IO 效率较低,所以会积累很多任务在系统中,容易导致系统崩溃。(可以用限流等方式解决,但是实现方式繁琐复杂)
# direct IO
Direct IO 即直接 IO(直接读取文件,不经过系统缓存),默认情况下,用户进程读取文件时,操作系统都会用到内核提供的 Page Cache 来加速文件的读取,例如 MySQL 的预读;但是有些情况下使用操作系统提高的 Page Cache 可能会对用户进程起着反作用,例如高并发的 Nginx 传输大文件,由于大文件频繁地占据着 Page Cache,导致其他小文件(例如 JavaScript、HTML 和图片等静态文件)不能缓存命中,此时 Page Cache 变成了摆设,这时候就需要配置 Direct IO 来避免大文件缓存到 Page Cache,避免缓存失效和降低内存的使用。
# 优缺点
Direct IO 绕过 Page Cache,减少了 Page Cache 和用户数据复制次数,降低了文件读写所带来的 CPU 负载能力和内存带宽的占用率。然而,Direct IO 并非没有缺点。首先,不经过内存缓冲区直接进行磁盘读写操作,必然会引起阻塞,因而需要将 Direct IO 与异步 IO(AIO)一起使用。然后,除了缓存外,内核(IO 调度算法)会试图缓存尽量多的连续 IO 在 PageCache 中,最后合并成一个更大的 IO 再发给磁盘,这样可以减少磁盘的寻址操作,内核也会预读后续的 IO 放在 PageCache 中,减少磁盘操作。Direct IO 绕过了 Page Cache,所以无法享受 Page Cache 所带来的性能提升。
优点
- 降低 CPU 和内存负载:由于 Direct IO 绕过来 Page Cache 的拷贝,可以降低文件读写带来的 CPU 负载和内存压力
- 对高并发大文件读写友好:大文件难以命中 PageCache 缓存,又带来额外的内存拷贝,同时还挤占了小文件使用 PageCache 时需要的内存,使用 Direct IO 可以减少 PageCache 被占用的缺点。
缺点
- 无法利用缓存:由于 Direct IO 绕过了 Page Cache,所以无法利用 Page Cache 预读和 IO 聚合特性。
# 小结
sendfile
零拷贝技术可以有效的降低 CPU 负载,减少上下文切换和内存的占用,使用在读取大文件时使用。例如 Kafka 使用sendfile
实现高性能的文件读取,Nginx
通过利用sendfile
系统调用,Nginx
消除了文件描述符之间的冗余数据复制,从而最大限度地减少了 CPU 的使用。mmap
既可以提供零拷贝也可以提供进程间通信,对于频繁访问的文件,mmap
可以减少系统调用的开销,提高性能。例如Nginx
通过mmap
使用master
与worker
进程的内存共享,master
就读取的配置文件worker
也可以看到。aio
一般与DirectIO
配合使用,适合高并发大文件的传输。例如Nginx
通过aio
和DirectIO
优化高并发下大文件的传输,提搞 Page Cache 命中率。