4、sendfile零拷贝机制:从系统调用到行情分发
各位同学,今天我们来聊聊sendfile。这个系统调用,说白了就是Linux零拷贝技术的「扛把子」。我在做行情系统那几年,几乎天天跟它打交道。你想想看,交易所一秒能推送几十万笔行情,如果每次都要把数据从内核拷到用户态,再拷回去发出去——那延迟早就爆了。
4.1 sendfile系统调用详解
先看它的原型,很简单:
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
参数不多,但每个都有讲究:
- out_fd:目标文件描述符,通常是socket
- in_fd:源文件描述符,必须是支持mmap的文件(比如普通文件)
- offset:从源文件的哪个位置开始读,传NULL就从当前偏移
- count:要传输的字节数
sendfile的返回值是实际传输的字节数。如果返回-1,errno会告诉你原因。常见的错误有EAGAIN(非阻塞模式下缓冲区满)、EINVAL(参数不合法)。
它的工作流程,我画了张图:
看到没?数据从磁盘到网卡,只经过两次DMA传输和一次CPU拷贝。传统方式要四次拷贝,两次上下文切换。sendfile直接把用户空间跳过去了。
4.2 DMA引擎协同
DMA(Direct Memory Access)是什么?说白了就是硬件自己搬数据,不用CPU插手。我打个比方:CPU是项目经理,DMA是搬砖工。项目经理只需要告诉搬砖工「把A搬到B」,然后就可以去干别的活了。
在sendfile流程中,DMA干了两次活:
- 第一次DMA:从磁盘把数据搬到内核的Page Cache
- 第二次DMA:从Socket缓冲区把数据搬到网卡发送
中间那一次CPU拷贝(从Page Cache到Socket Buffer),其实也很快——因为数据已经在内存里了,只是改个指针的事。嗯,这里要注意,CPU拷贝虽然叫「拷贝」,但在现代CPU面前,几十KB的数据就是一瞬间的事。
4.3 scatter/gather DMA
这个技术名字听着高大上,其实原理很简单。传统DMA要求数据在物理内存上是连续的。但你想啊,Page Cache里的数据可能分散在不同的物理页上。如果每次都要把数据整理成连续的再传输,那效率就低了。
Scatter/gather DMA允许DMA引擎直接从多个不连续的物理地址读取数据,拼成一个数据流发送出去。就像你手里有几张纸,不用先粘在一起,直接一张一张递给对方就行。
在sendfile场景下,scatter/gather DMA的作用是:
- Scatter(分散):从磁盘读取数据时,DMA可以把数据分散写入多个Page Cache页
- Gather(聚集):从Socket Buffer发送时,DMA可以从多个不连续的页中读取数据,组装成网络包发送
这技术对行情分发特别重要。因为行情数据往往是多个小文件或内存片段,scatter/gather让它们能一次DMA搞定,不用合并。
| 特性 | 传统DMA | Scatter/Gather DMA |
|---|---|---|
| 内存连续性要求 | 必须连续 | 不要求连续 |
| CPU参与度 | 需要CPU整理数据 | CPU几乎不参与 |
| 传输效率 | 较低 | 高 |
| 适用场景 | 小数据块 | 大数据块、碎片化数据 |
4.4 sendfile在行情分发中的应用
好了,理论讲完了,咱们来点实战。行情分发是sendfile的经典应用场景。我当年做的一个项目,需要把交易所的Level-2行情实时推送给上千个客户端。
传统做法是:
// 传统方式:read + write
char buf[65536];
while (1) {
int n = read(fd_file, buf, sizeof(buf)); // 用户态拷贝
write(fd_socket, buf, n); // 用户态拷贝
}
用sendfile改写后:
// sendfile方式:零拷贝
off_t offset = 0;
while (1) {
int n = sendfile(fd_socket, fd_file, &offset, 65536);
if (n <= 0) break;
}
代码量少了一半,性能提升了几倍。我实测过,在10Gbps网络下,sendfile的吞吐量能达到传统方式的3-5倍。延迟从微秒级降到纳秒级。
不过,sendfile也不是万能的。我遇到过几个坑:
- 大文件问题:sendfile一次最多传2GB(32位系统),64位系统没这个限制,但要注意offset类型
- 非阻塞socket:如果socket设了非阻塞,sendfile可能返回EAGAIN,需要重试
- 文件空洞:如果文件有空洞(稀疏文件),sendfile会传0字节,可能导致客户端解析出错
最后说一句,sendfile在Linux 2.6.33之后支持了TCP_CORK和TCP_NODELAY的配合。如果你要发多个小数据包,建议先设置TCP_CORK,等数据攒够了再发。这样可以减少网络包的数量,提高带宽利用率。
好了,sendfile就讲这么多。记住它的核心思想:数据搬运的事,尽量让硬件干,别让CPU掺和。下一节我们讲splice,它比sendfile更灵活,但限制也更多。
公众号:蓝海资料掘金营,微信deep3321