“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
Go 中的零拷贝技术:sendfile、splice 与 io.Copy 的性能代价
深入解析 Go 中零拷贝技术的原理、实现与代价,揭示 io.Copy 在系统调用层面的性能瓶颈。
一、什么是零拷贝?为什么重要?
零拷贝(Zero-copy)是一种 I/O 优化技术,旨在减少数据在内核空间和用户空间之间的复制次数。传统 I/O 操作中,数据从磁盘或网络读取时,需要经过多次拷贝:从硬件到内核缓冲区,再从内核缓冲区到用户空间缓冲区,最后可能再写回内核缓冲区进行发送。每一次拷贝都消耗 CPU 周期和内存带宽,对于高吞吐量的网络服务(如反向代理、文件服务器、视频流服务)来说,这些开销会显著影响性能。
零拷贝的核心思路是:让数据在内核空间内直接流动,避免不必要的用户空间介入。这通常通过操作系统提供的特殊系统调用来实现,例如 Linux 的 sendfile、splice 和 tee。
二、传统 I/O 的代价:io.Copy 的隐形成本
在 Go 中,io.Copy 是最常用的数据拷贝函数之一。它的工作方式非常直观:从 io.Reader 读取数据到缓冲区(默认 32KB),然后写入 io.Writer。这个过程中,数据至少经历了两次拷贝:
- 从内核空间到用户空间:当
io.Copy从os.File读取时,会触发read系统调用,将数据从内核缓冲区复制到用户空间的 Go 切片。 - 从用户空间到内核空间:当
io.Copy写入网络连接(如net.TCPConn)时,会触发write系统调用,将数据从用户空间复制到内核的 socket 缓冲区。
此外,每次系统调用都涉及上下文切换(从用户态到内核态再返回),这本身就有数百纳秒到数微秒的开销。对于高并发场景,这些累积的代价非常可观。
示例:使用 io.Copy 代理文件到 TCP 连接
go
func proxyFileToTCP(filename string, conn net.Conn) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(conn, f)
return err
}
这段代码背后,每个 32KB 数据块都会经历:read 系统调用 → 数据从内核拷贝到用户空间 → write 系统调用 → 数据从用户空间拷贝回内核。对于大文件或高并发连接,CPU 的拷贝开销和上下文切换次数会急剧增加。
三、sendfile:最经典的零拷贝实现
sendfile 是 Linux 2.6.33 引入的系统调用,专门用于将数据从一个文件描述符直接传输到另一个文件描述符(典型场景:文件到 socket)。它的签名如下:
c
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
关键点:
in_fd必须是一个支持mmap的文件(即普通文件),不能是 socket 或 pipe。out_fd通常是一个 socket。- 数据直接从内核的文件缓存(page cache)传输到 socket 缓冲区,完全绕过用户空间。
Go 中的 sendfile 支持
Go 标准库在 net 包中偷偷使用了 sendfile。当你通过 net.TCPConn 或 net.UnixConn 从 *os.File 读取并写入时,Go 内部会检测是否可以使用 sendfile 优化。具体实现位于 src/net/sendfile_linux.go:
go
// 简化的伪代码
func sendFile(c *netFD, r io.Reader) (written int64, err error) {
// 检查 r 是否是 *os.File
if f, ok := r.(*os.File); ok {
// 尝试使用 sendfile 系统调用
n, err := syscall.Sendfile(c.sysfd, f.Fd(), nil, remain)
// ...
}
// 否则回退到通用 io.Copy
}
这意味着,如果你直接使用 io.Copy(conn, file),Go 会自动尝试 sendfile 路径。但有一个陷阱:如果 io.Copy 的 Reader 不是 *os.File(例如 bufio.Reader 或自定义 Reader),则不会触发 sendfile。
性能对比
在原文作者的基准测试中,传输一个 1GB 文件:
io.Copy(传统方式):约 1.2 GB/s,CPU 占用约 15%。sendfile优化路径:约 3.5 GB/s,CPU 占用约 3%。
零拷贝带来的吞吐量提升接近 3 倍,CPU 消耗降低 5 倍。
四、splice:更通用的零拷贝
splice 是比 sendfile 更通用的系统调用,它可以在两个文件描述符之间移动数据,不需要其中一个必须是文件。它的签名:
c
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
关键特性:
- 至少一个文件描述符必须是 pipe(管道)。
- 它通过在内核空间操作页表来实现零拷贝,而不是真正的数据移动。
- 可以用于 socket-to-socket 的直接传输(通过一个中间 pipe)。
典型用法:socket 到 socket 的零拷贝代理
假设你正在编写一个 TCP 代理,需要将数据从一个连接转发到另一个连接。传统 io.Copy 会涉及用户空间缓冲。使用 splice 可以这样实现:
go
import "golang.org/x/sys/unix"
func spliceProxy(dst, src *net.TCPConn) error {
// 创建一个 pipe
pipes := make([]int, 2)
if err := unix.Pipe2(pipes, unix.O_NONBLOCK); err != nil {
return err
}
defer unix.Close(pipes[0])
defer unix.Close(pipes[1])
// 循环从 src 读取到 pipe,再从 pipe 写入 dst
buf := make([]byte, 32*1024)
for {
// 从 src 到 pipe
n, err := unix.Splice(src.Fd(), nil, pipes[1], nil, len(buf), 0)
if n == 0 || err != nil {
break
}
// 从 pipe 到 dst
_, err = unix.Splice(pipes[0], nil, dst.Fd(), nil, int(n), 0)
if err != nil {
break
}
}
return nil
}
注意:虽然 splice 避免了用户空间拷贝,但它仍然需要两次系统调用(一次从源到 pipe,一次从 pipe 到目标),并且 pipe 本身在内核中占用缓冲区。不过,由于数据始终在内核空间流转,整体性能通常优于 io.Copy。
Go 标准库对 splice 的支持
截至 Go 1.21,标准库并未直接暴露 splice 给用户,但在内部(如 net/http 的反向代理)有使用 splice 的尝试。例如,httputil.ReverseProxy 在 Linux 上会尝试使用 splice 来转发请求体。
五、零拷贝的适用场景与限制
零拷贝并非万能药,它有一些重要的限制:
数据不能修改:因为数据不经过用户空间,你无法在传输过程中对数据进行任何处理(如加密、压缩、格式转换)。如果需要修改数据,必须使用传统方式。
文件描述符类型限制:
sendfile:输入必须是普通文件,输出必须是 socket。splice:至少一端是 pipe。- 不支持
*os.File到*os.File的零拷贝(文件到文件复制建议使用io.Copy或os.CopyFS)。
小数据量不划算:对于小于几 KB 的数据,系统调用的开销可能超过节省的拷贝成本。零拷贝更适合大块数据传输(通常 > 64KB)。
跨平台问题:
sendfile和splice是 Linux 特有的。macOS 有sendfile但接口不同,Windows 有TransmitFile。Go 标准库在内部做了平台适配,但如果你直接使用syscall包,代码将不可移植。
六、实战:如何让 io.Copy 充分利用零拷贝
作为 Go 开发者,你可能不需要手动调用 sendfile 或 splice,但了解以下原则可以帮助你写出更高效的代码:
直接使用
io.Copy(conn, file):Go 会自动检测是否可以使用sendfile。避免在中间包裹bufio.Reader或ioutil.ReadAll。对于自定义 Reader,考虑实现
io.ReaderFrom接口:io.Copy会检查 Writer 是否实现了ReadFrom方法。如果实现了,它会调用ReadFrom而非逐块读写。例如,net.TCPConn实现了ReadFrom,内部会尝试sendfile或splice。使用
net.TCPConn的ReadFrom方法:如果你需要手动控制,可以直接调用conn.ReadFrom(reader),它会尝试最优的零拷贝路径。对于高吞吐代理,考虑使用
golang.org/x/sys/unix中的Splice和Sendfile:但要注意跨平台兼容性。
一个完整的优化示例:
go
// 传统方式(可能触发两次拷贝)
func copySlow(dst io.Writer, src io.Reader) {
io.Copy(dst, src)
}
// 优化方式(利用 ReadFrom 的零拷贝)
func copyFast(dst io.Writer, src io.Reader) {
// 如果 dst 是 *net.TCPConn,会调用其 ReadFrom
// ReadFrom 内部尝试 sendfile/splice
if rf, ok := dst.(io.ReaderFrom); ok {
rf.ReadFrom(src)
} else {
io.Copy(dst, src)
}
}
七、性能代价的量化分析
原文作者通过实验量化了不同场景下的性能差异:
| 场景 | 吞吐量 | CPU 使用率 | 系统调用次数 |
|---|---|---|---|
| io.Copy(文件→socket) | 1.2 GB/s | 15% | 约 64,000 次/秒 |
| sendfile(文件→socket) | 3.5 GB/s | 3% | 约 32 次/秒 |
| splice(socket→socket) | 2.8 GB/s | 5% | 约 64 次/秒 |
注意:sendfile 的系统调用次数远少于 io.Copy,因为每次调用可以传输整个文件(或指定大小),而 io.Copy 需要每 32KB 触发一次 read+write。
八、总结与最佳实践
- 零拷贝是网络 I/O 优化的利器,尤其适合文件服务器、CDN 节点、反向代理等场景。
- Go 标准库已经为你做了大量工作:使用
io.Copy时,尽量保持 Reader 和 Writer 是原始*os.File或*net.TCPConn,避免中间包装。 - 不要过早优化:对于大多数应用,
io.Copy的性能已经足够。只有当 profiling 显示 I/O 是瓶颈,且数据量较大时,才考虑手动使用sendfile或splice。 - 跨平台意识:如果代码需要在多个操作系统上运行,使用标准库的抽象接口(如
io.Copy)比直接调用系统调用更安全。
最后,记住零拷贝的核心哲学:减少数据在内存中的无意义移动。理解了这一点,你就能在合适的场景做出正确的选择。
原文链接:https://segflow.github.io/post/zero-copy-sendfile-splice/