惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
Help Net Security
Help Net Security
月光博客
月光博客
N
News and Events Feed by Topic
Cloudbric
Cloudbric
博客园 - 司徒正美
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tenable Blog
The Register - Security
The Register - Security
The Hacker News
The Hacker News
I
InfoQ
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
Schneier on Security
Schneier on Security
WordPress大学
WordPress大学
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
K
Kaspersky official blog
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
F
Fortinet All Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security @ Cisco Blogs
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
Google Online Security Blog
Google Online Security Blog
S
Schneier on Security
C
Cisco Blogs
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Latest news
Latest news
PCI Perspectives
PCI Perspectives
T
The Blog of Author Tim Ferriss
P
Palo Alto Networks Blog
T
Tor Project blog
Project Zero
Project Zero
云风的 BLOG
云风的 BLOG
Webroot Blog
Webroot Blog
Attack and Defense Labs
Attack and Defense Labs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
Go 语言中的零拷贝
2023-03-10 · via 蛮荆

2023-03-10 Golang 操作系统

传统读写模式

传统读写模式流程图

  1. 第一次数据拷贝: 用户进程发起 read() 系统调用,当前上下文从用户态切换至内核态,DMA(Direct Memory Access) 引擎从文件中读取数据,并存储到内核态缓冲区 (DMA 拷贝)
  2. 第二次数据拷贝: 将数据从内核态缓冲区拷贝到用户态缓冲区 (CPU 拷贝),然后返回给用户进程,拷贝数据时会发生一次上下文切换 (从内核态切换到用户态)
  3. 第三次数据拷贝: 用户进程发起 write() 系统调用,当前上下文从用户态切换至内核态,数据从用户态缓冲区被拷贝到 Socket 缓冲区 (CPU 拷贝)
  4. 第四次数据拷贝: write() 系统调用结束返回到用户进程,当前上下文从内核态切换至用户态,第四次数据拷贝为异步执行,从 Socket 缓冲区拷贝到网卡 (DMA 拷贝)

transferTo

transferTo() 和 send() 类似,也是一个系统调用,用于在文件之间高效地传输数据。

transferTo 在操作系统层面实现了零拷贝技术,允许将数据直接从一个文件传输到另一个文件,而无需通过用户空间进行中转。

transferTo 流程图

  1. 第一次数据拷贝: 用户进程发起 transferTo() 调用,将文件数据拷贝到一个 Read buffer(内核态)中,当前上下文从用户态切换至内核态
  2. 第二次数据拷贝: 内核将 Read buffer 中的数据拷贝到 Socket 缓冲区
  3. 第三次数据拷贝: 数据从 Socket 缓冲区拷贝到网卡,当前上下文从内核态切换至用户态

相比较于传统的读写模式, transferTo 把上下文的切换次数从 4 次减少到 2 次,同时把数据拷贝的次数从 4 次降低到了 3 次, 虽然已经前进了一大步,但是作为过渡阶段,transferTo 距离零拷贝还有一些距离。

零拷贝

零拷贝是相对于用户态来讲的,数据在用户态不发生任何拷贝。

sendfile + DMA

sendfile() 是作用于两个文件描述符之间的数据拷贝的系统调用,这个拷贝操作是直接在内核中进行的,没有用户态到内核态的数据拷贝和上下文切换带来的开销,所以称为零拷贝技术。

Linux2.4 内核对 sendfile 系统调用做了改进:

sendfile 改进

  1. 用户进程发起 sendfile() 系统调用,当前上下文从用户态切换至内核态,DMA 将数据拷贝到内核缓冲区
  2. 向 Socket 缓冲区中发送当前数据在内核缓冲区的地址和偏移量两个值
  3. 根据 Socket 缓冲区的地址和偏移量,直接将内核缓冲区的数据拷贝到网卡,当前上下文从内核态切换至用户态

零拷贝流程图

相比较于传统的读写模式, sendfile + DMA 把上下文的切换次数从 4 次减少到 2 次,同时把数据拷贝的次数从 4 次降低到了 2 次 (2 次均为 DMA 拷贝),完全消除了数据从用户态和内核态之间拷贝数据带来的开销。

sendfile + DMA 虽然已经足够高效,但是依然存在两个不足之处:

  1. 方案本身需要引入新的硬件支持
  2. 输入文件描述符仅支持文件类型

splice

针对 sendfile + DMA 方案存在的不足,Linux 引入了 splice() 系统调用, splice() 不需要硬件支持,能够实现在任意的两个文件描述符时之间传输数据。

splice() 是基于管道缓冲区机制实现的,所以两个参数文件描述符必须有一个是管道设备。在实际开发中,splice() 作为实现零拷贝的首选,因此 sendfile() 的内部实现也替换为了 splice()。

Go 语言中的零拷贝

现在有了前文的理论基础后,我们来看下在 Go 语言中标准库的零拷贝方法原型和应用方法,笔者的 Go 版本为 go1.19 linux/amd64

sendfile

sendfile 的方法原型为 syscall.Sendfile,文件路径为 syscall/syscall_unix.go。

func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error)

一个简单的使用示例:

package main

import (
	"fmt"
	"os"
	"syscall"
)

func main() {
	// 设置源文件
	src, err := os.Open("/tmp/source.txt")
	if err != nil {
		panic(err)
	}
	defer src.Close()

	// 设置目标文件
	target, err := os.Create("/tmp/target.txt")
	if err != nil {
		panic(err)
	}
	defer target.Close()

	// 获取源文件的文件描述符
	srcFd := int(src.Fd())

	// 获取目标文件的文件描述符
	targetFd := int(target.Fd())

	// 使用 Sendfile 实现零拷贝 (拷贝 10 个字节)
	// 如果因为字符编码导致的字符截断问题 (如中文乱码问题), 结果自动保留到截断前的最后完整字节
	// 例如文件内容为 “星期三四五六七”,count 参数为 4, 那么只会拷贝第一个字 (一个汉字 3 个字节)
	// 但是需要注意的是,方法的返回值 written 不受影响 (和 count 参数保持一致)
	// 所以实际开发中,第三个参数 offset 必须设置正确,否则就可能引起乱码或数据丢失问题
	n, err := syscall.Sendfile(targetFd, srcFd, nil, 4)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("写入字节数: %d", n)
}

splice

sendfile 的方法原型为 syscall.Splice,文件路径为 syscall/zsyscall_linux_amd64.go。

func Splice(rfd int, roff *int64, wfd int, woff *int64, len int, flags int) (n int64, err error)

一个简单的使用示例:

package main

import (
	"fmt"
	"os"
	"syscall"
)

func main() {
	// 设置源文件
	src, err := os.Open("/tmp/source.txt")
	if err != nil {
		panic(err)
	}
	defer src.Close()

	// 设置目标文件
	target, err := os.Create("/tmp/target.txt")
	if err != nil {
		panic(err)
	}
	defer target.Close()

	// 创建管道文件
	// 作为两个文件传输数据的中介
	pipeReader, pipeWriter, err := os.Pipe()
	if err != nil {
		panic(err)
	}
	defer pipeReader.Close()
	defer pipeWriter.Close()

	// 设置文件读写模式
	// 笔者在标准库中没有找到对应的常量说明
	// 读者可以参考这个文档:
	//   https://pkg.go.dev/golang.org/x/sys/unix#pkg-constants
	//   SPLICE_F_NONBLOCK = 0x2
	spliceNonBlock := 0x02

	// 使用 Splice 将数据从源文件描述符移动到管道 writer
	_, err = syscall.Splice(int(src.Fd()), nil, int(pipeWriter.Fd()), nil, 1024, spliceNonBlock)

	if err != nil {
		panic(err)
	}

	// 使用 Splice 将数据从管道 reader 移动到目标文件描述符
	n, err := syscall.Splice(int(pipeReader.Fd()), nil, int(target.Fd()), nil, 1024, spliceNonBlock)
	if err != nil {
		panic(err)
	}

	fmt.Printf("写入字节数: %d", n)
}

附录

宏内核和微内核

1. 宏内核

宏内核是将操作系统功能作为一个紧密结合的整体放到内核,由于各模块共享信息,因此有很高的性能。

2. 微内核

由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。

在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态,因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。

内核类型

扩展阅读