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
英文地址,請注明出處:http://en.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. 核心問題 在…