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

推荐订阅源

C
Check Point Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
W
WeLiveSecurity
T
Troy Hunt's Blog
Project Zero
Project Zero
Security Archives - TechRepublic
Security Archives - TechRepublic
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
T
Threat Research - Cisco Blogs
T
Tenable Blog
Jina AI
Jina AI
Recorded Future
Recorded Future
T
The Exploit Database - CXSecurity.com
N
News | PayPal Newsroom
P
Palo Alto Networks Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
G
GRAHAM CLULEY
A
Arctic Wolf
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Register - Security
The Register - Security
Last Week in AI
Last Week in AI
P
Privacy & Cybersecurity Law Blog
Microsoft Azure Blog
Microsoft Azure Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
SecWiki News
SecWiki News
S
Schneier on Security
有赞技术团队
有赞技术团队
IT之家
IT之家
美团技术团队
Cisco Talos Blog
Cisco Talos Blog
NISL@THU
NISL@THU
P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
Hacker News: Ask HN
Hacker News: Ask HN
罗磊的独立博客
博客园_首页
Cyberwarzone
Cyberwarzone
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Webroot Blog
Webroot Blog
Latest news
Latest news
D
DataBreaches.Net
Know Your Adversary
Know Your Adversary
P
Privacy International News Feed
宝玉的分享
宝玉的分享
Simon Willison's Weblog
Simon Willison's Weblog
N
News and Events Feed by Topic

蛮荆

如何获取更多的免费服务器 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. 微内核

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

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

内核类型

扩展阅读