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

来源:https://www.cnblogs.com/goodcitizen/archive/2022/09/20/things_about_standard_input_and_output.html
-Advertisement-
Play Games

提起標準 IO 庫,第一印象就是 printf/scanf,這有什麼可說的?但是一個流是如何處理寬窄字元集、緩存方式的?如何在程式內部將標準輸出重定向到文件?FILE* 與 fd 是如何相互轉換的?在處理大文件時 fseek/fseeko/fsetpos 有何區別?創建臨時文件時 tmpnam/te... ...


前言

標準 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 [email protected]

fwide.o: fwide.c ../apue.h 
	gcc -Wall -g -c $< -o [email protected] 

fwidew: fwidew.o apue.o
	gcc -Wall -g $^ -o [email protected]

fwidew.o: fwide.c ../apue.h 
	gcc -Wall -g -c $< -o [email protected] -DUSE_WCHAR

apue.o: ../apue.c ../apue.h 
	gcc -Wall -g -c $< -o [email protected]

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 [email protected]

fseeko64.o: fseeko.c ../apue.h 
	gcc -Wall -g -c $< -o [email protected]

apue.o: ../apue.c ../apue.h 
	gcc -Wall -g -c $< -o [email protected]

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 [email protected]

fseeko32.o: fseeko.c ../apue.h 
	gcc -Wall -g -c $< -o [email protected]  -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE

apue.o: ../apue.c ../apue.h 
	gcc -Wall -g -c $< -o [email protected]

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 的基礎上增加了越界檢查,超過緩衝區尾端的任
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 很多人學編程經常是腦子一熱然後就去網上一搜資源就開始學習了,但學到了後面發現目前所學的東西並不是自己最喜歡的,好像自己更喜歡另一個技術,感覺自己學錯了,於是乎又去學習別的東西。 結果竹籃打水一場空,前面所付出的努力都白費了,甚至有人還花了錢買了課,這個實在是划不來。 所以在你學一門編程語言之前,一定 ...
  • 配置訪問介面 public IConfiguration _Config; public 類名 (IConfiguration Config) { _Config = Config; } 配置文件數據示例 { "AllowedHosts": "*", "Users": [ { "Id": "123" ...
  • 使用 Buffered Paint API 繪製帶有淡入淡出動畫的控制項 發表於2011 年 10 月 23 日 Windows 窗體提供了許多機制來構建與操作系統風格相匹配的專業自定義 UI 控制項;通過結合視覺風格渲染器、系統顏色/畫筆、ControlPaint類等,可以在用戶代碼中重現大多數標準 ...
  • 一:背景 1. 講故事 前段時間有位朋友在分析他的非托管泄漏時,發現NT堆的_HEAP_ENTRY 的 Size 和 !heap 命令中的 Size 對不上,來咨詢是怎麼回事? 比如下麵這段輸出: 0:000> !heap 0000000000550000 -a Index Address Name ...
  • 本文技術方案支持.Net/.Net Core/.Net Framework 數據分頁,幾乎是任何應用系統的必備功能。但當數據量較大時,分頁操作的效率就會變得很低。大數據量分頁時,一個操作耗時5秒、10秒、甚至更長時間都是有可能的,但這在用戶使用的角度是不可接受的…… 數據分頁往往有三種常用方案。 第 ...
  • 上課筆記 文件系統結構 /根目錄 /bin/ 存放系統命令,普通用戶與root都可以執行 /etc/ 配置文件保存位置 /lib/ 系統調用的函數庫保存位置 /var/ 目錄用於存儲動態數據,例如緩存、日誌文件、軟體運行過程中產生的文件等 /home/ 普通用戶目錄 /proc/ 配置文件目錄 /r ...
  • 摘要:一鍵創建實驗環境,開發者通過實驗手冊指導,快速體驗華為雲IoT服務,在雲端即可實現雲服務的實踐、調測和驗證等開發流程。 本文分享自華為雲社區《物聯網雲上實驗上新,帶您深度體驗華為雲IoT服務》,作者:華為IoT雲服務。 華為雲IoT沙箱實驗新品上線,誠邀廣大開發者參與體驗。一鍵創建實驗環境,開 ...
  • EndNote X9 for Mac是一款非常值得推薦的文獻管理軟體,不僅可以讓您免於手動收集和整理您的研究資料和格式化書目的繁瑣工作,還可以讓您在與同事協調時更加輕鬆自如。讓你的團隊專註科研,更高效的共用文獻開展協作。 詳情:EndNote X9 for Mac(最好用的文獻管理軟體) 引文報告 ...
一周排行
    -Advertisement-
    Play Games
  • Github / Gitee QQ群(1群) : 813100564 / QQ群(2群) : 579033769 視頻教學 介紹 MiniWord .NET Word模板引擎,藉由Word模板和數據簡單、快速生成文件。 Getting Started 安裝 nuget link : https:// ...
  • Array.Sort Array類中相當實用的我認為是Sort方法,相比起冗長的冒泡排序,它的出現讓排序更加的簡化 結果如下: 還可以聲明一個靜態方法用來專門調用指定數組排序,從名為 array 的一維數組中 a 索引處開始,到 b 元素 從小到大排序。 註意: a + b 不能大於 array 的 ...
  • 前言 在上一篇文章CLR類型系統概述里提到,當運行時掛起時, 垃圾回收會執行堆棧遍歷器(stack walker)去拿到堆棧上值類型的大小和堆棧根。這裡我們來翻譯BotR里一篇專門介紹Stackwalking的文章,希望能加深理解。 順便說一句,StackWalker在中文里似乎還沒有統一的翻譯,J ...
  • 使用過 nginx 的小伙伴應該都知道,這個中間件是可以設置跨域的,作為今天的主角,同樣的 反向代理中間件的 YARP 毫無意外也支持了跨域請求設置。 有些小伙伴可能會問了,怎樣才算是跨域呢? 在 HTML 中,一些標簽,例如 img、a 等,還有我們非常熟悉的 Ajax,都是可以指向非本站的資源的 ...
  • 什麼是Git Git 是一個開源的分散式版本控制系統,用於敏捷高效地處理任何或小或大的項目。 Git 是 Linus Torvalds 為了幫助管理 Linux 內核開發而開發的一個開放源碼的版本控制軟體。 Git 與常用的版本控制工具 CVS, Subversion 等不同,它採用了分散式版本庫的 ...
  • 首先CR3是什麼,CR3是一個寄存器,該寄存器內保存有頁目錄表物理地址(PDBR地址),其實CR3內部存放的就是頁目錄表的記憶體基地址,運用CR3切換可實現對特定進程記憶體地址的強制讀寫操作,此類讀寫屬於有痕讀寫,多數驅動保護都會將這個地址改為無效,此時CR3讀寫就失效了,當然如果能找到CR3的正確地址... ...
  • 說明 onlyoffice為一款開源的office線上編輯組件,提供word/excel/ppt編輯保存操作 以下操作均基於centos8系統,officeonly鏡像版本7.1.2.23 鏡像下載地址:https://yunpan.360.cn/surl_y87CKKcPdY4 (提取碼:1f92 ...
  • 二叉樹查找指定的節點 前序查找的思路 1.先判斷當前節點的no是否等於要查找的 2.如果是相等,則返回當前節點 3.如果不等,則判斷當前節點的左子節點是否為空,如果不為空,則遞歸前序查找 4.如果左遞歸前序查找,找到節點,則返回,否繼續判斷,當前的節點的右子節點是否為空,如果不為空,則繼續向右遞歸前 ...
  • ##Invalid bound statement (not found)出現原因和解決方法 ###前言: 想必各位小伙伴在碼路上經常會碰到奇奇怪怪的事情,比如出現Invalid bound statement (not found),那今天我就來分析以下出現此問題的原因。 其實出現這個問題實質就是 ...
  • ###一、背景知識 爬蟲的本質就是一個socket客戶端與服務端的通信過程,如果我們有多個url待爬取,只用一個線程且採用串列的方式執行,那隻能等待爬取一個結束後才能繼續下一個,效率會非常低。 需要強調的是:對於單線程下串列N個任務,並不完全等同於低效,如果這N個任務都是純計算的任務,那麼該線程對c ...