sendfile系统调用及示例

好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 sendfile 函数,它是一个高效的系统调用,用于在两个文件描述符之间直接传输数据,通常用于将文件内容发送到网络套接字,而无需将数据从内核空间复制到用户空间再复制回内核空间。


1. 函数介绍

sendfile 是一个 Linux 系统调用,旨在优化数据传输操作,特别是从一个文件描述符读取数据并将其写入到另一个文件描述符的场景。它最典型的用例是 Web 服务器将静态文件(如 HTML, CSS, JS, 图片)发送给客户端。

传统上,要完成这样的操作,程序需要:

  1. 调用 read() 从文件(例如磁盘)读取数据到用户空间的缓冲区。
  2. 调用 write() 将用户空间缓冲区的数据写入套接字(网络)。

这种方式涉及多次数据拷贝:磁盘 -> 内核缓冲区 -> 用户缓冲区 -> 内核套接字缓冲区 -> 网络。

sendfile 通过让内核直接在内核空间中完成数据从源文件描述符到目标文件描述符的传输,避免了用户空间和内核空间之间的数据拷贝,从而大大提高了效率,减少了 CPU 的使用。这被称为**零拷贝 **(Zero-Copy) 技术。

你可以把它想象成一个“传送带”:

  • 传统方式:东西从传送带 A 拿下来 -> 放到卡车 -> 再放到传送带 B。
  • sendfile:东西直接从传送带 A 转移到传送带 B,无需经过卡车(用户空间)。

2. 函数原型

#include <sys/sendfile.h> // 必需ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

注意: sendfile 最初是 Linux 特有的,但后来被其他一些系统(如 Solaris)采用。在某些系统上,可能需要定义特定的宏(如 _GNU_SOURCE)才能使用。


3. 功能

  • 高效传输: 在内核内部直接将数据从 in_fd(输入文件描述符)传输到 out_fd(输出文件描述符)。
  • 减少拷贝: 避免了将数据拷贝到用户空间缓冲区的步骤。
  • 减少系统调用: 一次 sendfile 调用可以完成原本需要多次 read/write 调用才能完成的工作。

4. 参数

  • int out_fd: 输出文件描述符。这是数据要被写入的目标。
    • 通常是一个**套接字 **(socket) 文件描述符,例如通过 socket()accept() 获得的。
    • 在较新的 Linux 内核(2.6.33+)中,out_fd 也可以是普通文件。
  • int in_fd: 输入文件描述符。这是数据要被读取的源。
    • 通常是一个**普通文件 **(regular file) 的文件描述符,例如通过 open() 获得的。
    • 必须支持 mmap-like 语义,因此不能是套接字、管道等。
  • off_t *offset: 一个指向 off_t 类型变量的指针,该变量指定从 in_fd 的何处开始读取数据。
    • 如果 offsetNULL:从 in_fd 当前的文件偏移量开始读取,并且读取后该偏移量会相应更新。
    • 如果 offset NULL:从 *offset 指定的字节位置开始读取。重要:在这种情况下,in_fd 的文件偏移量不会被修改,而 *offset 的值会在 sendfile 返回时被更新为新的偏移量(即 *offset = *offset + number of bytes sent)。
  • size_t count: 指定要传输的最大字节数

5. 返回值

  • 成功时: 返回实际传输的字节数(0 <= 返回值 <= count)。
    • 如果返回值为 0,通常表示在 offset 处已经到达输入文件的末尾。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EBADF 文件描述符无效,EINVAL 参数无效,ENOMEM 内存不足,EIO I/O 错误等)。

6. 相似函数,或关联函数

  • splice: 另一个零拷贝的数据传输函数,功能更强大,可以在任意两个可 pipe 的文件描述符之间传输数据。
  • tee: 用于在两个管道之间复制数据,而不消耗数据。
  • read / write: 传统的数据传输方式,涉及用户空间拷贝。
  • mmap / write: 另一种零拷贝方法,先将文件映射到内存,然后写入套接字。sendfile 通常更简单高效。
  • copy_file_range: (Linux 4.5+) 用于在两个文件描述符之间复制数据,类似于 sendfile,但功能略有不同。

7. 示例代码

示例 1:使用 sendfile 发送文件到套接字 (简化版 HTTP 服务器片段)

这个例子演示了 Web 服务器如何使用 sendfile 高效地将文件内容发送给客户端。

// 注意:这是一个简化的示例,缺少完整的 HTTP 解析、错误处理等。
// 编译时需要链接网络库: gcc -o sendfile_server sendfile_server.c#include <sys/sendfile.h>  // sendfile
#include <sys/socket.h>    // socket, bind, listen, accept, send, recv
#include <sys/stat.h>      // fstat
#include <fcntl.h>         // open
#include <netinet/in.h>    // sockaddr_in
#include <arpa/inet.h>     // inet_addr
#include <unistd.h>        // close, fstat
#include <stdio.h>         // perror, printf
#include <stdlib.h>        // exit
#include <string.h>        // strstr, strlen#define PORT 8080
#define BUFFER_SIZE 1024void send_http_response(int client_sock, const char *status_line, const char *headers) {char response[BUFFER_SIZE];int len = snprintf(response, sizeof(response), "%s\r\n%s\r\n", status_line, headers);if (len > 0 && len < sizeof(response)) {send(client_sock, response, len, 0);}
}int main() {int server_fd, new_socket;struct sockaddr_in address;int addrlen = sizeof(address);int file_fd;struct stat file_stat;off_t offset;ssize_t bytes_sent;// 1. 创建套接字文件描述符if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 2. 配置服务器地址address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 3. 绑定套接字if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}// 4. 监听if (listen(server_fd, 3) < 0) {perror("listen");close(server_fd);exit(EXIT_FAILURE);}printf("Server listening on port %d\n", PORT);// 5. 接受客户端连接 (这里简化为处理一个连接)if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");close(server_fd);exit(EXIT_FAILURE);}printf("Client connected.\n");// 6. 简单读取客户端请求 (假设是 GET / HTTP/1.1)char buffer[BUFFER_SIZE] = {0};read(new_socket, buffer, BUFFER_SIZE - 1);printf("Received request:\n%s\n", buffer);// 7. 简单解析,检查是否请求根路径 "/"if (strstr(buffer, "GET / ") != NULL) {const char *filename = "index.html"; // 假设服务器根目录下有 index.html// 8. 打开要发送的文件file_fd = open(filename, O_RDONLY);if (file_fd == -1) {perror("open file");const char *not_found = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";send(new_socket, not_found, strlen(not_found), 0);close(new_socket);close(server_fd);exit(EXIT_FAILURE);}// 9. 获取文件状态 (主要是大小)if (fstat(file_fd, &file_stat) == -1) {perror("fstat");close(file_fd);close(new_socket);close(server_fd);exit(EXIT_FAILURE);}// 10. 发送 HTTP 响应头char headers[BUFFER_SIZE];snprintf(headers, sizeof(headers),"HTTP/1.1 200 OK\r\n""Content-Type: text/html\r\n""Content-Length: %ld\r\n""\r\n",(long)file_stat.st_size);send(new_socket, headers, strlen(headers), 0);printf("Sent HTTP headers.\n");// 11. 使用 sendfile 发送文件内容offset = 0;ssize_t remaining = file_stat.st_size;while (remaining > 0) {// sendfile 可能不会一次发送完所有数据bytes_sent = sendfile(new_socket, file_fd, &offset, remaining);if (bytes_sent == -1) {perror("sendfile");break;}remaining -= bytes_sent;printf("Sent %zd bytes, %zd bytes remaining.\n", bytes_sent, remaining);}if (remaining == 0) {printf("File sent successfully using sendfile.\n");} else {printf("Error or incomplete transfer.\n");}close(file_fd);} else {// 处理其他请求或发送 404const char *not_found = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";send(new_socket, not_found, strlen(not_found), 0);}// 12. 关闭连接和服务器套接字close(new_socket);close(server_fd);printf("Connection closed.\n");return 0;
}

代码解释:

  1. 创建、绑定、监听 TCP 套接字,建立一个简单的服务器。
  2. 接受一个客户端连接。
  3. 读取客户端的 HTTP 请求(简化处理)。
  4. 检查请求是否为 GET /
  5. 如果是,打开服务器上的 index.html 文件。
  6. 使用 fstat 获取文件大小。
  7. 构造并发送 HTTP 响应头(包含 Content-Length)。
  8. 关键步骤: 使用 sendfile 将文件内容发送到客户端套接字。
    • new_socket: 输出文件描述符(套接字)。
    • file_fd: 输入文件描述符(文件)。
    • &offset: 指向 off_t 变量的指针,用于跟踪文件读取位置。初始为 0。
    • file_stat.st_size: 要传输的总字节数。
  9. sendfile 可能不会一次性传输所有请求的字节,因此使用 while 循环确保整个文件都被发送。
  10. 在循环中更新 remaining 字节数。
  11. 最后关闭文件和套接字。
示例 2:使用 sendfile 复制文件 (out_fd 为普通文件)

这个例子演示了在较新内核(Linux 2.6.33+)中,如何使用 sendfile 在两个普通文件之间复制数据。

#define _GNU_SOURCE // 启用 GNU 扩展以使用 sendfile 的新特性
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main() {const char *source_filename = "source_file.txt";const char *dest_filename = "dest_file_copy.txt";int src_fd, dest_fd;struct stat stat_buf;ssize_t total_bytes = 0;ssize_t bytes_sent;off_t offset = 0;// 1. 创建并写入源文件src_fd = open(source_filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);if (src_fd == -1) {perror("open source file for writing");exit(EXIT_FAILURE);}const char *data = "This is the content of the source file.\nIt has multiple lines.\n";if (write(src_fd, data, strlen(data)) == -1) {perror("write to source file");close(src_fd);exit(EXIT_FAILURE);}close(src_fd);printf("Created source file '%s'.\n", source_filename);// 2. 打开源文件 (只读)src_fd = open(source_filename, O_RDONLY);if (src_fd == -1) {perror("open source file for reading");exit(EXIT_FAILURE);}// 3. 获取源文件大小if (fstat(src_fd, &stat_buf) == -1) {perror("fstat source file");close(src_fd);exit(EXIT_FAILURE);}// 4. 创建/打开目标文件 (写入/创建/截断)dest_fd = open(dest_filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);if (dest_fd == -1) {perror("open destination file");close(src_fd);exit(EXIT_FAILURE);}printf("Copying '%s' to '%s' using sendfile...\n", source_filename, dest_filename);// 5. 使用 sendfile 复制数据// 注意:在旧内核上,这可能会失败,因为 out_fd 不是套接字while (total_bytes < stat_buf.st_size) {// 计算本次要发送的字节数 (防止溢出)size_t count = stat_buf.st_size - total_bytes;if (count > 0x7ffff000) { // sendfile 一次传输上限 (约 2GB)count = 0x7ffff000;}bytes_sent = sendfile(dest_fd, src_fd, &offset, count);if (bytes_sent == -1) {perror("sendfile");// 可能是内核不支持普通文件作为 out_fdif (errno == EINVAL) {printf("Error: sendfile likely doesn't support copying between regular files on this system/kernel.\n");printf("Consider using splice or standard read/write loop instead.\n");}close(src_fd);close(dest_fd);exit(EXIT_FAILURE);}total_bytes += bytes_sent;printf("Copied %zd bytes in this call, total: %zd/%ld\n",bytes_sent, total_bytes, (long)stat_buf.st_size);}printf("File copy completed successfully. %zd bytes copied.\n", total_bytes);// 6. 清理close(src_fd);close(dest_fd);return 0;
}

代码解释:

  1. 首先创建一个名为 source_file.txt 的源文件并写入一些内容。
  2. 以只读模式打开源文件,并使用 fstat 获取其大小。
  3. 以写入、创建、截断模式打开(或创建)目标文件 dest_file_copy.txt
  4. 进入 while 循环,使用 sendfile(dest_fd, src_fd, &offset, count) 将数据从源文件传输到目标文件。
  5. 关键: out_fd 是目标文件的描述符,这需要 Linux 内核 2.6.33 或更高版本的支持。在不支持的旧内核上,sendfile 会返回 -1,并将 errno 设置为 EINVAL
  6. 循环直到复制完整个文件。
  7. 最后关闭两个文件描述符。

重要提示与注意事项:

  1. 零拷贝优势: sendfile 的主要优势在于减少了数据在内核空间和用户空间之间的拷贝次数,降低了 CPU 开销,提高了吞吐量。
  2. 适用范围:
    • 经典用法: in_fd 是文件,out_fd 是套接字。这在所有支持 sendfile 的 Linux 版本上都有效。
    • 扩展用法: in_fdout_fd 都可以是普通文件(Linux 2.6.33+)或一个文件一个套接字。
  3. 非阻塞 I/O: sendfile 在处理非阻塞套接字时,如果套接字缓冲区已满,sendfile 可能会传输部分数据并返回,或者根据平台行为阻塞或返回错误(如 EAGAIN)。需要正确处理返回值。
  4. 传输限制: 一次 sendfile 调用传输的字节数可能有限制(历史上是 0x7ffff000 字节)。对于大文件,可能需要循环调用。
  5. offset 参数: 理解 offsetNULL 和非 NULL 时的行为差异非常重要。使用非 NULL offset 可以实现线程安全的文件传输,因为不修改文件自身的偏移量。
  6. 错误处理: 始终检查 sendfile 的返回值。除了常见的错误码,还要特别注意 EINVAL,它可能表示不支持的操作(如旧内核上文件到文件的复制)。
  7. 现代替代: splicecopy_file_rangesendfile 的现代替代或补充,提供了更灵活的数据传输能力。

总结:

sendfile 是一个强大的系统调用,通过利用内核的零拷贝机制,显著提高了文件到套接字(以及文件到文件)的数据传输效率。理解其参数(特别是 offset)和返回值对于正确使用它至关重要。在编写高性能网络服务器或需要高效文件操作的程序时,sendfile 是一个非常有价值的工具。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/90764.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/90764.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构习题--删除排序数组中的重复项

数据结构习题–删除排序数组中的重复项 给你一个 非严格递增排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 方法&…

Docker的容器设置随Docker的启动而启动

原因也比较简单&#xff0c;在docker run 的时候没有设置–restartalways参数。 容器启动时&#xff0c;需要增加参数 –restartalways no - 容器退出时&#xff0c;不重启容器&#xff1b; on-failure - 只有在非0状态退出时才从新启动容器&#xff1b; always - 无论退出状态…

JWT安全机制与最佳实践详解

JWT&#xff08;JSON Web Token&#xff09; 是一种开放标准&#xff08;RFC 7519&#xff09;&#xff0c;用于在各方之间安全地传输信息作为紧凑且自包含的 JSON 对象。它被广泛用于身份验证&#xff08;Authentication&#xff09;和授权&#xff08;Authorization&#xff…

如何解决pip安装报错ModuleNotFoundError: No module named ‘ipython’问题

【Python系列Bug修复PyCharm控制台pip install报错】如何解决pip安装报错ModuleNotFoundError: No module named ‘ipython’问题 摘要 在开发过程中&#xff0c;我们常常会遇到pip install报错的问题&#xff0c;其中一个常见的报错是 ModuleNotFoundError: No module named…

从三维Coulomb势到二维对数势的下降法推导

题目 问题 7. 应用 9.1.4 小节描述的下降法&#xff0c;但针对二维的拉普拉斯方程&#xff0c;并从三维的 Coulomb 势出发 KaTeX parse error: Invalid delimiter: {"type":"ordgroup","mode":"math","loc":{"lexer&qu…

直播一体机技术方案解析:基于RK3588S的硬件架构特性​

硬件配置​​主控平台​​▸ 搭载瑞芯微RK3588S旗舰处理器&#xff08;四核A762.4GHz 四核A55&#xff09;▸ 集成ARM Mali-G610 MP4 GPU 6TOPS算力NPU▸ 双通道LPDDR5内存 UFS3.1存储组合​​专用加速单元​​→ 板载视频采集模块&#xff1a;支持4K60fps HDMI环出采集→ 集…

【氮化镓】GaN取代GaAs作为空间激光无线能量传输光伏转换器材料

2025年7月1日,西班牙圣地亚哥-德孔波斯特拉大学的Javier F. Lozano等人在《Optics and Laser Technology》期刊发表了题为《Gallium nitride: a strong candidate to replace GaAs as base material for optical photovoltaic converters in space exploration》的文章,基于T…

直播美颜SDK动态贴纸模块开发指南:从人脸关键点识别到3D贴合

很多美颜技术开发者好奇&#xff0c;如何在直播美颜SDK中实现一个高质量的动态贴纸模块&#xff1f;这不是简单地“贴图贴脸”&#xff0c;而是一个融合人脸关键点识别、实时渲染、贴纸驱动逻辑、3D骨骼动画与跨平台性能优化的系统工程。今天&#xff0c;就让我们从底层技术出发…

学习游戏制作记录(剑投掷技能)7.26

1.实现瞄准状态和接剑状态准备好瞄准动画&#xff0c;投掷动画和接剑动画&#xff0c;并设置参数AimSword和CatchSword投掷动画在瞄准动画后&#xff0c;瞄准结束后才能投掷创建PlayerAimSwordState脚本和PlayerCatchSwordState脚本并在Player中初始化&#xff1a;PlayerAimSwo…

【c++】问答系统代码改进解析:新增日志系统提升可维护性——关于我用AI编写了一个聊天机器人……(14)

在软件开发中&#xff0c;代码的迭代优化往往从提升可维护性、可追踪性入手。本文将详细解析新增的日志系统改进&#xff0c;以及这些改进如何提升系统的实用性和可调试性。一、代码整体背景代码实现了一个基于 TF-IDF 算法的问答系统&#xff0c;核心功能包括&#xff1a;加载…

visual studio2022编译unreal engine5.4.4源码

UE5系列文章目录 文章目录 UE5系列文章目录 前言 一、ue5官网 二.编译源码中遇到的问题 前言 一、ue5官网 UE5官网 UE5源码下载地址 这样虽然下载比较快,但是不能进行代码git管理,以后如何虚幻官方有大的版本变动需要重新下载源码,所以我们还是最好需要visual studio2022…

vulhub Earth靶场攻略

靶场下载 下载链接&#xff1a;https://download.vulnhub.com/theplanets/Earth.ova 靶场使用 将压缩包解压到一个文件夹中&#xff0c;右键&#xff0c;用虚拟机打开&#xff0c;就创建成功了&#xff0c;然后启动虚拟机&#xff1a; 这时候靶场已经启动了&#xff0c;咱们现…

Python训练Day24

浙大疏锦行 元组可迭代对象os模块

Spring核心:Bean生命周期、外部化配置与组件扫描深度解析

Bean生命周期 说明 程序中的每个对象都有生命周期&#xff0c;对象的创建、初始化、应用、销毁的整个过程称之为对象的生命周期&#xff1b; 在对象创建以后需要初始化&#xff0c;应用完成以后需要销毁时执行的一些方法&#xff0c;可以称之为是生命周期方法&#xff1b; 在sp…

日语学习-日语知识点小记-进阶-JLPT-真题训练-N1阶段(1):2017年12月-JLPT-N1

日语学习-日语知识点小记-进阶-JLPT-真题训练-N1阶段&#xff08;1&#xff09;&#xff1a;2017年12月-JLPT-N1 1、前言&#xff08;1&#xff09;情况说明&#xff08;2&#xff09;工程师的信仰&#xff08;3&#xff09;真题训练2、真题-2017年12月-JLPT-N1&#xff08;1&a…

(一)使用 LangChain 从零开始构建 RAG 系统|RAG From Scratch

RAG 的主要动机 大模型训练的时候虽然使用了庞大的世界数据&#xff0c;但是并没有涵盖用户关心的所有数据&#xff0c; 其预训练令牌&#xff08;token&#xff09;数量虽大但相对这些数据仍有限。另外大模型输入的上下文窗口越来越大&#xff0c;从几千个token到几万个token,…

OpenCV学习探秘之一 :了解opencv技术及架构解析、数据结构与内存管理​等基础

​一、OpenCV概述与技术演进​ 1.1技术历史​ OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是由Intel于1999年发起创建的开源计算机视觉库&#xff0c;后来交由OpenCV开源社区维护&#xff0c;旨在为计算机视觉应用提供通用基础设施。经历20余年发展&…

什么是JUC

摘要 Java并发工具包JUC是JDK5.0引入的重要并发编程工具&#xff0c;提供了更高级、灵活的并发控制机制。JUC包含锁与同步器&#xff08;如ReentrantLock、Semaphore等&#xff09;、线程安全队列&#xff08;BlockingQueue&#xff09;、原子变量&#xff08;AtomicInteger等…

零基础学后端-PHP语言(第二期-PHP基础语法)(通过php内置服务器运行php文件)

经过上期的配置&#xff0c;我们已经有了php的开发环境&#xff0c;编辑器我们继续使用VScode&#xff0c;如果是新来的朋友可以看这期文章来配置VScode 零基础学前端-传统前端开发&#xff08;第一期-开发软件介绍与本系列目标&#xff09;&#xff08;VScode安装教程&#x…

扩散模型逆向过程详解:如何从噪声中恢复数据?

在扩散模型中&#xff0c;逆向过程的目标是从噪声数据逐步恢复出原始数据。本文将详细解析逆向条件分布 q(zt−1∣zt,x)q(\mathbf{z}_{t-1} \mid \mathbf{z}_t, \mathbf{x})q(zt−1​∣zt​,x)的推导过程&#xff0c;揭示扩散模型如何通过高斯分布实现数据重建。1. 核心问题 在…