首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

APUE 讀書筆記:標準 I/O 庫 (下) —— 二進制 I/O、流定位、格式化 I/O、實現細節與臨時檔案

     編輯:關於C語言

前言:《UNIX 環境高級程式設計》(APUE) 第 5 章標準 I/O 庫下半部核心內容

本文涵蓋:**標準 I/O 效能基準測試分析、二進制 I/O (fread/fwrite)、流定位 (ftell/fseek vs fgetpos/fsetpos)、格式化 I/O 族函數、標準 I/O 實現細節 (FILE 結構、文件描述符關聯)、臨時檔案建立 (tmpnam/tmpfile/tempnam)**。配合原書圖表數據深度解讀。


一、標準 I/O 效能基準測試:為什麼 fgetc 比 read(1 byte) 快 100 倍?

測試場景

  • 複製一個約 1.5MB 的檔案
  • 四種實現:系統呼叫 `read/write` (BUFSIZ=1/4096)、標準 I/O `getc/putc`、標準 I/O `fgets/fputs`
  • 環境:早期 UNIX 系統 (原書數據),結構性結論放之四海而皆準

關鍵數據表 (原書 Table 5-1 摘要)

版本用戶 CPU系統 CPU時鐘時間說明
read/write BUFSIZ=1高高**最慢**300 萬次系統呼叫
read/write BUFSIZ=4096低低**最快 (基準)**360 次系統呼叫
getc/putc (巨集)中高低~2x 基準300 萬次巨集展開,無系統呼叫開銷
fgetc/fputc (函數)中高低~2x 基準300 萬次函數呼叫
fgets/fputs中低~1x 基準3 萬次函數呼叫 (按行),memcpy 優化

核心結論深度解讀

  1. 系統呼叫開銷是殺手:`read/write` BUFSIZ=1 版本執行 **300 萬次系統呼叫**,每次陷入核心、上下文切換、檢查權限、複製數據。標準 I/O 透過**用戶空間緩衝**,將 300 萬次縮減為 **360 次** (BUFSIZ=4096),系統 CPU 時間大幅下降。
  2. 用戶 CPU 差異源於函數/巨集呼叫次數:`getc/putc` 通常實現為巨集,每字元一次展開;`fgetc/fputc` 為函數呼叫;`fgets/fputs` 按行操作,呼叫次數減少 100 倍。巨集雖無函數呼叫開銷,但程式碼膨脹導致指令快取未命中,實測與函數版本接近。
  3. fgets/fputs 勝在「批次處理」:每次處理一行 (平均 50 字元),呼叫次數僅 3 萬次。若內部用 `memcpy` (常為手寫組合語言優化),效能逼近 `read/write` 基準版。
  4. 程式碼體積權衡:`getc/putc` 巨集版本 `.text` 段最大 (巨集展開),`fgetc/fputc` 函數版本最小。

現代視角補充

  • 現代 CPU 快取、分支預測、超標量流水線放大了「呼叫次數」的影響。
  • `stdio` 實現常用 **鎖自由緩衝區** 或 **執行緒本地緩衝** 進一步降低同步開銷 (glibc `_IO_FILE` 複雜實現)。
  • **建議**:大量字元處理用 `fread/fwrite` 或 `fgets/fputs`;格式化輸出用 `fprintf`;避免逐字元 `getc/putc` 迴圈。

二、二進制 I/O:fread / fwrite

標準 I/O 為處理**非文本資料** (結構體、陣列、二進制檔案) 提供:

#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
// 返回:成功讀寫的**項目數** (nmemb),非位元組數

兩種經典用法

// 1. 讀寫基本型別陣列
float data[100];
fwrite(&data[2], sizeof(float), 4, fp);  // 寫入第 3~6 個元素 (索引 2,3,4,5)

// 2. 讀寫結構體 (整體序列化)
struct Record {
    int id;
    char name[32];
    double score;
} rec;
fwrite(&rec, sizeof(rec), 1, fp);  // size=sizeof(rec), nmemb=1

關鍵注意事項

  • 返回值語義:返回**完整項目數**。讀取時 `< nmemb` 表示 EOF 或錯誤,**必須配合 `feof()` / `ferror()` 區分**;寫入時 `< nmemb` 直接視為錯誤。
  • 可移植性陷阱:二進制檔案**僅適用於同架構、同編譯器、同編譯選項**的系統間交換。原因:
    1. 結構體對齊/填充位元組不同 (`#pragma pack`, `-fpack-struct`)
    2. 基本型別大小不同 (如 `long` 32/64 位)
    3. 位元序不同 (大端/小端)
    4. 浮點數表示不同 (IEEE 754 vs 非標準)
  • 跨平台方案:使用 **序列化協定** (Protocol Buffers, MessagePack, JSON, CBOR, XDR) 或手動逐欄位讀寫 (網路位元序 `htonl/ntohl`)。

三、流定位:隨機存取的兩套 API

傳統 API:ftell / fseek / rewind (假設位置可用 long 表示)

long ftell(FILE *fp);           // 返回相對檔案開頭的位元組偏移量,錯誤 -1L
int fseek(FILE *fp, long offset, int whence); // whence: SEEK_SET/CUR/END
void rewind(FILE *fp);          // 等同 fseek(fp, 0, SEEK_SET) + 清除錯誤標誌
  • 二進制檔案:`offset` 為位元組偏移,`whence` 任意組合有效。
  • 文本檔案:**僅支援 `whence=SEEK_SET` 且 `offset=0` 或 `offset=ftell()` 返回值**。因為文本模式下換行符轉換 (`\n` ⇄ `\r\n`) 導致邏輯位置 ≠ 物理位元組偏移。

現代 API:fgetpos / fsetpos (POSIX.1 / C99,支援大檔案)

int fgetpos(FILE *fp, fpos_t *pos);  // 將當前位置存入不透明物件 pos
int fsetpos(FILE *fp, const fpos_t *pos); // 還原位置
// fpos_t 可為結構體/長整數,足以表示 64 位甚至更大偏移
  • 可移植性:非 UNIX 系統 (如 Windows、大型主機) 文件位置可能不以簡單位元組偏移表示,**跨平台程式必須使用 `fgetpos/fsetpos`**。
  • 大檔案支援 (LFS):`fpos_t` 通常為 64 位,配合 `_FILE_OFFSET_BITS=64` 編譯選項,支援 >2GB 檔案定位。

四、格式化 I/O:printf / scanf 族函數完整圖譜

輸出族

int printf(const char *format, ...);           // → stdout
int fprintf(FILE *fp, const char *format, ...); // → 指定流
int sprintf(char *buf, const char *format, ...); // → 字串緩衝區 (危險:無長度檢查)
int snprintf(char *buf, size_t n, const char *format, ...); // 安全版 (C99)

// 可變參數列表版 (v-前綴)
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *fp, const char *format, va_list ap);
int vsprintf(char *buf, const char *format, va_list ap);
int vsnprintf(char *buf, size_t n, const char *format, va_list ap);

關鍵點:`sprintf` **極易造成緩衝區溢位**,**嚴禁使用**,永遠用 `snprintf`。返回值為**寫入字元數 (不含結尾 `\0`)**,截斷時返回**本應寫入的總字元數** (C99 規範)。

輸入族

int scanf(const char *format, ...);           // ← stdin
int fscanf(FILE *fp, const char *format, ...); // ← 指定流
int sscanf(const char *buf, const char *format, ...); // ← 字串

// 返回值:成功賦值的**項目數**,EOF 表示輸入失敗/結束
// 掃描集: %[^\\n] 讀取整行, %*d 跳過整數不賦值

格式化字串核心規格 (必背)

%[flags][width][.precision][length]specifier
flags:  - (左對齊) + (正負號) 空格 (正數前空格) # (替代形式) 0 (前導零)
width:  最小欄寬 (可為 * 由參數指定)
precision: .n  小數位數/字串最大長度 (可為 *)
length: hh (char) h (short) l (long/double) ll (long long) L (long double) j (intmax_t) z (size_t) t (ptrdiff_t)
specifier: d/i (有號十進制) u (無號十進制) o (八進制) x/X (十六進制) f/F (定點) e/E (科學) g/G (自動) a/A (十六進制浮點) c (字元) s (字串) p (指標) n (寫入已輸出字元數) % (字面 %)

五、標準 I/O 實現細節:FILE 結構與文件描述符關聯

核心關係圖

用戶程式
    │
    ▼
FILE *fp (指向 _IO_FILE / struct __sFILE 等實現結構)
    │
    ├── int _fileno (文件描述符 fd) ←→ 核心檔案表 → inode → 磁碟
    ├── unsigned char *_IO_buf_base (緩衝區基址)
    ├── size_t _IO_buf_size (緩衝區大小)
    ├── unsigned char *_IO_read_ptr / _IO_write_ptr (讀/寫指標)
    ├── _IO_read_end / _IO_write_end (緩衝區邊界)
    ├── short _flags (緩衝模式: _IO_UNBUFFERED, _IO_LINE_BUF, _IO_NO_READS...)
    └── 鎖 (執行緒同步)
        │
        ▼
標準 I/O 函數 (fread, fprintf, fseek...)
    │
    ▼ (緩衝區滿/刷新/無緩衝/行緩衝遇換行)
系統呼叫 (read, write, lseek, close)

關鍵函數:fileno()

int fileno(FILE *fp);  // 返回關聯的文件描述符
// 用途: 需要混用系統 IO 與標準 IO 時 (如: 將 socket fd 封裝為 FILE*, 或對 fd 進行 fcntl/epoll 操作)

緩衝模式控制:setvbuf / setbuf

int setvbuf(FILE *fp, char *buf, int mode, size_t size);
// mode: _IOFBF (全緩衝) | _IOLBF (行緩衝) | _IONBF (無緩衝)
// buf=NULL, size=0 → 標準庫自動分配 (通常 BUFSIZ=8192 或 st_blksize)
// 必須在**打開流後、任何操作前**呼叫

六、臨時檔案:安全建立與自動清理

三大標準函數對比

函數返回值特點安全性適用場景
tmpnam(char *s)路徑字串指標只產生**唯一路徑名**,**不建立檔案**。需自行 `fopen`。最多 `TMP_MAX` 次調用。低 (TOCTOU 競態:產生名→開檔間隙可被搶佔)需預知路徑名、跨進程共享臨時檔
tmpfile(void)FILE* (二進制 wb+)**原子性建立並打開**唯一臨時檔。**關閉或程式結束時自動刪除**。無路徑名洩露。**最高** (推薦)大多數臨時資料處理、單進程內部使用
tempnam(const char *dir, const char *pfx)路徑字串指標 (malloc 分配,需 free)可指定目錄 (`dir`/`TMPDIR`/`P_tmpdir`/`/tmp`) 與前綴。不建立檔案。低 (同 tmpnam),且需記得 free 返回指標需控制臨時檔存放位置、命名規則

最佳實踐:mkstemp / mkdtemp (POSIX,更安全)

// mkstemp: 原子性建立唯一檔案並返回 fd (O_RDWR | O_CREAT | O_EXCL, 0600)
char template[] = "/tmp/myapp_XXXXXX";
int fd = mkstemp(template);  // template 被修改為實際檔名
FILE *fp = fdopen(fd, "w+");
// 使用完畢:
fclose(fp);  // 或 close(fd) + unlink(template)

// mkdtemp: 建立唯一目錄
char dtemplate[] = "/tmp/myapp_XXXXXX";
char *dir = mkdtemp(dtemplate);
  • `XXXXXX` 會被替換為隨機字元,**保證唯一性**。
  • 權限 `0600` (檔) / `0700` (目錄) **僅擁有者可讀寫**,防止資訊洩露。
  • **無 TOCTOU 競態**,是現代程式建立臨時資源的標準方式。

總結: APUE 第 5 章下半部揭示了標準 I/O 庫「用戶空間緩衝」的本質:**以空間換時間,以複雜度換通用性**。理解 `fread/fwrite` 的二進制語義限制、掌握 `fgetpos/fsetpos` 的大檔案/跨平台優勢、警惕 `sprintf` 的緩衝區溢位、**優先使用 `tmpfile`/`mkstemp` 建立臨時資源**,是寫出健壯、高效、可移植 C 程式的關鍵。建議配合 `strace -e trace=read,write,lseek ./your_program` 觀察實際系統呼叫行為,驗證緩衝策略效果。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved