[apue] 標準 I/O 庫那些事兒

前言

標準 IO 庫自 1975 年誕生以來,至今接近 50 年了,令人驚訝的是,這期間只對它做了非常小的修改。除了耳熟能詳的 printf/scanf,回過頭來對它做個全方位的審視,看看到底優秀在哪裡。

打開關閉

要想使用 IO 流就必需打開它們。三個例外是標準輸入 stdin、標準輸出 stdout、標準錯誤 stderr,它們在進入 main 時就準備好了,可以直接使用,與之對應的文件描述符分別是 STDIN_FILENO / STDOUT_FILENO / STDERR_FILENO。除此之外的流需要打開才能使用:

FILE* fopen(const char * restrict path, const char * restrict mode);
FILE* fdopen(int fildes, const char *mode);
FILE* freopen(const char *path, const char *mode, FILE *stream);
FILE* fmemopen(void *restrict *buf, size_t size, const char * restrict mode);
  • fopen 用於打開指定的文件作為流
  • fdopen 用於打開已有的文件描述符作為流
  • freopen 用於在指定的流上打開指定的文件
  • fmemopen 用於打開已有的內存作為流

fopen

大部分打開操作都需要提供 mode 參數,它主要由 r/w/a/b/+ 字符組成,相關的組合與 open 的 oflag 參數對應關係如下:

mode oflag
r O_RDONLY
r+ O_RDWR
w O_WRONLY | O_CREAT | O_TRUNC
w+ O_RDWR | O_CREAT | O_TRUNC
a O_WRONLY | O_CREAT | O_APPEND
a+ O_RDWR | O_CREAT | O_APPEND

其中 b 表示按二進制數據處理,不提供時按文本數據處理,不過 unix like 的文件不區分二進制數據與文本數據,加不加沒什麼區別,所以上面沒有列出。

fdopen

fdopen 提供了一種便利,將已有的 fd 封裝在 FILE* 中,特別當描述符是通過接口傳遞進來時就尤為有用了。fdopen 的一個問題是 fd 本身的讀寫標誌要與 mode 參數相容,否則會打開失敗,下面的程序用來驗證 mode 與 oflags 的相容關係:

#include "../apue.h"
#include <wchar.h> 

int main (int argc, char* argv[])
{
  if (argc < 4)
    err_sys ("Usage: fdopen_t path type1 type2"); 

  char const* path = argv[1]; 
  char const* type1  = argv[2]; 
  char const* type2 = argv[3]; 
  int flags = 0; 
  if (strchr (type1, 'r') != 0)
  {
    if (strchr (type1, '+') != 0)
      flags |= O_RDWR; 
    else 
      flags |= O_RDONLY;
  }
  else if (strchr (type1, 'w') != 0)
  {
    flags |= O_TRUNC; 
    if (strchr (type1, '+') != 0)
      flags |= O_RDWR; 
    else 
      flags |= O_WRONLY;
  }
  else if (strchr (type1, 'a') != 0)
  {
    flags |= O_APPEND; 
    if (strchr (type1, '+') != 0)
      flags |= O_RDWR; 
    else 
      flags |= O_WRONLY;
  }

  int fd = open (path, flags, 0777);  
  if (fd == 0)
    err_sys ("fopen failed"); 

  printf ("(%d) open type %s, type %s ", getpid (), type1, type2);
  FILE* fp = fdopen (fd, type2); 
  if (fp == 0)
    err_sys ("fdopen failed"); 

  printf ("OK\n"); 
  fclose (fp); 
  return 0; 
}

程序接收 3 個參數,分別是待測試文件、oflags 和 mode,因 oflags 為二進制不方便直接傳遞,這裡借用 mode 的 r/w/a 在內部做個轉換。

使用下面的腳本驅動:

#! /bin/sh

oflags=("r" "w" "a" "r+" "w+" "a+")
modes=("r" "r+" "w" "w+" "a" "a+")
for oflag in ${oflags[@]}
do
    for mode in ${modes[@]}
    do
        ./fdopen_t abc.txt ${oflag} ${mode}
    done
done

下面是程序輸出:

$ sh fdopen_t.sh
(62061) open type r, type r OK
(62062) open type r, type r+ fdopen failed: Invalid argument
(62063) open type r, type w fdopen failed: Invalid argument
(62064) open type r, type w+ fdopen failed: Invalid argument
(62065) open type r, type a fdopen failed: Invalid argument
(62066) open type r, type a+ fdopen failed: Invalid argument
(62067) open type w, type r fdopen failed: Invalid argument
(62068) open type w, type r+ fdopen failed: Invalid argument
(62069) open type w, type w OK
(62070) open type w, type w+ fdopen failed: Invalid argument
(62071) open type w, type a OK
(62072) open type w, type a+ fdopen failed: Invalid argument
(62073) open type a, type r fdopen failed: Invalid argument
(62074) open type a, type r+ fdopen failed: Invalid argument
(62075) open type a, type w OK
(62076) open type a, type w+ fdopen failed: Invalid argument
(62077) open type a, type a OK
(62078) open type a, type a+ fdopen failed: Invalid argument
(62079) open type r+, type r OK
(62080) open type r+, type r+ OK
(62081) open type r+, type w OK
(62082) open type r+, type w+ OK
(62083) open type r+, type a OK
(62084) open type r+, type a+ OK
(62085) open type w+, type r OK
(62086) open type w+, type r+ OK
(62087) open type w+, type w OK
(62088) open type w+, type w+ OK
(62089) open type w+, type a OK
(62090) open type w+, type a+ OK
(62091) open type a+, type r OK
(62092) open type a+, type r+ OK
(62093) open type a+, type w OK
(62094) open type a+, type w+ OK
(62095) open type a+, type a OK
(62096) open type a+, type a+ OK

總結一下:

mode oflags
r O_RDONLY/O_RDWR
w O_WRONLY/O_RDWR
a O_WRONLY/O_RDWR
r+/w+/a+ O_RDWR

其中與創建文件相關的選項均會失效,如 w 的 O_TRUNC 與 a 的 O_APPEND,也就是說 fdopen 指定 mode a 打開成功的流可能完全沒有 append 能力;指定 w 打開成功的流也可能壓根沒有 truncate,感興趣的讀者可以修改上面的 demo 驗證。

fileno

fdopen 無意間已經展示了如何將 fd 轉換為 FILE*,反過來也可以獲取 FILE* 底層的 fd,這就需要用到另外一個接口了:

int fileno(FILE *stream);

freopen

freopen 一般用於將一個指定的文件打開為一個預定義的流,在使用方式上有些類似 dup2:

  • 如果 stream 代表的流已經打開,則先關閉
  • 打開成功後返回 stream

如果想在程序中將 stdin/stdout/stderr 重定向到文件,使用 freopen 將非常方便,不然的話就需要 fopen 一個新流,並使用 fprintf / fputs / fscanf / fgets … 等帶一個流參數的版本在新流上執行讀寫工作。如果已有大量的這類函數調用,重構起來會非常頭疼,freopen 很好的解決了這個痛點。

不過無法在指定的流上使用特定的 fd,這是因為 freopen 只接受 path 作為參數,沒有名為 fdreopen 這樣的東東。freopen 會清除流的 eof、error 狀態及定向和緩衝方式,這些概念請參考後面的小節。

fmemopen

fmemopen 是新加入的接口,用於在一塊內存上執行 IO 操作,如果給 buf 參數 NULL,則它會自動分配 size 大小的內存,並在關閉流時自動釋放內存。

fclose

fclose 用於關閉一個流,關閉流會自動關閉底層的 fd,使用 fdopen 打開的流也是如此。

int fclose(FILE *stream);

進程退出時會自動關閉所有打開的流。

定向 (orientation)

除了針對 ANSI 字符集,標準 IO 庫還可以處理國際字符集,此時一個字符由多個位元組組成,稱為寬字符集,ANSI 單字符集也稱為窄字符集。寬字符集中一般使用 wchar_t 代替 char 作為輸入輸出參數,下面是寬窄字符集接口對應關係:

窄字符集 寬字符集
printf/fprintf/sprintf/snprintf/vprintf wprintf/fwprintf/swprintf/vwprintf
scanf/fscanf/sscanf/vscanf wscanf/fwscanf/swscanf/vwscanf
getc/fgetc/getchar/ungetc getwc/fgetwc/getwchar/ungetwc
putc/fputc/putchar putwc/fputwc/putwchar
gets/fgets fgetws
puts/fputs fputws

主要區別是增加了一個 w 標誌。由於寬窄字符集主要影響的是字符串操作,上表幾乎列出了所有的標準庫與字符/字符串相關的接口。接口不是一一對應的關係,例如沒有 getws/putws 這種接口,一個可能的原因是 gets/puts 本身已不建議使用,所以也沒有必要增加對應的寬字符接口;另外也沒有 swnprintf 或 snwprintf 這種接口,可能是考慮到類似 utf-8 這種變長多位元組字符集不好計算字符數吧。

下面才是重點,一個流只能操作一種寬度的字符集,如果已經操作過寬字符集,就不能再操作窄字符集,反之亦然,這就是流的定向。除了調用上面的接口來隱式定向外,還可以通過接口顯示定向:

int fwide(FILE *stream, int mode);

fwide 只有在流未定向時才能起作用,對一個已定向的流調用它不會改變流的定向,mode 含義如下:

  • mode < 0:窄字符集定向
  • mode > 0:寬字符集定向
  • mode == 0:不對流進行定向,僅返迴流的當前定向,返回值含義同參數

下面的程序用來驗證 fwide 的上述特性:

#include "../apue.h"
#include <wchar.h> 

void do_fwide (FILE* fp, int wide)
{
  if (wide > 0)
      fwprintf (fp, L"do fwide %d\n", wide); 
  else
      fprintf (fp, "do fwide %d\n", wide); 
}

/**
 *@param: wide
 *   -1 : narrow
 *   1  : wide
 *   0  : undetermine
 */
void set_fwide (FILE* fp, int wide)
{
  int ret = fwide (fp, wide); 
  printf ("old wide = %d, new wide = %d\n", ret, wide); 
}

void get_fwide (FILE* fp)
{
    set_fwide (fp, 0); 
}

int main (int argc, char* argv[])
{
  int towide = 0; 
  FILE* fp = fopen ("abc.txt", "w+"); 
  if (fp == 0)
    err_sys ("fopen failed"); 

#if defined (USE_WCHAR)
  towide = 1;
#else
  towide = -1;  
#endif

#if defined (USE_EXPLICIT_FWIDE)
  // set wide explicitly
  set_fwide (fp, towide); 
#else
  // set wide automatically by s[w]printf
  do_fwide (fp, towide); 
#endif 

  get_fwide (fp); 

  // test set fwide after wide determined
  set_fwide (fp, towide > 0 ? -1 : 1); 

  get_fwide (fp); 

  // test output with same wide
  do_fwide (fp, towide); 
  // test output with different wide
  do_fwide (fp, towide > 0 ? -1 : 1); 

  fclose (fp); 
  return 0; 
}

通過給 Makefile 不同的編譯開關來控制生成的 demo:

all: fwide fwidew

fwide: fwide.o apue.o
	gcc -Wall -g $^ -o $@

fwide.o: fwide.c ../apue.h 
	gcc -Wall -g -c $< -o $@ 

fwidew: fwidew.o apue.o
	gcc -Wall -g $^ -o $@

fwidew.o: fwide.c ../apue.h 
	gcc -Wall -g -c $< -o $@ -DUSE_WCHAR

apue.o: ../apue.c ../apue.h 
	gcc -Wall -g -c $< -o $@

clean: 
	@echo "start clean..."
	-rm -f *.o core.* *.log *~ *.swp fwide fwidew 
	@echo "end clean"

.PHONY: clean

生成兩個程序:fwide 使用窄字符集,fwidew 使用寬字符集:

$ ./fwide
old wide = -1, new wide = 0
old wide = -1, new wide = 1
old wide = -1, new wide = 0
$ cat abc.txt 
do fwide -1
do fwide -1

$ ./fwidew
old wide = 1, new wide = 0
old wide = 1, new wide = -1
old wide = 1, new wide = 0
$ cat abc.txt 
do fwide 1
do fwide 1

分別看兩個 demo 的輸出,其中 old wide 表示返回值,new wide 是參數,可以觀察到以下現象:

  • 一旦設置為一個定向,就無法更改定向
  • 如果不顯示設置定向,通過第一個標準 IO 庫調用可以確定定向,這裡使用的是 s[w]printf (可以設置 USE_EXPLICIT_FWIDE 來啟用顯示定向)
  • 使用非本定向的輸出接口無法輸出字符串到流 (do_fwide 向文件流寫入一行,共調用 3 次,只打印 2 行信息)

如果設置了 USE_EXPLICT_FWIDE 來顯示設置定向,輸出稍有不同:

$ ./fwide
old wide = -1, new wide = -1
old wide = -1, new wide = 0
old wide = -1, new wide = 1
old wide = -1, new wide = 0
$ cat abc.txt
do fwide -1

$ ./fwidew
old wide = 1, new wide = 1
old wide = 1, new wide = 0
old wide = 1, new wide = -1
old wide = 1, new wide = 0
$ cat abc.txt
do fwide 1

首先因為顯示設置 fwide 導致上面的輸出增加了一行,其次因為省略了隱式的 f[w]printf 調用,下面的輸出少了一行,但是結論不變。

最後注意 fwide 無出錯返回,需要使用 errno 來判斷是否發生了錯誤,為了防止上一個調用的錯誤碼干擾結果,最好在發起調用前清空 errno。

freopen 會清除流的定向。

緩衝

緩衝是標準 IO 庫的核心,通過緩衝來減少內核 IO 的次數以提升性能是標準 IO 對內核 IO (read/write) 的重大改進。

一個流對象 (FILE*) 內部記錄了很多信息:

  • 文件描述符  (fd)
  • 緩衝區指針
  • 緩衝區長度
  • 當前緩衝區字符數
  • 出錯標誌位
  • 文件結束標誌位

其中很多信息是與緩衝相關的。

緩衝類型

標準 IO 的緩衝主要分為三種類型:

  • 全緩衝,填滿緩衝區後才進行實際 IO 操作
  • 行緩衝,在輸入和輸出中遇到換行符或緩衝區滿才進行實際 IO 操作
  • 無緩衝,每次都進行實際 IO 操作

對於行緩衝,除了上面提到的兩種場景,當通過標準 IO 庫試圖從以下流中得到輸入數據時,會造成所有行緩衝輸出流被沖洗 (flush):

  • 從不帶緩衝的流中得到輸入數據
  • 從行緩衝的流中得到輸入數據,後者要求從內核得到數據 (行緩衝用盡)

這樣做的目的是,所需要的數據可能已經在行緩衝區中,沖洗它們來保證從系統 IO 中獲取最新的數據。

術語沖洗 (flush) 也稱為刷新,使流所有未寫的數據被傳送至內核:

int fflush(FILE *stream);

如果給 stream 參數 NULL,將導致進程所有輸出流被沖洗。

對於三個預定義的標準 IO 流 (stdin/stdout/stderr) 的緩衝類型,ISO C 有以下要求:

  • 當且僅當 stdin/stdout 不涉及交互式設備時,它們才是全緩衝的
  • stderr 不可以是全緩衝的

很多系統默認使用下列類型的緩衝:

  • stdin/stdout
    • 關聯終端設備:行緩衝
    • 其它:全緩衝
  • stderr :無緩衝

stdin/stdout 默認是關聯終端設備的,除非重定向到文件。

在進行第一次 IO 時,標準庫會自動為全緩衝或行緩衝的流分配 (malloc) 緩衝區,也可以直接指定流的緩衝類型,這一點與流的定位類似:

void setbuf(FILE *restrict stream, char *restrict buf);
void setbuffer(FILE *stream, char *buf, int size);
int setlinebuf(FILE *stream);
int setvbuf(FILE *restrict stream, char *restrict buf, int type, size_t size);

與流的定位不同的是,流的緩衝類型在確定後仍可以更改。

上面幾個接口中的重點是 setvbuf,其中 type 為流類型,可以選取以下幾個值:

  • _IONBF:unbuffered,無緩衝
  • _IOLBF:line buffered,行緩衝
  • _IOFBF:fully buffered,全緩衝

根據 type、buf、size 的不同組合,可以得到不同的緩衝效果:

type size buffer 效果
_IONBUF ignore ignore 無緩衝
_IOLBUF 0 NULL (自動分配合適大小的緩衝,關閉時自動釋放) 行緩衝
非 NULL (同上,用戶提供的 buffer 被忽略)
>0 NULL (自動分配 size 大小的緩衝,關閉時自動釋放) *
非 NULL (緩衝區長度大於等於 size,關閉時用戶釋放)
_IOFBF 同上 同上 全緩衝

其中標星號的表示 ANSI C 擴展。其它接口都可視為 setvbuf 的簡化:

接口 等價效果
setbuf setvbuf (stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
setbuffer setvbuf (stream, buf, buf ? _IOFBF : _IONBF, size);
setlinebuffer setvbuf (stream, (char *)NULL, _IOLBF, 0);

setbuf 要求 buf 參數不為 NULL 時緩衝區大小應大於等於 BUFSIZ (CentOS 上為 8192)。

freopen 會重置流的緩衝類型。

setvbuf 不帶 buf 時的語義

構造程序驗證第一個表中的結論,在開始之前,我們需要準確的獲取流當前的緩衝區類型、大小等信息,然而標準 IO 庫沒有提供這方面的接口,幸運的是,如果只看 linux 系統,可以將問題簡化:

struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2;
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
};

上面是 linux 中 FILE 結構體的定義,其中

  • _IO_file_flags/_flags 存放緩衝區類型
  • _IO_buf_base 為緩衝區地址
  • _IO_buf_end 為緩衝區末尾+1
  • _IO_buf_end – _IO_buf_base 為緩衝區長度

這樣單純通過 FILE* 就能獲取緩衝區信息了:

void tell_buf (char const* name, FILE* fp)
{
  printf ("%s is: ", name); 
  if (fp->_flags & _IO_UNBUFFERED)
    printf ("unbuffered\n"); 
  else if (fp->_flags & _IO_LINE_BUF)
    printf ("line-buffered\n"); 
  else 
    printf ("fully-buffered\n"); 

  printf ("buffer size is %d, %p\n", fp->_IO_buf_end - fp->_IO_buf_base, fp->_IO_buf_base); 
  printf ("discriptor is %d\n\n", fileno (fp)); 
}

有了 tell_buf 就可以構造驗證程序了:

#include "../apue.h"
#include <stdio.h> 

int main (int argc, char* argv[])
{
  tell_buf ("stdin", stdin); 

  int a; 
  scanf ("%d", &a); 
  printf ("a = %d\n", a); 
  tell_buf ("stdin", stdin); 
  tell_buf ("stdout", stdout); 
  tell_buf ("stderr", stderr); 
  fprintf (stderr, "a = %d\n", a); 
  tell_buf ("stderr", stderr); 
  printf ("\n"); 

  char buf[BUFSIZ] = { 0 }; 
  printf ("bufsiz = %d, address = %p\n", BUFSIZ, buf); 
  setbuf (stdout, NULL); 
  tell_buf ("stdout (no)", stdout); 
  setbuf (stderr, buf); 
  tell_buf ("stderr (has)", stderr); 
  setbuf (stdin, buf); 
  tell_buf ("stdin (has)", stdin); 
  printf ("\n"); 

  setvbuf (stderr, NULL, _IONBF, 0); 
  tell_buf ("stderr (no)", stderr); 
  setvbuf (stdout, buf, _IOFBF, 2048); 
  tell_buf ("stdout (full, 2048)", stdout); 
  setvbuf (stderr, buf, _IOLBF, 1024); 
  tell_buf ("stderr (line, 1024)", stderr); 
  setvbuf (stdout, NULL, _IOLBF, 4096); 
  tell_buf ("stdout (line null 4096)", stdout); 
  setvbuf (stderr, NULL, _IOFBF, 3072); 
  tell_buf ("stderr (full null 3072)", stderr); 
  setvbuf (stdout, NULL, _IOFBF, 0); 
  tell_buf ("stdout (full null 0)", stdout); 
  setvbuf (stderr, NULL, _IOLBF, 0); 
  tell_buf ("stderr (line null 0)", stderr); 
  return 0; 
}

程序依據空行分為三部分,做個簡單說明:

  • 第一部分驗證 stdin/stdout/stderr 緩衝的初始狀態、第一次執行 IO 後的狀態
    • 為了驗證 stdin 第一次執行 IO 操作後的狀態,加了一個 scanf 操作
    • 對於 stdout 因 tell_buf 本身使用到了 printf 操作,會導致 stdout 緩衝區的默認分配,所以無法驗證它的初始狀態
    • 因沒有使用 stderr 輸出,所以可以驗證它的初始狀態
  • 第二部分驗證 setbuf 調用
    • stdout 無緩衝
    • stderr/stdin 全緩衝
  • 第三部分驗證 setvbuf 調用
    • stderr 無緩衝
    • stdout 帶 buf 全緩衝
    • stderr 帶 buf 行緩衝
    • stdout 無 buf 指定 size 行緩衝
    • stderr 無 buf 指定 size 全緩衝
    • stdout 無 buf 0 size 全緩衝
    • stderr 無 buf 0 size 行緩衝

下面是程序輸出:

$ ./fgetbuf 
stdin is: fully-buffered
buffer size is 0, (nil)
discriptor is 0

<42>
a = 42
stdin is: line-buffered
buffer size is 1024, 0x7fcf9483d000
discriptor is 0

stdout is: line-buffered
buffer size is 1024, 0x7fcf9483e000
discriptor is 1

stderr is: unbuffered
buffer size is 0, (nil)
discriptor is 2

a = 42
stderr is: unbuffered
buffer size is 1, 0x7fcf94619243
discriptor is 2


bufsiz = 8192, address = 0x7fff8b5bbcb0
stdout (no) is: unbuffered
buffer size is 1, 0x7fcf94619483
discriptor is 1

stderr (has) is: fully-buffered
buffer size is 8192, 0x7fff8b5bbcb0
discriptor is 2

stdin (has) is: fully-buffered
buffer size is 8192, 0x7fff8b5bbcb0
discriptor is 0


stderr (no) is: unbuffered
buffer size is 1, 0x7fcf94619243
discriptor is 2

stdout (full, 2048) is: fully-buffered
buffer size is 2048, 0x7fff8b5bbcb0
discriptor is 1

stderr (line, 1024) is: line-buffered
buffer size is 1024, 0x7fff8b5bbcb0
discriptor is 2

stdout (line null 4096) is: line-buffered
buffer size is 2048, 0x7fff8b5bbcb0
discriptor is 1

stderr (full null 3072) is: fully-buffered
buffer size is 1024, 0x7fff8b5bbcb0
discriptor is 2

stdout (full null 0) is: fully-buffered
buffer size is 2048, 0x7fff8b5bbcb0
discriptor is 1

stderr (line null 0) is: line-buffered
buffer size is 1024, 0x7fff8b5bbcb0
discriptor is 2

為了方便觀察,用兩個換行區分各個部分的輸出。可以看出:

  • stdin/stderr 初始時是沒有分配緩衝區的,執行第一次 IO 後,stdin/stdout 變為行緩衝類型,stderr 變為無緩衝,都分配了獨立的緩衝區空間 (地址不同)。特別是 stderr,雖然是無緩衝的,底層也有 1 位元組的緩衝區存在,這點需要注意
  • setbuf 調用設置全緩衝後,stderr/stdin 的緩衝區地址變為 buf 字符數組地址;stdout 設置為無緩衝後,緩衝區重新獲得 1 位元組的新地址
  • setvbuf 設置 stderr 無緩衝場景同 setbuf 情況,緩衝區重新分配為 1 位元組的新地址
  • setvbuf 設置 stdout 全緩衝、設置 stderr 行緩衝的場景同 setbuf 情況,緩衝區地址變為 buf 字符數組地址,大小變為 size 參數的值
  • setvbuf 設置 stdout 行緩衝、設置 stderr 全緩衝不帶 buf (NULL) 的結果就不太一樣了,緩衝區地址和大小均未改變,僅緩衝類型發生變更
  • setvbuf 設置 stdout 全緩衝、設置 stderr 行緩衝不帶 buf (NULL) 0 size 的結果同上,緩衝區地址和大小均未改變,僅緩衝類型發生變更

最後兩個 case 與書上所說不同,看看 man setvbuf 怎麼說:

Except for unbuffered files, the buf argument should point to a buffer at least size bytes long; this buffer will be used  instead  of  the
current  buffer.   If  the argument buf is NULL, only the mode is affected; a new buffer will be allocated on the next read or write opera‐
tion.  The setvbuf() function may be used only after opening a stream and before any other operations have been performed on it.

翻譯一下:當不帶 buf 調用時只更新緩衝類型,緩衝區地址將在下一次 IO 時更新。對程序稍加改造進行驗證,每個 setvbuf 調用後加上輸出語句 (fprintf) 來強制 IO 庫分配空間:

  setvbuf (stderr, NULL, _IONBF, 0); 
  tell_buf ("stderr (no)", stderr); 
  setvbuf (stdout, buf, _IOFBF, 2048); 
  fprintf (stdout, "a = %d\n", a); 
  tell_buf ("stdout (full, 2048)", stdout); 
  setvbuf (stderr, buf, _IOLBF, 1024); 
  fprintf (stderr, "a = %d\n", a); 
  tell_buf ("stderr (line, 1024)", stderr); 
  setvbuf (stdout, NULL, _IOLBF, 4096); 
  fprintf (stdout, "a = %d\n", a); 
  tell_buf ("stdout (line null 4096)", stdout); 
  setvbuf (stderr, NULL, _IOFBF, 3072); 
  fprintf (stderr, "a = %d\n", a); 
  tell_buf ("stderr (full null 3072)", stderr); 
  setvbuf (stdout, NULL, _IOFBF, 0); 
  fprintf (stdout, "a = %d\n", a); 
  tell_buf ("stdout (full null 0)", stdout); 
  setvbuf (stderr, NULL, _IOLBF, 0); 
  fprintf (stderr, "a = %d\n", a); 
  tell_buf ("stderr (line null 0)", stderr); 
  return 0; 

再執行 tell_buf,然鵝輸出沒有任何改觀。不過發現緩衝類型和緩衝區 buffer 確實起作用了:

  • 設置為全緩衝的流 fprintf 不會立即輸出,需要使用 fflush 沖洗一下
  • 由於 stdout 和 stderr 使用了一塊緩衝區,同樣的信息會被分別輸出一次

為了避免上面這些問題,決定使用文件流重新驗證上面 4 個 case,構造驗證程序如下:

#include "../apue.h"
#include <stdio.h> 

int main (int argc, char* argv[])
{
  FILE* fp = NULL; 
  FILE* fp1 = fopen ("flbuf.txt", "w+"); 
  FILE* fp2 = fopen ("lnbuf.txt", "w+"); 
  FILE* fp3 = fopen ("nobuf.txt", "w+"); 
  FILE* fp4 = fopen ("unbuf.txt", "w+"); 
  
  fp = fp1; 
  if (setvbuf (fp, NULL, _IOFBF, 8192) != 0)
      err_sys ("fp (full null 8192) failed"); 
  tell_buf ("fp (full null 8192)", fp); 

  fp = fp2; 
  if (setvbuf (fp, NULL, _IOLBF, 3072) != 0)
      err_sys ("fp (line null 3072) failed"); 
  tell_buf ("fp (line null 3072)", fp); 

  fp = fp3; 
  if (setvbuf (fp, NULL, _IOLBF, 0) != 0)
      err_sys ("fp (line null 0) failed"); 
  tell_buf ("fp (line null 0)", fp); 

  fp = fp4; 
  if (setvbuf (fp, NULL, _IOFBF, 0) != 0)
      err_sys ("fp (full null 0) failed"); 
  tell_buf ("fp (full null 0)", fp); 

  fclose (fp1); 
  fclose (fp2); 
  fclose (fp3); 
  fclose (fp4); 
  return 0; 

這個程序相比之前主要改進了以下幾點:

  • 使用文件 IO 流代替終端 IO 流
  • 每個流都是新構造的,調用 setvbuf 之前未執行任何 IO 操作
  • 加入錯誤處理,判斷 setvbuf 是否出錯 (返回非 0 值)

編譯運行得到下面的輸出:

$ ./fgetbuf_fp
fp (full null 8192) is: fully-buffered
buffer size is 4096, 0x7fccd6c23000
discriptor is 3

fp (line null 3072) is: line-buffered
buffer size is 0, (nil)
discriptor is 4

fp (line null 0) is: line-buffered
buffer size is 0, (nil)
discriptor is 5

fp (full null 0) is: fully-buffered
buffer size is 4096, 0x7fccd6c21000
discriptor is 6

有了一些改觀:

  • 全緩衝的緩衝區都創建了
  • 行緩衝的緩衝區都沒有創建
  • 緩衝區的長度都沒有使用用戶提供的值,而使用默認值 4096

結合之前 man setvbuf 對延後分配緩衝區的說明,在每個 setvbuf 調用後面加一條輸出語句強制 IO 庫分配空間:

fputs ("fp", fp); 

觀察輸出:

$ ./fgetbuf_fp
fp (full null 8192) is: fully-buffered
buffer size is 4096, 0x7f8047525000
discriptor is 3

fp (line null 3072) is: line-buffered
buffer size is 4096, 0x7f8047523000
discriptor is 4

fp (line null 0) is: line-buffered
buffer size is 4096, 0x7f8047522000
discriptor is 5

fp (full null 0) is: fully-buffered
buffer size is 4096, 0x7f8047521000
discriptor is 6

這次都有緩衝區了,且默認值都是 4K。結合前後兩個例子,可以合理的推測 setvbuf 不帶 buf 參數的行為:

  • 只有當流沒有分配緩衝區時,setvbuf 調用才生效,否則仍延用之前的緩衝區不重新分配
  • 忽略 size 參數,統一延用之前的 size 或默認值

稍微修改一下程序進行驗證:

fp = fp1;

將所有為 fp 賦值的地方都改成上面這句,即保持 fp 不變,讓 4 個用例都使用 fp1,再次運行:

$ ./fgetbuf_fp
fp (full null 8192) is: fully-buffered
buffer size is 4096, 0x7f6ea5349000
discriptor is 3

fp (line null 3072) is: line-buffered
buffer size is 4096, 0x7f6ea5349000
discriptor is 3

fp (line null 0) is: line-buffered
buffer size is 4096, 0x7f6ea5349000
discriptor is 3

fp (full null 0) is: fully-buffered
buffer size is 4096, 0x7f6ea5349000
discriptor is 3

觀察到緩衝區地址一直沒有變化。當已經為流指定了用戶提供的緩衝區,使用 setvbuf 不帶 buf 參數的方式並不能讓系統釋放這塊內存地址的使用權。

這引入了另外一個問題 —— 一旦指定了用戶提供的緩衝區空間,還能讓系統自動分配緩衝區嗎?答案是不能。有的讀者可能不信,憑直覺認為分以下兩步可以實現這個目標:

  1. 設置流的類型為無緩衝類型
  2. 設置流的類型為不帶 buf 的行或全緩衝類型,從而觸發流緩衝區的自動分配

構造下面的程序驗證:

#include "../apue.h"
#include <stdio.h> 

int main (int argc, char* argv[])
{
  char buf[BUFSIZ] = { 0 };
  printf ("BUFSIZ = %d, address %p\n", BUFSIZ, buf); 
  FILE* fp = fopen ("unbuf.txt", "w+"); 
  
  setbuf (fp, buf); 
  tell_buf ("fp (full)", fp); 

  setbuf (fp, NULL); 
  if (setvbuf (fp, NULL, _IOLBF, 4096) != 0)
      err_sys ("fp (line null 4096) failed"); 
  fputs ("fp", fp); 
  tell_buf ("fp (line null 4096)", fp); 

  setbuf (fp, NULL); 
  if (setvbuf (fp, NULL, _IOFBF, 3072) != 0)
      err_sys ("fp (full null 3072) failed"); 
  tell_buf ("fp (full null 3072)", fp); 

  setbuf (fp, NULL); 
  if (setvbuf (fp, NULL, _IOLBF, 2048) != 0)
      err_sys ("fp (line null 2048) failed"); 
  fputs ("fp", fp); 
  tell_buf ("fp (line null 2048)", fp); 

  setbuf (fp, NULL); 
  if (setvbuf (fp, NULL, _IOFBF, 1024) != 0)
      err_sys ("fp (full null 1024) failed"); 
  tell_buf ("fp (full null 1024)", fp); 

  fclose (fp); 
  return 0; 
}

每次調用 setvbuf 前增加一個 setbuf 調用,重置為無緩衝類型來釋放流的緩衝區。得到如下輸出:

$ ./fgetbuf_un 
BUFSIZ = 8192, address 0x7ffe07ddcb80
fp (full) is: fully-buffered
buffer size is 8192, 0x7ffe07ddcb80
discriptor is 3

fp (line null 4096) is: line-buffered
buffer size is 1, 0xf69093
discriptor is 3

fp (full null 3072) is: fully-buffered
buffer size is 1, 0xf69093
discriptor is 3

fp (line null 2048) is: line-buffered
buffer size is 1, 0xf69093
discriptor is 3

fp (full null 1024) is: fully-buffered
buffer size is 1, 0xf69093
discriptor is 3

觀察到最後緩衝區大小都是 1 位元組,地址不再改變,且看着不像有效內存地址。所以最終結論是:一旦用戶為流提供了緩衝區,這塊緩衝區的內存就會一直被該流佔用,直到流關閉、流設置為無緩衝、用戶提供其它的緩衝區代替。這個結論只在 linux (CentOS) 上有效,其它平台因 FILE 結構不同沒有驗證,感興趣的讀者可以修改程序自行驗證。

最後,雖然流的緩衝區可以更改,但是不建議這樣做,從上面的例子可以看出,大多數類型變更會引發緩衝區重新分配,其中的數據就會隨之丟失,導致信息讀取、寫入不全的問題。

行緩衝流的自動沖洗

有了上面的鋪墊,回過頭用它來驗證一下行緩衝流被沖洗的兩種情況:

  • 從不帶緩衝的流中得到輸入數據
  • 從行緩衝的流中得到輸入數據,後者要求從內核得到數據 (行緩衝用盡)

構造 fflushline 程序如下:

#include "../apue.h"

int main (int argc, char* argv[])
{
  FILE* fp1 = fopen ("flbuf.txt", "w+"); 
  FILE* fp2 = fopen ("lnbuf.txt", "w+"); 
  FILE* fp3 = fopen ("nobuf.txt", "r+"); 
  if (fp1 == 0 || fp2 == 0 || fp3 == 0)
    err_sys ("fopen failed"); 

  // initialize buffer type
  // fp1 keep full buffer
  if (setvbuf (fp2, NULL, _IOLBF, 0) < 0)
      err_sys ("set line buf failed"); 

  if (setvbuf (fp3, NULL, _IONBF, 0) < 0)
      err_sys ("set no buf failed"); 

  // fill buffer
  printf ("first line to screen! "); 
  fprintf (fp1, "first line to full buf! "); 
  fprintf (fp2, "first line to line buf! "); 

  // case 1: read from line buffered FILE* and need fetch data from system
  sleep (3); 
  getchar(); 

  // fill buffer again
  printf ("last line to screen."); 
  fprintf (fp1, "last line to full buf."); 
  fprintf (fp2, "last line to line buf."); 

  // case 2: read from no buffered FILE* 
  sleep (3); 
  int ret = fgetc (fp3); 
  // give user some time to check file content
  // note no any output here to avoid repeat case 1
  sleep (10); 

  printf ("\n%c: now all over!\n", ret); 

  fclose (fp1); 
  fclose (fp2); 
  fclose (fp3); 
  return 0; 
}

初始化了三個文件,從文件名可以了解到它們的緩衝類型,前兩個用於寫,後一個用於讀,用於讀的 nobuf.txt 必需在程序運行前手工創建並寫入一些數據。

分別為各個文件流的緩衝區填充了一些數據,注意這裡沒有加換行符,以防行緩衝的文件遇到換行符沖洗數據。然後分兩個用例來檢驗書中的兩個結論,如果書中說的沒錯,當 getchar 從行緩衝的 stdin 或 fgetc 從無緩衝的 fp3 讀數據時,行緩衝的 fp2 對應的文件中應該有數據,而全緩衝的 fp1 對應的文件中沒有數據。下面是實際的運行輸出:

> ./fflushline
...
> first line to screen! 
                                                                     cat lnbuf.txt flbuf.txt <
...
> last line to screen.
                                                                     cat lnbuf.txt flbuf.txt <
..........
> a: now all over!
                                                                     cat lnbuf.txt flbuf.txt <
first line to line buf! last line to line buf.first line to full buf! last line to full buf. <

為了清晰起見,將兩個終端的輸出放在了一起,> 開頭的是測試程序的輸出,< 結尾的是 cat 文件的輸出。

其中第一個 cat 是為了驗證對 stdin 調用 getchar 的結果,第二個 cat 是為了驗證 fgetc (fp3) 的結果,最後一個是為了驗證程序結束後的結果。與預期不同的是,不論是讀取行緩衝 (stdin) 還是無緩衝文件 (fp3),fp2 文件均沒有被沖洗,直到最後文件關閉才發生了沖洗。為了驗證 fp2 確實是行緩衝的,將 fprintf fp2 的語句都加上換行符,新的輸出果然變了:

> ./fflushline
...
> first line to screen! 
                                                                     cat lnbuf.txt flbuf.txt <
                                                                     first line to line buf! <
...
> last line to screen.
                                                                     cat lnbuf.txt flbuf.txt <
                                                                     first line to line buf! <
                                                                      last line to line buf. <
..........
> a: now all over!
                                                                     cat lnbuf.txt flbuf.txt <
                                                                      last line to line buf. <
                                              first line to full buf! last line to full buf. <

看起來行緩衝確實是起作用了。回過頭來觀察程序的第一次輸出,對於 stdout 的 printf 輸出,當讀取 stdin 或無緩衝文件 fp3 時,都會被沖洗!為了證明是 getchar / fgetc(fp3) 的影響,特地在它們之前加了 sleep,而輸出的 … 中點的個數表示的就是等待秒數,與程序中設定的一致!另外不光是輸出時機與讀取文件相吻合,輸出的內容還會自動加換行符,按理說沖洗文件僅僅把緩存中的內容寫到硬件即可,不應該修改它們,可現實就是這樣。

因此結論是,如果僅限於 stdout,書中結論是成立的。讀取 stdin 會沖洗 stdout 這個我覺得是有道理的,但是讀 fp3 會沖洗 stdout 我是真沒想到,有些東西不親自去試一下,永遠不清楚居然會是這樣。一開始懷疑只要是針對字符設備的行緩衝文件,都有這個特性,猜測 fp2 沒有自動沖洗是因為它重定向的磁盤是塊設備的緣故,看看 man setvbuf 怎麼說:

The three types of buffering available are unbuffered, block buffered, and line buffered.  When an output stream is unbuffered, information
appears on the destination file or terminal as soon as written; when it is block buffered many characters are saved up  and  written  as  a
block;  when  it is line buffered characters are saved up until a newline is output or input is read from any stream attached to a terminal
device (typically stdin).  The function fflush(3) may be used to force the block out early.  (See fclose(3).)  Normally all files are block
buffered.   When the first I/O operation occurs on a file, malloc(3) is called, and a buffer is obtained.  If a stream refers to a terminal
(as stdout normally does) it is line buffered.  The standard error stream stderr is always unbuffered by default.

翻譯一下第三行關於行緩衝的說明:當關聯在終端上的流 (典型的如 stdin) 被讀取時,所有行緩衝流會被沖洗。相比書中的結論,加了一個限定條件——關聯到終端的流,與測試結論是相符的。

所以最終的結論是,關聯到終端的行緩衝流 (stdout) 被沖洗的條件:

  • 從不帶緩衝的流中得到輸入數據
  • 從行緩衝的流中得到輸入數據,後者要求從內核得到數據 (行緩衝用盡)

至於是關聯到終端的流,還是關聯到一切字符設備的流,感興趣的讀者可以修改上面的例子自行驗證。

讀寫

打開一個流後有三種方式可以對其進行讀寫操作。

一次一個字符

int getc(FILE *stream);
int fgetc(FILE *stream);
int getchar(void);

int putc(int c, FILE *stream);
int fputc(int c, FILE *stream);
int putchar(int c);

其中 getc/fgetc、putc/fputc 的區別主要是前者一般實現為宏,後者一般實現為函數,因此在使用第一個版本時,需要注意宏的副作用,如參數的多次求值,舉個例子:

int ch = getc (files[i++]);

就可能會對 i 多次自增,使用函數版本就不存在這個問題。不過相應的,使用函數的性能低於宏版本。下面是一種 getc 的實現:

#define getc(_stream)     (--(_stream)->_cnt >= 0 \
                ? 0xff & *(_stream)->_ptr++ : _filbuf(_stream))

由於 _stream 在宏中出現了多次,因此上面的多次求值問題是鐵定出現的。當然了,有些系統這個宏是轉調了一個底層函數,就不存在這方面的問題了。

getchar 等價於 fgetc (stdin),putchar 等價於 fputc (stdout)。

讀取字符接口均使用 unsigned char 接收下一個字符,再將其轉換為 int 返回,這樣做主要是有兩個方面的考慮:

  • 直接將 char 轉換為 int 返回,存在高位為 1 時得到負值的可能性,容易與出錯場景混淆
  • 出錯或到達文件尾時,返回 EOF (-1),此值無法存放在 char/unsigned char 類型中

因此千萬不要使用 char 或 unsigned char 類型接收 getc/fgetc/getchar 返回的結果,否則上面的問題仍有可能發生。

讀取流出錯和到達文件尾返回的錯誤一樣,在這種場景下,如果需要進一步甄別發生了哪種情況,需要調用以下接口進行判斷:

int feof(FILE *stream);
int ferror(FILE *stream);

這些接口返迴流內部的 eof 和 error 標記。對於寫流出錯的場景,就不需要判斷 eof 了,鐵定是 error 了。

當流處於出錯或 eof 狀態時,繼續在流上進行讀寫操作將直接返回 EOF,需要手動清空錯誤或 eof 標誌:

void clearerr(FILE *stream);

針對輸入,可以將已讀取的字符再壓入流中:

int ungetc(int c, FILE *stream);

對於通過查看下個字符來決定如何處理後面輸入的程序而言,回送是一個很有用的操作,可以避免使用單獨的變量保存已讀取的字符,並根據是否已讀取來判斷是從該變量獲取下個字符、還是從流中,從而簡化了程序的編寫。

一次只能回送一個字符,雖然可以通過多次調用來回送多個字符,但不保證都能回送成功,因為回送不會寫入設備,只是放在緩衝區,受緩衝區大小限制有回送上限。回送的字符可以不必是 getc 返回的字符,但是不能為 EOF。ungetc 是除 clearerr 外可以清除 eof 標誌位的接口之一,達到文件尾可以回送字符而不返回錯誤就是這個原因。

對於 ungetc 到底能回送多少個字符,構造了下面的程序去驗證:

#include "../apue.h"
#include <wchar.h> 

int main (int argc, char* argv[])
{
  int ret = 0; 
  while (1)
  {
    ret = getc (stdin); 
    if (ret == EOF)
      break; 

    printf ("read %c\n", (unsigned char) ret); 
  }

  if (feof (stdin))
    printf ("reach EndOfFile\n"); 
  else 
    printf ("not reach EndOfFile\n"); 

  if (ferror (stdin))
    printf ("read error\n"); 
  else 
    printf ("not read error\n"); 

  ungetc ('O', stdin); 
  printf ("after ungetc\n"); 

  if (feof (stdin))
    printf ("reach EndOfFile\n"); 
  else 
    printf ("not reach EndOfFile\n"); 

  if (ferror (stdin))
    printf ("read error\n"); 
  else 
    printf ("not read error\n"); 

  unsigned long long i = 0; 
  char ch = 0; 
  while (1)
  {
    ch = 'a' + i % 26; 
    if (ungetc (ch, stdin) < 0)
    {
      printf ("ungetc %c failed\n", ch); 
      break; 
    }
    ++ i; 
    if (i % 100000000 == 0)
        printf ("unget %llu: %c\n", i, ch); 
  }

  printf ("unget %llu chars\n", i); 
  if (ungetc (EOF, stdin) == EOF)
    printf ("ungetc EOF failed\n"); 

  while (1)
  {
    ret = getc (stdin); 
    if (ret == EOF)
      break; 

    if (i % 100000000 == 0 || i <  30)
        printf ("read %llu: %c\n", i, (unsigned char) ret); 
      
    --i;
    // prevent unsigned overflow
    if (i > 0)
        --i; 
  }

  printf ("over!\n"); 
  return 0; 
}

程序包含三個大的循環:

  • 第一個循環是處理輸入字符的,當用戶輸入 Ctrl+D 時退出這個循環,並打印當前 ferror/feof 的值,通過 ungetc 回送字符後再次打印 ferror/feof 的值;
  • 第二個循環不停的回送字符,直到系統出錯,並打印回送的字符總量,之後驗證回送 EOF 返回失敗的用例;
  • 第三個循環將回送的字符讀取回來,並打印最後 30 個字符的內容,看看和開頭回送的內容是否一致;

最後用戶輸入 Ctrl+D 退出整個程序,下面來看看程序的輸出吧:

查看代碼
$ ./fungetc 
abc123
read a
read b
read c
read 1
read 2
read 3
read 

<Ctrl+D>
reach EndOfFile
not read error
after ungetc
not reach EndOfFile
not read error
unget 100000000: v
unget 200000000: r
unget 300000000: n
unget 400000000: j
unget 500000000: f
unget 600000000: b
unget 700000000: x
unget 800000000: t
unget 900000000: p
unget 1000000000: l
unget 1100000000: h
unget 1200000000: d
unget 1300000000: z
unget 1400000000: v
unget 1500000000: r
unget 1600000000: n
unget 1700000000: j
unget 1800000000: f
unget 1900000000: b
unget 2000000000: x
unget 2100000000: t
unget 2200000000: p
unget 2300000000: l
unget 2400000000: h
unget 2500000000: d
unget 2600000000: z
unget 2700000000: v
unget 2800000000: r
unget 2900000000: n
unget 3000000000: j
unget 3100000000: f
unget 3200000000: b
unget 3300000000: x
unget 3400000000: t
unget 3500000000: p
unget 3600000000: l
unget 3700000000: h
unget 3800000000: d
unget 3900000000: z
unget 4000000000: v
unget 4100000000: r
unget 4200000000: n
ungetc v failed
unget 4294967295 chars
ungetc EOF failed
read 4200000000: n
read 4100000000: r
read 4000000000: v
read 3900000000: z
read 3800000000: d
read 3700000000: h
read 3600000000: l
read 3500000000: p
read 3400000000: t
read 3300000000: x
read 3200000000: b
read 3100000000: f
read 3000000000: j
read 2900000000: n
read 2800000000: r
read 2700000000: v
read 2600000000: z
read 2500000000: d
read 2400000000: h
read 2300000000: l
read 2200000000: p
read 2100000000: t
read 2000000000: x
read 1900000000: b
read 1800000000: f
read 1700000000: j
read 1600000000: n
read 1500000000: r
read 1400000000: v
read 1300000000: z
read 1200000000: d
read 1100000000: h
read 1000000000: l
read 900000000: p
read 800000000: t
read 700000000: x
read 600000000: b
read 500000000: f
read 400000000: j
read 300000000: n
read 200000000: r
read 100000000: v
read 29: c
read 28: b
read 27: a
read 26: z
read 25: y
read 24: x
read 23: w
read 22: v
read 21: u
read 20: t
read 19: s
read 18: r
read 17: q
read 16: p
read 15: o
read 14: n
read 13: m
read 12: l
read 11: k
read 10: j
read 9: i
read 8: h
read 7: g
read 6: f
read 5: e
read 4: d
read 3: c
read 2: b
read 1: a
read 0: O
<Ctrl+D>
over!

下面做個簡單說明:

  • 用戶輸入 abc123 實際上是 7 個字符 (包含結尾 \n),這是打印 7 行內容的原因,一個多餘空行是 printf (“read %c\n”, ‘\n’) 的結果
  • 第一次 Ctrl+D 後 eof 標誌為 true,error 狀態為 false;ungetc 後,兩個狀態都被重置
  • 進入回送循環,為防止打印太多內容,每一億行打印一條日誌,最終輸出 4294967295 條記錄
  • 進入讀取循環,讀取了 UINT_MAX+1 條記錄,剛好包含了第一次 ungetc 的 ‘0’ 字符。可以認為這個緩存大小是 4294967295+1 即 4 GB

注意這裡使用 unsigned long long 類型避免 int 或 unsigned int 溢出問題。

從試驗結果來看,ungetc 的緩衝比想像的要大的多,一般認為有個 64 KB 就差不多了,實際遠遠超過了這個。不清楚這個是終端設備專有的,還是所有緩衝區都這麼大,感興趣的讀者可以修改上面的程序自行驗證。

一次一行

char* gets(char *str);
char* fgets(char * restrict str, int size, FILE * restrict stream);
   
int puts(const char *s);
int fputs(const char *restrict s, FILE *restrict stream);  

其中 gets 等價於 fgets (str, NaN, stdin), puts 等價於 fputs (s, stdout)。但是在一些細節上它們還有差異:

接口  gets fgets puts fputs
獲取字符數 無限制 * <size-1 * n/a n/a
尾部換行 去除 保留 添加 不添加 *
末尾 null 添加 添加 不寫出 不寫出

做個簡單說明:

  • gets 無法指定緩衝區大小從而可能導致緩衝區溢出,不推薦使用
  • fgets 讀取的字符數 (包含末尾換行) 若大於 size-1,則只讀取 size-1,最後一個字符填充 null 返回,下次調用繼續讀取此行;反之將返回完整的字符行 (包含末尾換行) 與結尾 null
  • puts/fputs 輸出一行時不要求必需以換行符結束,puts 會自動添加換行符,fputs 原樣輸出,如果希望在一行內連續打印多個字符串,fputs 是唯一選擇

一次一個記錄

size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
size_t fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);

可以用來直接讀寫簡單類型數組、結構體、結構體數組,其中 size 表明元素尺寸,一般是簡單類型、結構體的 sizeof 結果,nitems 表示數組長度,如果是單元素操作,則為 1。

返回值表示讀寫的元素個數,如果與 nitems 一致則無錯誤發生;如果小於 nitems,對於讀,需要通過 feof 或 ferror 來判斷是否出錯,對於寫,則鐵定是出錯了。

不推薦跨機器傳遞二進制數據,主要是結構尺寸隨操作系統 (位元組順序及表達方式)、編譯器及編譯器選項 (位元組對齊)、程序版本而變化,處理不好可能直接導致應用崩潰,如果有這方面的需求,最好是求助於 grpc、protobuf 等第三方庫。

定位

同 read/write 可以使用 lseek 定位一樣,標準 IO 庫也支持文件定位。

int fseek(FILE *stream, long offset, int whence);
int fseeko(FILE *stream, off_t offset, int whence);

long ftell(FILE *stream);
off_t ftello(FILE *stream);

int fgetpos(FILE *restrict stream, fpos_t *restrict pos);
int fsetpos(FILE *stream, const fpos_t *pos);

void rewind(FILE *stream);

fseek/ftell 用於設置/讀取小於 2G 的文件偏移,fseeko/ftello 可以操作大於 2G 的文件偏移,fsetpos/fgetpos 是 ISO C 的一部分,兼容非 unix like 系統。

fseek/fseeko 的 whence 參數與 lseek 相同,可選擇 SEEK_SET/SEEK_CUR/SEEK_END/SEEK_HOLE…,fseeko 的 off_t 類型是 long 還是 long long 由宏 _FILE_OFFSET_BITS 為 32 還是 64 決定,如果想操作大於 2 GB 的文件,需要定義 _FILE_OFFSET_BITS=64,這個定義同樣會影響 lseek。下面是這個宏的一些說明:

Macro: _FILE_OFFSET_BITS
    This macro determines which file system interface shall be used, one replacing the other. Whereas _LARGEFILE64_SOURCE makes the 64 bit interface available as an additional interface, _FILE_OFFSET_BITS allows the 64 bit interface to replace the old interface.
    If _FILE_OFFSET_BITS is defined to the value 32, the 32 bit interface is used and types like off_t have a size of 32 bits on 32 bit systems.
    If the macro is defined to the value 64, the large file interface replaces the old interface. I.e., the functions are not made available under different names (as they are with _LARGEFILE64_SOURCE). Instead the old function names now reference the new functions, e.g., a call to fseeko now indeed calls fseeko64.
    If the macro is not defined it currently defaults to 32, but this default is planned to change due to a need to update time_t for Y2038 safety, and applications should not rely on the default.
    This macro should only be selected if the system provides mechanisms for handling large files. On 64 bit systems this macro has no effect since the *64 functions are identical to the normal functions.

翻譯一下:文件系統提供了兩套接口,一套是 32 位的 (fseeko32),一套是 64 位的 (fseeko64),_FILE_OFFSET_BITS 的值決定了 fseeko 是調用 fseeko32 還是 fseeko64。如果是 32 位系統,還需要定義 _LARGEFILE64_SOURCE 使能 64 位接口;如果是 64 位系統,則定不定義 _FILE_OFFSET_BITS=64 都行,因為默認已經指向 64 位的了。在一些系統上即使定義了 _FILE_OFFSET_BITS 也不能操作大於 2GB 的文件,此時需要使用 fseek64 或 _llseek,詳見附錄。

下面這個程序演示了使用 fseeko 進行大於 4G 文件的讀寫:

#include "../apue.h"
#include <errno.h>

int main (int argc, char* argv[])
{
  FILE* fp = fopen ("large.dat", "r"); 
  if (fp == 0)
    err_sys ("fopen failed"); 

  int i = 0; 
  off_t ret = 0; 
  off_t pos[2] = { 2u*1024*1024*1024+100 /* 2g more */, 4ull*1024*1024*1024+100 /* 4g more */ }; 
  for (i = 0; i<2; ++ i)
  {
      if (fseeko (fp, pos[i], SEEK_SET) == -1)
      {
          printf ("fseeko failed for %llu, errno %d\n", pos[i], errno); 
      }
      else 
      {
          printf ("fseeko to %llu\n", pos[i]); 
          ret = ftello (fp); 
          printf ("after fseeko: %llu\n", ret); 
      }
  }

  return 0; 
}

讀取的文件事先通過 dd 創建:

$ dd if=/dev/zero of=larget.dat bs=1G count=5
5+0 records in
5+0 records out
5368709120 bytes (5.4 GB) copied, 22.9034 s, 234 MB/s

文件大小是 5G,剛好可以用來驗證大於 2G 和大於 4G 的場景。下面是程序輸出:

$ ./fseeko64
fseeko to 2147483748
after fseeko: 2147483748
fseeko to 4294967396
after fseeko: 4294967396

注意程序中使用了 2u 和 4ull 來分別指定常量類型為 unsigned int 與 unsigned long long,來防止 int 溢出。

在 64 位 linux 上編譯不需要增加額外宏定義:

all: fseeko64

fseeko64: fseeko64.o apue.o
	gcc -Wall -g $^ -o $@

fseeko64.o: fseeko.c ../apue.h 
	gcc -Wall -g -c $< -o $@

apue.o: ../apue.c ../apue.h 
	gcc -Wall -g -c $< -o $@

clean: 
	@echo "start clean..."
	-rm -f *.o core.* *.log *~ *.swp fseeko64 
	@echo "end clean"

.PHONY: clean

在 32 位上需要同時指定兩個宏定義:

all: fseeko32

fseeko32: fseeko32.o apue.o
	gcc -Wall -g $^ -o $@

fseeko32.o: fseeko.c ../apue.h 
	gcc -Wall -g -c $< -o $@  -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE

apue.o: ../apue.c ../apue.h 
	gcc -Wall -g -c $< -o $@

clean: 
	@echo "start clean..."
	-rm -f *.o core.* *.log *~ *.swp fseeko32
	@echo "end clean"

.PHONY: clean

注意在 64 位上無法通過指定 -D_FILE_OFFSET_BITS=32 來訪問 32 位接口。

成功的 fseek/fseeko 清除流的 EOF 標誌,並清除 ungetc 緩衝內容;rewind 等價於 fseek (stream, 0L, SEEK_SET),成功的 rewind 還會清除錯誤標誌。

下面的程序演示了 fseek 的這個特性:

#include "../apue.h"

int main (int argc, char* argv[])
{
  int ret = 0; 
  while (1)
  {
    ret = getc (stdin); 
    if (ret == EOF)
      break; 

    printf ("read %c\n", (unsigned char) ret); 
  }

  if (feof (stdin))
    printf ("reach EndOfFile\n"); 
  else 
    printf ("not reach EndOfFile\n"); 

  if (ferror (stdin))
    printf ("read error\n"); 
  else 
    printf ("not read error\n"); 

  if (fseek (stdin, 13, SEEK_SET) == -1)
    printf ("fseek failed\n"); 
  else 
    printf ("fseek to 13\n"); 

  printf ("after fseek\n"); 
  if (feof (stdin))
    printf ("reach EndOfFile\n"); 
  else 
    printf ("not reach EndOfFile\n"); 

  if (ferror (stdin))
    printf ("read error\n"); 
  else 
    printf ("not read error\n"); 

  int i = 0; 
  char ch = 0; 
  for (i=0; i<26; ++ i)
  {
    ch = 'a'+i; 
    if (ungetc (ch, stdin) != ch)
    {
      printf ("ungetc failed\n"); 
      break; 
    }
    else 
      printf ("ungetc %c\n", ch); 
  }

  if (fseek (stdin, 20, SEEK_SET) == -1)
    printf ("fseek failed\n"); 
  else 
    printf ("fseek to 20\n"); 

  while (1)
  {
    ret = getc (stdin); 
    if (ret == EOF)
      break; 

    printf ("read %c\n", (unsigned char) ret); 
  }

  return 0; 
}

做個簡單說明:

  • 讀取文件直到 eof,將驗證文件處於 EOF 狀態
  • fseek 到文件中某一位置,驗證文件 EOF 狀態清空
  • ungetc 填充回退緩存數據,再次 fseek,驗證 ungetc 緩存清空
  • 從文件當前位置讀取直到結尾

因為需要對輸入進行 fseek,這裡將 stdin 重定向到文件,測試文件中包含由 26 個小寫字母按順序組成的一行內容,下面是程序輸出:

查看代碼
 ./fseek < abc.txt 
read a
read b
read c
read d
read e
read f
read g
read h
read i
read j
read k
read l
read m
read n
read o
read p
read q
read r
read s
read t
read u
read v
read w
read x
read y
read z
read 

reach EndOfFile
not read error
fseek to 13
after fseek
not reach EndOfFile
not read error
ungetc a
ungetc b
ungetc c
ungetc d
ungetc e
ungetc f
ungetc g
ungetc h
ungetc i
ungetc j
ungetc k
ungetc l
ungetc m
ungetc n
ungetc o
ungetc p
ungetc q
ungetc r
ungetc s
ungetc t
ungetc u
ungetc v
ungetc w
ungetc x
ungetc y
ungetc z
fseek to 20
read u
read v
read w
read x
read y
read z
read 

最後只讀取了 6 個字母,證實確實 seek 到了位置 20 且 ungetc 緩存為空 (否則會優先讀取回退緩存中的 26 個字符)。

格式化 (format)

標準 IO 庫的格式化其實是一系列函數組成的函數族,按輸入輸出分為 printf/scanf 兩大類。

printf 函數族

int printf(const char * restrict format, ...);
int fprintf(FILE * restrict stream, const char * restrict format, ...);
int sprintf(char * restrict str, const char * restrict format, ...);
int snprintf(char * restrict str, size_t size, const char * restrict format, ...);
int asprintf(char **ret, const char *format, ...);
  • printf 等價於 fprintf (stdin, format, …)
  • sprintf 將變量打印到字符緩衝區,便於後續進一步處理。它在緩衝區末尾添加一個 null 字符,但這個字符不計入返回的字符數中
  • snprintf 在 sprintf 的基礎上增加了越界檢查,超過緩衝區尾端的任何字符都會被丟棄
  • asprintf 在 sprintf 的基礎上增加了緩衝區自動分配 (malloc),通過 *ret 參數獲取,緩衝區的銷毀 (free) 是調用者的責任

以上接口返回負數表示編碼錯誤。

重點關注一下 snprintf,如果返回的字符數大於等於 size 參數,則表明發生了截斷,如果以 result 代表生成的總字符數、size 代表緩衝區大小,那麼可以分以下幾種情況討論:

  • result == size,因末尾補 null 原則,實際只能寫入 size-1 個字符,返回 result == size
  • result < size,因末尾補 null,實際寫入 result+1 個字符 <= size,返回 result < size
  • result > size,因末尾補 null,實際寫入 size-1 個字符,返回 result > size

綜上,在發生截斷時 result >= size。其實關鍵就在理解等於的情況——因末尾補 null 佔用了一個字符,導致寫入的字符少了一個從而發生截斷——即有一個字符因為末尾 null 被擠出去了。

上面列出的不是 printf 函數族的全部,如果考慮 va_list 的話,它還有差不多數量的版本:

int vprintf(const char * restrict format, va_list ap);
int vfprintf(FILE * restrict stream, const char * restrict format, va_list ap);
int vsprintf(char * restrict str, const char * restrict format, va_list ap);
int vsnprintf(char * restrict str, size_t size, const char * restrict format, va_list ap);
int vasprintf(char **ret, const char *format, va_list ap);

區別只是將變長參數 (…) 換作為了 va_list,適用於已經將變長參數轉換為 va_list 的場景,因為這一轉換是單向的。

printf format

所有 printf 函數族都接受統一的 format 格式,它遵循下面的格式:

% [flags] [fldwidth] [precision] [lenmodifier] convtype
  • flags:支持 +/-/space/#/0 等符號,用於控制對齊、前導符號填充、前綴等
  • fldwidth:用於說明轉換的最小字段寬度,可以設置為非負十進制數或星號 (*),當為後者時,寬度由被轉換參數的前一個整型參數指定
  • precision:用於指定精度,格式為 .NNN 或 .*,NNN 為整型數字,星號作用同上。用於說明:
    • 整型轉換後最少輸出的數字位數
    • 浮點轉換後小數點後的最少位數
    • 字符串轉換後的最大字符數
  • lenmodifier:支持 hh/h/l/ll/j/z/t/L 等符號,用於說明參數長度 (及是否有符號)
  • convtype:支持 d/i/o/u/x/X/f/F/g/G/a/A/c/s/p/n/%/C/S 等符號,控制如何解釋參數
flags
標誌 說明
在字段內左對齊輸出 (默認右對齊)
+ 總是顯示帶符號轉換的符號 (即顯示正負號)
space 如果第一個字符不是符號,則在其前面加上一個空格
# 指定另一種轉換形式 (十六進制加 0x 前綴)
0 添加前導 0 (而非空格) 進行對齊

這裡對 # 做個單獨說明,直接上代碼:

printf ("#: %#5d, %#5x, %#5o\n", 42, 42, 42);

同樣的數據,使用 d/x/o 不同的轉換類型 (轉換類型請參考下面的小節) 指定輸出 10/16/8 進制時,# 可以為它們添加合適的前綴 (無/0x/0):

#:    42,  0x2a,   052
lenmodifier
修飾符 說明
hh 有符號 (d/i) 或無符號 (u) 的 char
h 有符號 (d/i) 或無符號 (u) 的 short
l 有符號 (d/i) 或無符號 (u) 的 long 或寬字符
ll 有符號 (d/i) 或無符號 (u) 的 long long
j intmax_t 或 uintmax_t
z size_t
t ptrdiff_t
L long double

大部分人對於 lu/llu/ld/lld 更熟悉一些,如果只想打印一個整型的低兩位位元組或者最低位位元組,可以用 hu/hd/hhu/hhd 代替強制轉換。

對於 size_t/ptrdiff_t 等隨系統位數變更長度的類型,不好指定 %lu 還是 %llu,因此統一使用單獨的 %zu 及 %tu 代替。

除了可以用 %ld 或 %lu 指定長整數外,還可以通過 %lc 與 %ls 指定寬字符與寬字符串,以及 %Lf 或 %LF 指定長精度浮點。

%j 對應的 intmax_t 和 uintmax_t 是兩種獨立的類型,用來表示標準庫支持的最大有符號整型和無符號整型,目前流行的系統支持的最大整數是 64 位,不過不排除將來擴展到 128 位、256 位… 無論位數如何擴展,intmax_t/uintmax_t 都可以指向系統支持的最大位數整型,不過目前支持的並不是非常好,不建議使用,原因參考附錄。

convtype
轉換類型 說明
d, i 有符號十進制
o 無符號八進制
u 無符號十進制
x, X 無符號十六進制
f, F double 精度浮點
e, E 指數格式的 double 精度浮點
g, G 解釋為 f/F/e/E,取決於被轉換的值
a, A 十六進制指數格式的 double 精度浮點數
c 字符
s 字符串
p 指向 void 的指針
n 將到目前為止所寫的字符數寫入到指針所指向的無符號整型中
% % 符號自身
C 寬字符,等價於 lc
S 寬字符串,等價於 ls

這裡對 %n 做個單獨說明,它可以將當前已經轉換的字符數寫入調用者提供的指向整型的指針中,用戶可以根據得到的字符數排除輸出數據中前 N 個轉換成功的字符,方便出問題時縮小排查範圍、快速定位轉換失敗位置。

scanf 函數族

int fscanf(FILE *restrict stream, const char *restrict format, ...);
int scanf(const char *restrict format, ...);
int sscanf(const char *restrict s, const char *restrict format, ...);

int vfscanf(FILE *restrict stream, const char *restrict format, va_list arg);
int vscanf(const char *restrict format, va_list arg);
int vsscanf(const char *restrict s, const char *restrict format, va_list arg);

因為不需要提供輸出緩衝區,scanf 函數族的數量大為精簡:

  • scanf 等價於 fscanf (stdint, format, …);
  • sscanf 從緩衝區中獲取變量的值而不是標準 IO,便於對已經從 IO 獲取的數據進行處理
  • v 前綴的接口接受 va_list 參數代替可變參數 (…)

以上接口返回 EOF 表示遇到文件結尾或轉換出錯。

scanf format

所有 scanf 函數族都接受統一的 format 格式,它遵循下面的格式:

% [*] [fldwidth] [lenmodifier] convtype
  • *:用於抑制轉換,按照轉換說明的部分進行轉換,但轉換結果並不存放在參數中,適用於測試的場景
  • fldwidth:用於說明轉換的最大字段寬度,含義剛好與 printf 函數族中的相反,類似前者的 precision。
  • lenmodifier:支持 hh/h/l/ll/j/z/t/L 等符號,用於說明要用轉換結果初始化的參數大小 (及是否有符號),與 printf 函數族的用法相同
  • convtype:支持 d/i/o/u/x/f/F/g/G/a/A/e/E/c/s/[]/[^]//p/n/%/C/S 等符號,控制如何解釋參數
convtype
轉換類型 說明
d 有符號十進制,基數為 10
i 有符號十進制,基數由輸入格式決定 (0x/0…)
o 無符號八進制 (輸入可選的有符號)
u 無符號十進制,基數為 10 (輸入可選的有符號)
x 無符號十六進制 (輸入可選的有符號)
a, A, e, E, f, F, g, G 浮點數
c 字符
s 字符串
[ 匹配列出的字符序列,以 ] 終止
[^ 匹配除列出的字符以外的所有字符,以 ] 終止
p 指向 void 的指針
n 將到目前為止讀取的字符數寫入到指針所指向的無符號整型中
% % 字符本身
C 寬字符,等價於 lc
S 寬字符串,等價於 ls

與 printf 中的 convtype 的最大不同之處,是可以為無符號轉換類型提供有符號的數據,例如 scanf (“%u”, &longval) 將 -1 轉換為 4294967295 存放在 int 整型中。

另外還有幾品點不同:

  • %d/%i 含義不同,%d 僅能解析十進制數據,%i 可以解析 10/16/8 進制數據,取決於輸入數據的前綴
  • %[a-zA-Z0-9_] 指定的範圍可以讀取一個由字母數字下劃線組成的分詞,[] 中可以設置任何想要的字符
  • %[^a-zA-Z0-9_] 剛好相反,遇到字母數字下劃線則停止解析,[^] 中也可以設置任何不想要的字符中斷解析

臨時文件

不同的標準定義的臨時文件函數不同,下面分別說明。

ISO C

ISO C 提供了兩個函數用於幫助創建臨時文件:

char *tmpnam(char *s);
FILE *tmpfile(void);
  • tmpnam:只負責產生唯一的文件名,打開過程由調用者負責
  • tmpfile:以 “wb+” 方式打開一個臨時文件流,調用者可以直接使用

tmpnam 的參數 s 用於存儲生成的臨時文件名,要求它指向的緩衝區長度至少是 L_tmpnam (CentOS 上是 20),生成成功時返回 s 給調用者。如果這個參數為 NULL,tmpnam 將使用內部的靜態存儲區記錄臨時文件名並返回,這樣一來將導致 tmpnam 不是可重入的,既不線程安全也不信號安全。特別是連續生成多個臨時文件名並分別保存指針的做法,所有指針都將指向最後一個文件名,這一點需要注意。

tmpfile 可以理解為在 tmpnam 的基礎上做了一些額外的工作:

FILE* tmpfile(void)
{
    FILE* fp = NULL; 
    char tmp[L_tmpnam] = { 0 }; 
    char *ptr = tempnam (tmp); 
    if (ptr != NULL)
    {
        fp = fopen (ptr, "wb+"); 
        if (fp != NULL)
            unlink (ptr); 
    }

    return fp;
}

上面偽代碼的關鍵點就是自動打開臨時文件並立即 unlink,以便在關閉文件流時系統自動刪除臨時文件。

雖然演示代碼跨越了兩個調用,實際上這個接口是原子的,它比 tmpnam + fopen 更安全,後者仍有一定的機率遇到進程間競爭導致的同名文件存在的問題,因此推薦使用前者。

tmpnam 生成的臨時文件格式為:/tmp/fileXXXXXX,每個文件後綴由 6 個隨機的數字、大小寫字母組成,理論上可以生成 C626=61474519 (62=大寫字母 26+小寫字母 26+數字 10)。不過每個系統都會限制 tmpnam 不重複生成臨時文件名的上限,這由 TMP_MAX 指明 (CentOS 上是 238328)。

XSI

Single UNIX Specification 的 XSI 擴展定義了另外的臨時文件處理函數:

char *tempnam(const char *dir, const char *pfx);
char *mktemp(char *template);
int mkstemp(char *template);

類比 ISO C 提供的接口:

  • tempnam 與 mktemp 均生成文件名不創建文件,類似於 tmpnam
  • mkstemp 直接生成文件,類似於 tmpfile

不過有一些細節不同,下面分別說明。

tempnam 可以指定生成的臨時文件名目錄和前綴,目錄規則由以下邏輯決定:

  1. 定義了 TMPDIR 環境變量且存在,用它;
  2. 參數 dir 非 NULL 且存在,用它;
  3. 常量 P_tmpdir (CentOS 上為 /tmp) 指定的目錄存在,用它;
  4. 使用系統臨時目錄作為目錄,通常是 /tmp。

系統臨時目錄 /tmp 作為保底策略時回退到和 tmpnam 相同的目錄。需要注意,若提供的 dir 參數不起作為,可以檢查

  • dir 指向的目錄是否存在
  • 是否定義了 TMPDIR 環境變量

tempnam 的 pfx 參數指定臨時文件前綴,至多使用這個參數的前 5 個字符,剩餘部分將由系統隨機生成來保證唯一性,例如以 cnblogs 作為前綴,生成的文件名可能是 cnbloaslfBV,即 cnbloXXXXXX 的形式,隨機部分長度與形式和 tmpnam 保持一致。不提供 pfx 參數時 (指定 NULL),使用前綴 file 作為默認值。返回值由 malloc 分配,使用後需要調用者釋放 (free),這避免了 tmpnam 使用靜態存儲區的弊端。

mktemp 也不限制臨時文件目錄,它採取的是另外一種策略:由用戶提供臨時文件的完整路徑,只在末尾預留 6 個 X 字符以備系統改寫,改寫後的 template 參數用作返回值,調用失敗時會清空 template 中的內容。整個過程只操作用戶提供的存儲空間,既無靜態存儲區,也無內存的分配和釋放,在存儲空間方面幾乎是最優雅的解決方案。以 /home/yunh/code/apue/05.chapter/this_is_a_temp_name_XXXXXX 為例,生成的文件名為 /home/yunh/code/apue/05.chapter/this_is_a_temp_name_Rb89wh,隨機變化的部分同 tmpnam 和 tempnam,如果沒有將 XXXXXX 放在文件名末尾,或末尾的 X 字符數不足 6 個,則直接返回參數非法 (22) 的錯誤。

mkstemp 的臨時文件命名規則與 mktemp 完全一致,可以理解為 mktemp + open 的組合,與 tmpfile = tmpnam + fopen 相似,不同的是:

  • mkstemp 返迴文件句柄 (fd) 而不是 FILE*
  • mkstemp 打開文件後沒有自動 unlink,關閉臨時文件句柄後文件不會自動刪除,需要手動調用 unlink 清理,文件路徑可以直接通過更新後的 template 參數獲取

以上就是  ISO C 與 SUS XSI 提供的臨時文件接口,如果只在 *nix 系統上開發,可以使用強大的 XSI 接口;如果需要兼容 windows 等非 *nix 系統,最好使用 ISO C 接口。

結語

標準 IO 庫固然優秀,但是也有一些後來者嘗試改進它,主要是以下幾種:

  • 減少數據複製,提高效率
    • fio:讀取時直接返回 IO 庫內部存儲地址,減少一次數據拷貝
    • sfio:性能與 fio 差不多,提供存儲區流、流處理模塊、異常處理等功能。可以對一個流壓入 (push) 處理模塊,這一點非常類似 Solaris 的 STREAMS 系統,可參考 《[apue] 神奇的 Solaris pipe
    • ASI:使用 mmap 提高性能,接口類似於存儲分配函數 (malloc/realloc/free)
  • 嵌入式等內存受限系統環境下更好的工作 (IO 庫直接實現為 C 庫的一部分)
    • uClibc
    • newlibc

原書提到的這幾個庫基本是老古董了,有一些早已停止更新,相對於性能提升,stdio 帶來的通用性、可移植性它們無法取代的,不建議替換。不過作為一個審視標準 IO 庫缺點的視角,還是有一定意義的,感興趣的讀者可以自行搜索相關資訊。

參考

[1]. linux編程 fmemopen函數打開一個內存流 使用FILE指針進行讀寫訪問

[2]. 文件輸入/輸出 | File input/output

[3]. 走進C標準庫(3)——”stdio.h”中的getc和ungetc

[4]. linux下如何通過lseek定位大文件

[5]. 對大文件寫操作時謹慎使用fseek/lseek

[6]. lseek64的使用

[7]. 組合排列在線計算器

[8]. 32位Linux下使用2G以上大文件的幾個相關宏的關係

[9]. A Special Kind of Hell – intmax_t in C and C++

[10]. Are the benefits of SFIO over STDIO still valid?

[11]. 關於setvbuf()函數的詳解

[12]. setbuf函數詳解

[13]. setvbuf – cppreference.com