在系統程式設計中,掌握低階文件 I/O(open、read、write、close)是必備技能。本文解析一個經典的 C 語言文件拷貝程式,展示如何正確處理部分讀寫、中斷信號(EINTR)等邊界情況,確保數據完整性。
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main(int argc, char **argv) {
int from_fd, to_fd;
int bytes_read, bytes_write;
char buffer[BUFFER_SIZE];
char *ptr;
// 1. 參數檢查
if (argc != 3) {
fprintf(stderr, "Usage: %s fromfile tofile\n\a", argv[0]);
exit(1);
}
// 2. 開啟源檔案 (唯讀)
if ((from_fd = open(argv[1], O_RDONLY)) == -1) {
fprintf(stderr, "Open %s Error: %s\n", argv[1], strerror(errno));
exit(1);
}
// 3. 建立/開啟目標檔案 (寫入、創建、用戶讀寫權限)
if ((to_fd = open(argv[2], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR)) == -1) {
fprintf(stderr, "Open %s Error: %s\n", argv[2], strerror(errno));
exit(1);
}
// 4. 核心拷貝循環
while (bytes_read = read(from_fd, buffer, BUFFER_SIZE)) {
// 處理讀取錯誤:忽略 EINTR(被信號中斷),其他視為致命錯誤
if ((bytes_read == -1) && (errno != EINTR)) break;
else if (bytes_read > 0) {
ptr = buffer;
// 內層循環:確保所有讀取的位元組都被寫入
while (bytes_write = write(to_fd, ptr, bytes_read)) {
if ((bytes_write == -1) && (errno != EINTR)) break;
else if (bytes_write == bytes_read) break; // 寫入完成
else if (bytes_write > 0) { // 部分寫入
ptr += bytes_write;
bytes_read -= bytes_write;
}
}
if (bytes_write == -1) break; // 寫入致命錯誤
}
}
// 5. 清理資源
close(from_fd);
close(to_fd);
exit(0);
}
外層 while (bytes_read = read(...)) 處理讀取。read 可能返回:
> 0:實際讀取的位元組數(可能小於 BUFFER_SIZE,尤其在檔案尾部)。0:EOF,循環結束。-1:錯誤。若 errno == EINTR(被信號中斷),應重試;否則中止。write 不保證一次寫入所有請求的位元組(如管道、網路套接字、磁碟滿等情況)。內層循環透過 ptr 和遞減的 bytes_read 追蹤未寫入數據,直到全部寫完或發生致命錯誤。這是「經典拷貝代碼」的精髓所在。
errno使用 strerror(errno) 將錯誤碼轉為人類可讀訊息,並輸出至 stderr,符合 Unix 哲學:靜默成功,喧嘩失敗。
S_IRUSR | S_IWUSR創建新檔時明確指定擁有者讀寫權限(0600),避免預設權限過寬(如 0666 受 umask 影響)造成安全隱患。
對於單純的檔案拷貝,現代系統提供更高效的系統呼叫:
sendfile(2):內核態直接在檔案描述符間傳輸數據,避免用戶態緩衝區拷貝(零拷貝)。copy_file_range(2) (Linux 4.5+):同樣在內核態完成,支援檔案系統級優化(如 reflink)。但理解本文的 read/write 循環對於掌握系統程式基礎、處理非一般檔案(如設備、Socket)至關重要。
這段代碼雖短,卻涵蓋了系統程式設計的核心精髓:資源獲取與釋放、錯誤處理、部分 I/O 處理、信號安全。它是每個 C 程式設計師都應熟練掌握的「手藝活」。