FeelingLife FeelingLife
首页
  • Go

    • Go基础知识
  • Python

    • Python进阶
  • 操作系统
  • 计算机网络
  • MySQL
  • 学习笔记
  • 常用到的算法
  • Docker
  • Kubernetes
  • Observability
  • 容器底层
其他技术
  • 友情链接
  • 收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

xuqil

一介帆夫
首页
  • Go

    • Go基础知识
  • Python

    • Python进阶
  • 操作系统
  • 计算机网络
  • MySQL
  • 学习笔记
  • 常用到的算法
  • Docker
  • Kubernetes
  • Observability
  • 容器底层
其他技术
  • 友情链接
  • 收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 应用层
  • 运输层
  • 抓包

  • 网络编程

    • Linux的IO模型
    • select、poll和epoll
    • sendfile、aio和directIO
      • 零拷贝的种类
      • sendfile零拷贝
        • sendfile
        • sendfile的优缺点
        • Go 程序实现零拷贝
      • mmap实现零拷贝
        • mmap的原理
        • 使用示例
        • 应用场景
        • mmap的优缺点
      • aio
        • 原理
        • aio的优缺点
      • direct IO
        • 优缺点
      • 小结
    • ip命令的使用
  • 云网络

  • 常见面试题
  • 《计算机网络知识》
  • 网络编程
xuqil
2024-09-25
目录

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:

image-20240924212347299

read+send 系统调用发送文件经过的拷贝过程:

  1. read系统调用产生一次上下文切换:从用户态切换到内核态;
  2. DMA 执行拷贝,把文件内容拷贝到内核缓冲区 Page Cache;
  3. CPU 把文件内容从 Page Cache 拷贝到用户缓冲区;
  4. read系统调用返回,从内核态切换到用户态;
  5. write系统调用从用户态切换至内核态;
  6. CPU 把文件内容从用户缓冲区拷贝至 Socket 发送缓存区;
  7. write系统调用返回,从内核态切换到用户态;
  8. DMA 执行拷贝,把文件内容从 Socket 发送缓存区拷贝至网卡。

image-20240924212347299

sendfile 系统调用发送文件的过程:

  1. sendfile系统调用产生一次上下文切换:从用户态切换到内核态;
  2. DMA 执行拷贝,把文件内容拷贝到内核缓冲区 Page Cache;
  3. sendfile系统调用返回,从内核态切换到用户态;
  4. CPU 把文件内容从 Page Cache 拷贝至 Socket 发送缓存区;
  5. DMA 执行拷贝,把文件内容从 Socket 发送缓存区拷贝至网卡。

sendfile-sendfile.drawio

使用 DMA 技术可以进一步减少 CPU 拷贝:

  1. sendfile系统调用产生一次上下文切换:从用户态切换到内核态;
  2. DMA 执行拷贝,把文件内容拷贝到内核缓冲区 Page Cache;
  3. sendfile系统调用返回,从内核态切换到用户态;
  4. DMA 执行拷贝,把文件内容从 Page Cache 拷贝至网卡。

sendfile-sendfile.drawio

# sendfile的优缺点

优点

  1. 高性能:由于直接在内核中传输数据,避免了用户空间与内核空间之间的多次上下文切换,减少了 CPU 的负担。例如 Kafka 和 nginx 就使用 sendfile 做性能优化。
  2. 减少内存使用率:不要将文件数据拷贝到用户空间,降低了内存的占用,适合处理大文件。

缺点

  1. 灵活性限制:只能处理特定的文件描述符(如 socke 和文件),不支持一般的 I/O 操作。
  2. 不能对数据处理:由于数据的整个阶段都在内核中进行,不会拷贝到用户空间,程序不能对数据进行二次处理。
  3. 错误处理复杂:由于数据传输在内核中进行,调试和错误处理可能相对复杂。

# Go 程序实现零拷贝

在 Go 中实现零拷贝的方法主要涉及以下两个方面:

  1. 文件传输

    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
  2. 网络传输

    在 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 操作。

sendfile-sendfile.drawio

sendfile-sendfile.drawio

# mmap的原理

  1. 虚拟内存:每个进程在 Linux 中都有自己的虚拟地址空间。mmap 允许将文件或设备的内容映射到这个虚拟地址空间的一部分。

  2. 页管理:Linux 内核使用分页管理内存。mmap 将文件的部分或全部内容映射到内存页中。读取或写入这些映射的地址实际上是对文件的操作。

  3. 内存共享:多个进程可以通过 mmap 共享同一个文件。这使得进程之间能够方便地共享数据。

  4. 延迟加载:文件的内容并不会立即加载到内存中。只有在访问映射的地址时,内核才会从文件中读取数据(惰性加载)。

  5. 文件修改:如果使用 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;
}
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

这个示例打开一个文件,将其内容映射到内存中,并修改文件的第一个字符。通过 mmap,可以高效地处理文件内容,而不需要使用传统的读写操作。

# 应用场景

mmap 在 Linux 中有多种应用场景,主要包括以下几个方面:

  1. 文件 I/O 优化

    • 大文件处理:通过内存映射,可以直接访问大文件的部分内容,而无需将整个文件读入内存,节省内存和时间。

    • 高效读取:对于频繁访问的文件,mmap 可以减少系统调用的开销,提高性能。

  2. 进程间通信 (IPC)

    • 共享内存:多个进程可以通过 mmap 映射同一文件或设备,实现数据的共享和通信。使用 MAP_SHARED 可以保证数据的一致性。例如 nginx 中就用到了 mmap实现master进程与worker进程中间的进程间通信。
  3. 动态库加载

    • 库文件映射:操作系统可以通过 mmap 映射共享库文件到进程的地址空间,从而实现动态链接和加载,节省内存使用。
  4. 虚拟内存管理

    • 分页文件:操作系统可以使用 mmap 将交换空间映射到进程的地址空间,管理虚拟内存。
  5. 存储设备访问

    • 设备文件映射:可以将设备文件(如 /dev/mem)映射到内存,以便直接访问硬件设备。
  6. 文件系统实现

    • 文件系统缓存:一些文件系统(如 FUSE)使用 mmap 提供对文件的直接访问,提高了性能。

这些应用场景显示了 mmap 的灵活性和高效性,使其在处理大数据、进程间通信和系统资源管理等方面发挥了重要作用。

# mmap的优缺点

优点

  1. 高效性:减少了文件 I/O 的系统调用次数,通过内存直接访问文件内容,提高了性能。
  2. 易用性:可以像数组一样访问文件数据,简化了编程模型。
  3. 共享内存:支持多个进程共享同一映射区域,方便进程间通信。
  4. 延迟加载:只在访问时加载数据,节省内存。

缺点

  1. 复杂性:错误处理和映射管理可能较为复杂,尤其是在多进程环境中。
  2. 资源限制:映射的内存区域受限于系统的虚拟内存大小,可能导致映射失败。
  3. 数据一致性:使用 MAP_SHARED 时,多个进程可能导致数据不一致,需要额外同步机制。
  4. 不适合小文件:对于小文件,使用 mmap 可能引入额外开销,不如传统读写操作简单高效。

# aio

相对于同步 IO,异步 IO(Asychronous IO) 不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO 两个阶段,进程都是非阻塞的。

# 原理

img

  1. 第一阶段:
    1. 用户进程调用aio_read,创建信号回调函数
    2. 内核等待数据就绪
    3. 用户进程无需阻塞,可以做任何事情
  2. 第二阶段:
    1. 内核数据就绪
    2. 内核数据拷贝到用户缓冲区
    3. 拷贝完成,内核递交信号触发aio_read中的回调函数
    4. 用户进程处理数据

# aio的优缺点

优点:

  1. 高性能:用户进程不需要被阻塞着,可以干其他事,提高了性能
  2. 非阻塞:不再像非阻塞 IO 那样需要轮询来检查 IO 是否就绪,而是由系统信号来通知

缺点:

  1. IO 效率低时容易系统崩溃:在高并发场景下,因为 IO 效率较低,所以会积累很多任务在系统中,容易导致系统崩溃。(可以用限流等方式解决,但是实现方式繁琐复杂)

# direct IO

Direct IO 即直接 IO(直接读取文件,不经过系统缓存),默认情况下,用户进程读取文件时,操作系统都会用到内核提供的 Page Cache 来加速文件的读取,例如 MySQL 的预读;但是有些情况下使用操作系统提高的 Page Cache 可能会对用户进程起着反作用,例如高并发的 Nginx 传输大文件,由于大文件频繁地占据着 Page Cache,导致其他小文件(例如 JavaScript、HTML 和图片等静态文件)不能缓存命中,此时 Page Cache 变成了摆设,这时候就需要配置 Direct IO 来避免大文件缓存到 Page Cache,避免缓存失效和降低内存的使用。

img

# 优缺点

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 所带来的性能提升。

优点

  1. 降低 CPU 和内存负载:由于 Direct IO 绕过来 Page Cache 的拷贝,可以降低文件读写带来的 CPU 负载和内存压力
  2. 对高并发大文件读写友好:大文件难以命中 PageCache 缓存,又带来额外的内存拷贝,同时还挤占了小文件使用 PageCache 时需要的内存,使用 Direct IO 可以减少 PageCache 被占用的缺点。

缺点

  1. 无法利用缓存:由于 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 命中率。
上次更新: 2024/09/25, 15:44:41
select、poll和epoll
ip命令的使用

← select、poll和epoll ip命令的使用→

最近更新
01
ip命令的使用
11-02
02
veth-pair容器中的网络
11-02
03
退避算法
09-10
更多文章>
Theme by Vdoing | Copyright © 2018-2025 FeelingLife | 粤ICP备2022093535号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式