<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      [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 用于打開已有的內存作為流

      大部分打開操作都需要提供 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 提供了一種便利,將已有的 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 驗證。

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

      int fileno(FILE *stream);

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

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

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

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

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

      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

      posted @ 2022-09-20 11:58  goodcitizen  閱讀(872)  評論(0)    收藏  舉報
      主站蜘蛛池模板: caoporn免费视频公开| 国产精品成人免费视频网站京东| 中文字幕精品亚洲二区| 国产免费午夜福利蜜芽无码| 亚洲精品一区二区二三区| 国产日韩av免费无码一区二区三区| 无码AV中文字幕久久专区| 伊人成色综合人夜夜久久| 日韩精品无码一区二区三区视频| 亚洲乱码一卡二卡卡3卡4卡| 好紧好滑好湿好爽免费视频| 亚洲成在人天堂一区二区| 国产高清在线精品一区不卡| 国产欧美日韩精品丝袜高跟鞋| 日韩精品人妻av一区二区三区| 亚洲中文字幕av天堂| 人人人澡人人肉久久精品| 国产精品性视频一区二区| 无遮挡粉嫩小泬久久久久久久| 亚洲av成人三区国产精品| 国产精品无码mv在线观看| 精品人妻少妇一区二区三区| 国产欧美日韩综合精品一区二区| 岛国一区二区三区高清视频| 香蕉EEWW99国产精选免费| 国产在线观看免费观看不卡| 青青草原网站在线观看| 少妇被粗大的猛烈进出69影院一 | 久久er热在这里只有精品66| 久久精品国产亚洲av品| 性色av一区二区三区精品| 精品无码国产自产拍在线观看蜜| 亚洲乱码一二三四区国产| 人妻综合专区第一页| 体态丰腴的微胖熟女的特征| 97精品人妻系列无码人妻 | 成人性做爰aaa片免费看| 日韩卡一卡2卡3卡4卡| 国产精品v片在线观看不卡| 一区二区三区鲁丝不卡| 亚洲精品第一页中文字幕|