不能。sendfile仅支持文件到socket的内核态搬运,目标fd必须是TCP等面向连接socket;它是同步阻塞调用,需配合非阻塞socket与epoll或io_uring实现异步语义。

sendfile 系统调用在 C++ 中能直接用于跨进程零拷贝文件传输吗?
不能。Linux 的 sendfile 仅支持「文件描述符到 socket 描述符」的内核态数据搬运,且要求目标 fd 必须是 socket(或某些支持的特殊设备),不支持任意两个普通文件之间复制;更关键的是,它本身是同步阻塞系统调用——所谓“异步文件传输”必须靠上层封装实现,不是 sendfile 自带能力。
常见误判:以为调用一次 sendfile 就等于“异步传输完成”。实际上,若 socket 发送缓冲区满(比如网络卡顿或对端接收慢),sendfile 会阻塞,直到有空间或超时。真正异步需配合非阻塞 socket + epoll / io_uring 驱动。
-
sendfile不拷贝用户态内存,但仍是同步 syscall,返回前可能挂起线程 - 要实现异步语义,必须把
sendfile放进非阻塞上下文,并处理EAGAIN/EWOULDBLOCK - 不能用于 UDP socket ——
sendfile仅支持面向连接的 socket(TCP、SCTP)和部分支持的 packet socket - 源文件 fd 必须是普通文件(
S_ISREG()),不支持管道、socket、/proc 文件等
如何用 epoll + sendfile 实现真正的异步文件发送循环?
核心思路:把 socket 设为非阻塞,每次调用 sendfile 后检查返回值;若返回 -1 且 errno == EAGAIN,就注册 EPOLLOUT 事件,等内核通知 socket 可写再重试;同时记录已发送偏移,避免重复搬运。
关键点不是“调用 sendfile”,而是“怎么知道什么时候能再调一次”。下面是一个最小可行状态机片段:
立即学习“C++免费学习笔记(深入)”;
struct SendfileState {
int file_fd;
int sock_fd;
off_t offset = 0;
size_t total_to_send;
bool done = false;
};
<p>// 在 epoll_wait 返回 EPOLLOUT 后调用
ssize_t try_sendfile(SendfileState& s) {
ssize_t n = sendfile(s.sock_fd, s.file_fd, &s.offset, s.total_to_send - s.offset);
if (n > 0) {
if (s.offset >= s.total_to_send) {
s.done = true;
}
return n;
} else if (n == 0) {
// 文件读完但未达预期长度(如被截断)
s.done = true;
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 需等待 EPOLLOUT,不标记 done
return 0;
} else {
// 其他错误:EINTR 可重试,EINVAL/EBADF 等应终止
return -1;
}
return n;
}</p>- 必须传入
off_t*指针让内核更新偏移,不能每次都从 0 开始 -
sendfile第四参数是单次最多搬运字节数,建议设为64*1024或128*1024,太大可能触发 TCP 小包合并问题,太小增加 syscall 次数 - 务必检查
sendfile返回值是否为 0 —— 表示源文件已到 EOF,但未必发完预期数据 - 不要在信号处理函数里调用
sendfile:它不可重入,且errno可能被覆盖
为什么用 io_uring 替代 epoll + sendfile 更适合高吞吐场景?
因为 epoll + sendfile 仍需频繁进出内核:每次 sendfile 是 syscall,每次 epoll_wait 也是 syscall,而 io_uring 允许批量提交 + 无锁轮询完成队列,把“等 socket 可写”和“发起下一轮 sendfile”合并成一次内核交互。
Linux 5.19+ 支持 IORING_OP_SENDFILE,可直接提交零拷贝发送任务,无需用户管理偏移或重试逻辑:
struct io_uring_sqe* sqe = io_uring_get_sqe(&ring); io_uring_prep_sendfile(sqe, sock_fd, file_fd, &offset, count, 0); io_uring_sqe_set_data(sqe, &my_ctx); io_uring_submit(&ring);
-
IORING_OP_SENDFILE会自动处理EAGAIN并内部重试,用户只需等完成队列回调 - 偏移量
offset必须是用户态变量地址(内核会读写),不能是栈临时变量 - 当前不支持 sendfile 到 UDP —— 和原生 syscall 限制一致
- 需要开启
IORING_SETUP_IOPOLL才能绕过中断,但仅适用于支持轮询的驱动(如 NVMe、某些网卡)
实际部署时最容易被忽略的三个细节
零拷贝不是开个开关就能生效的魔法。很多线上服务启用了 sendfile 却没提升性能,问题往往出在这些地方:
- socket 开启了
TCP_NODELAY但没关TCP_CORK:后者会延迟发送,导致sendfile数据卡在内核发送队列,看似“没发出去” - 源文件被 mmap 或其它进程
write修改:sendfile 基于 page cache,若文件内容变更而 page cache 未刷新,可能发送脏数据或触发ESTALE - 忘记调用
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)清理 page cache —— 大文件反复传输时 cache 污染严重,反而拖慢后续 IO
真正压测时,用 perf record -e 'syscalls:sys_enter_sendfile' -p $(pid) 看实际调用频次,比看文档更容易发现是不是真走零拷贝路径。


















