UNIX高級環境編程(6)標準IO函數庫 - 流的概念和操做

標準IO函數庫隱藏了buffer大小和分配的細節,使得咱們能夠不用關心預分配的內存大小是否正確的問題。緩存

雖然這使得這個函數庫很容易用,可是若是咱們對函數的原理不熟悉的話,也容易遇到不少問題。網絡

 

1 流和FILE實體(Streams and FILE Objects)

前面的章節中,IO集中在文件描述符,每個打開的文件都對應一個文件描述符,經過文件描述符對文件進行操做。app

如今使用了標準IO庫,討論的重點集中在流(streams)。ide

簡要了解一下流:函數

  • 當咱們打開或建立了一個文件,咱們說咱們有一個流和該文件關聯。
  • stream支持單字節字符集和多字節字符集。stream的屬性orientation決定使用單字符集仍是多字符集。
  • 當一個stream被建立時,沒有指定orientation,這時,當使用寬字符集IO函數時,流的orientation設置爲支持寬字符集;當使用單字符集IO函數時,流的orientation設置爲支持單字符集。

只有兩個函數能夠修改流的orientation:測試

  • freopen會清除流的orientation;
  • fwide用來設置流的orientation。

fwide函數聲明:spa

#include <stdio.h>3d

#include <wchar.h>指針

int fwide(FILE* fp, int mode);rest

函數返回值:

  • 返回整數表示支持多字節字符集;
  • 返回負數表示支持單字節字符集;
  • 返回0表示沒有設置stream的orientation。

mode取值的不一樣決定函數fwide的不一樣的行爲:

  • 若是mode爲負數,fwide試着設置指定流支持單字節字符集;
  • 若是mode爲整數,fwide試着設置指定流支持多字節字符集;
  • 若是mode爲0,fwide不會試着設置流的orientation,可是會返回一個值表明當前流的orientation。

當咱們打開一個流,函數fopen返回一個指向FILE對象的指針。FILE對象一般是一個結構體,包含全部控制流所須要的信息,包括:

  • 實際IO所用的文件描述符;
  • 一個指向流所使用的buffer的指針;
  • buffer的大小;
  • 當前在buffer中的字符數;
  • error flag;
  • 等。

 

2 緩存(Buffering)

緩存(buffering)的做用是爲了儘量少地調用read和write系統調用。

標準IO庫提供三種類型的buffering:

徹底緩存(Fully buffered):在這種緩存機制中,實際的IO操做發生在緩存被寫滿時。正在寫入硬盤的文件被徹底緩存在buffer中。緩存空間每每在第一次IO操做時經過調用malloc函數獲取;

行緩存(Line buffered):在這種緩存機制中,實際的IO操做發生在新的一行字符被讀入或者輸出時,因此容許每一次只輸出一個字符。行緩存有兩點須要注意:buffer的大小是固定的,因此即便當前行沒有讀入或輸出結束,依然可能發生實際的IO,當buffer被寫滿時;一旦有輸入(從無緩存流或者行緩存流中輸入)發生,因此已在buffer中緩存的輸出流都會被馬上輸出(flush)。

flush:標準IO緩存中內容馬上寫入硬盤或者輸出。在終端設備中,flush的做用也多是丟棄緩存中得數據。

無緩存(Unbuffered):不緩存輸入或輸出內容。例如,若是咱們使用fputs函數輸出15個字符,那麼咱們但願這15個字符儘量快地被打印出來。如標準錯誤輸出就要求是無緩存輸出。

ISO C標準要求下面的緩存特性:

  1. 標準輸入輸出在不關聯交互設備的請款下,使用徹底緩存(fully buffered);
  2. 標準錯誤輸出不使用徹底緩存。

上面的標準顯然沒有具體說明各類狀況,通常來講:

  1. 標準錯誤輸出不適用緩存;
  2. 其餘流,若是關聯終端,則使用行緩存,不然使用徹底緩存。

咱們可使用函數setbuf和setvbuf函數更改流的緩存機制。

函數聲明

#include <stdio.h>

void setbuf(FILE* restrict fp, char* restrict buf);

int servbuf(FILE *restrict fp, char* restrict buf, int mode, size_t size);

函數返回值:

  • OK:0;
  • Error:非0

這些函數必須在流打開以後,其餘流操做執行以前被調用。

函數做用:

setbuf能夠打開或關閉緩存,打開緩存時,buf指向一個大小爲BUFSIZ(stdio.h中定義的宏)的buffer,一般打開的時徹底緩存,若是當前流關聯的是終端設備,有的系統也會使用行緩存;

servbuf能夠指定打開哪一種類型的緩存。mode的參數能夠取以下的值,若是指定爲無緩存,則參數buf和size都會被忽略。

NewImage

函數行爲總結以下表所示:

NewImage

一般來講,咱們應該讓系統本身選擇buffer大小並自動分配,這樣標準IO庫會在關閉流時自動釋放該內存。

 

flush函數。

函數聲明:

#include <stdio.h>

int fflush(FILE *fp);

函數做用:

使得該流的全部緩存中未寫入硬盤的數據傳入內核中。

一種特殊狀況是,若是fp爲NULL,fflush會使得全部緩存的數據都被flush。

 

3 打開流(opening a stream)

函數fopen、freopen和fdopen函數用來打開一個標準輸入輸出流。

函數聲明:

#include <stdio.h>

FILE *fopen(const char *restrict pathname, const char* restrict type);

FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);

FILE *fdopen(int fd, const char *type);

函數細節:

  • 函數fopen打開指定的文件;
  • 函數freopen函數打開指定的文件到指定的流上,若是該流已經被打開,則先關閉該流;若是以前已經被打開的流設置了orientation,則清理。函數freopen一般用來打開文件到預約義的流上,如標準輸入,標準輸出或標準錯誤輸出;
  • fdopen輸入一個文件描述符,將描述符關聯到一個標準IO流上。函數fdopen的做用主要是爲了將管道和網絡鏈接關聯到一個流上,而這些特殊類型的文件不能使用fopen函數打開,咱們必須先用特定的函數獲取文件描述符,而後用fdopen函數關聯到一個流上。

參數type取值以下表所示,一共有15種取值,有得取值做用相同:

NewImage

表格說明:

  • 參數中的b字符爲了讓標準IO系統區分文本文件(text file)和二進制文件(binary file),由於內核並不區分文件文件和二進制文件,因此b字符並不影響內核的行爲。
  • 函數fdopen的參數type和其餘的稍有不一樣。由於文件描述符已經被打開,因此打開文件流並不截斷文件至長度爲0。
  • 標準IO庫函數的append模式不能夠用來建立新文件,由於要獲得一個文件描述符,必須先打開一個存在的文件。
  • 一樣支持多進程同時以append模式寫同一個文件。

當打開一個流對文件進行讀寫時,有兩個限制:

  • 輸入後,若是不調用函數fflush, fseek, fsetpos或rewind的話,不能夠緊接着進行輸出。
  • 輸出後,若是不調用該函數fseek, fsetpos或rewind的話,不能夠緊接着進行輸入。

六種方式打開一個流總結以下表所示:

NewImage

須要注意的一點是,當以w和a模式建立一個新文件時,並不能像open或create函數同樣指定文件的權限標誌位。

一種解決方法是經過調整咱們的umask。

打開的流默認的是徹底緩存,若是該流關聯的是終端設備,則是行緩存。

像以前提到的那樣,咱們打開了一個流,並在其餘操做以前,能夠調用setbuf或setvbuf函數修改緩存方式。

關閉流

函數聲明:

#include <stdio.h>

int fclose(FILE* fp);

函數細節,關閉流以前:

  • 全部緩存待輸出的數據都會被輸出;
  • 全部緩存帶輸入的數據都會被丟棄;
  • 若是流使用的緩存是由標準IO庫分配,則緩存會被釋放;
  • 若是進程正常終止,則全部緩存數據都會被flush(輸出或者寫入硬盤),而且全部打開的流都會被關閉。

4 讀寫一個流(Reading and Writing a Stream) 

當咱們打開一個流,咱們有三種讀寫方式可供選擇:

  • 一次一個字符讀寫
  • 一次一行讀寫:使用函數fgets和fputs
  • 直接讀寫:每次讀寫固定長度的數據,使用函數fread和fwrite。

輸入函數

函數聲明:

#include <stdio.h>

int getc(FILE* fp);

int fgetc(FILE* fp);

int getchar(void);

函數返回值:

  • ok:下一個字符
  • EOF:文件結尾,通常爲-1
  • error:負數

函數細節:

  • getchar和getc不一樣的地方在於:前者必定實現爲函數,然後者能夠被實現爲一個宏;
  • 函數返回值將unsigned char轉型爲int,這裏,unsigned是爲了轉型爲int時不會是負數。返回整數的目的是爲了讓全部可能的值均可以返回,包括錯誤碼和文件結尾;
  • 文件結尾符EOF每每定義爲負數,而錯誤碼也是負數,所以咱們沒法從返回值上判斷是到達了文件結尾仍是報錯。
  • 爲了區分上面的兩種狀況,咱們須要調用函數ferror或者feof。

 

函數聲明:

#include <stdio.h>

int ferror(FILE* fp);

int feof(FILE* fp);    // Both return: nonzero(true) if condition is true, 0(false) otherwise

void clearerr(FILE* fp);

在大多的實現中,FILE對象中會維護兩個flag:

  • 一個error flag
  • 一個文件結尾符flag

這兩個flag均可以經過調用clearerr清空。

 

讀取一個流後,咱們能夠調用函數ungetc壓回讀出來的字符。

函數聲明:

#include <stdio.h>

int ungetc(int c, FILE* fp);

函數返回值:c if OK, EOF on error

函數細節:只支持單個個字符的壓回。

使用場景:

壓回操做常使用在下面的場景:對於一個輸入流,咱們須要根據下一個字符來判斷該如何處理當前的字符。

 

輸出函數

輸出函數和咱們討論過的輸入函數一一對應,再也不贅述。

函數聲明:

#include <stdio.h>

int putc(int c, FILE* fp);

int fputc(int c, FILE* fp);

int putchar(int c);

 

5 逐行輸入輸出操做(Line-at-a-Time IO)

函數fgets和gets提供了逐行輸入功能。

函數聲明:

#include <stdio.h>

char *fgets(char* restrict buf, int n, FILE* restrict fp);

char *gets(char* buf);

函數細節:

  • 兩個函數都是讀取一行數據至buffer中。
  • 函數gets從標準輸入流中讀取,fgets從指定的輸入流中讀取。
  • fgets須要咱們指定緩衝區大小,讀入的一行數據不得多於n-1個字符,以NULL結尾。若是fgets讀取該行數據長度大於n,則該次只讀取n-1個字符,並以null結尾,剩餘的字符在下次調用fgets時讀入。
  • gets函數不推薦使用,由於它不作越界檢查。

函數fputs和puts提供了逐行輸出的功能。

函數聲明:

#include <stdio.h>

int fputs(const char* restrict str, FILE* restrict fp);

int puts(const char* str);

函數細節:

  • fputs函數將一個以null結尾的字符串輸出到指定流中,最後的null byte並不輸出;
  • puts函數一樣會輸出一個以null結尾的字符串到標準輸出,最後的null byte並不輸出,輸出結束後會輸出一個換行符;
  • 因此咱們也不推薦使用puts函數,防止自動輸出一個換行符,可是咱們在使用fputs時要記得在必要的時候本身處理換行符。

 

6 標準輸入輸出效率分析

比較標準:

將必定量的數據從標準輸入拷貝到標準輸出,計算這一過程所須要的

  • 用戶CPU時間(User CPU)
  • 系統CPU時間(System CPU)
  • Clock time
  • 程序文本大小

Code:

使用getc和putc的版本:

#include "apue.h"

 

int

main(void)

{

    int     c;

 

    while ((c = getc(stdin)) != EOF)

        if (putc(c, stdout) == EOF)

            err_sys("output error");

 

    if (ferror(stdin))

        err_sys("input error");

 

    exit(0);

}

使用fgets和fputs的版本:

#include "apue.h"

 

int

main(void)

{

    char    buf[MAXLINE];

 

    while (fgets(buf, MAXLINE, stdin) != NULL)

        if (fputs(buf, stdout) == EOF)

            err_sys("output error");

 

    if (ferror(stdin))

        err_sys("input error");

 

    exit(0);

}

測試數據:95.8M 3百萬行

測試結果(和第三章中的數據進行了對比,以前跳過了該章節,能夠自行查看一下):

NewImage

結果說明:

  • 能夠發現標準IO庫函數User CPU時間都比read版本的最好時間要大,由於逐字符讀寫須要執行100million次循環,逐行讀寫須要執行3百萬次循環,而第一行使用的read的最有版本執行了25224次循環;
  • clock time的差別緣由在於用戶態時間的差別和等待IO完成的時間上的差別;
  • System CPU時間基本和以前版本的相同,由於內核請求數基本相同。所以,在不關心buffer大小和分配,或者只須要關心一行buffer大小的使用下,獲取了幾乎最優的buffer選擇。
  • 最後一列顯示了編譯器編譯後生成的彙編文件的大小。
  • 逐行讀寫比逐字符讀寫快得多,由於fgets和fputs是用memccpy實現,memccpy函數用匯編來實現,效率更高。
  • fgetc版本比read版本的最差時間(BUFFSIZE=1)要快得多,緣由在於read版本會執行200million次函數調用,因爲無緩存機制,因此相應的也會執行200million次系統調用,而fgetc版本也會執行200million次函數調用,可是因爲緩存機制,只須要執行25224次系統調用。咱們知道,系統調用的開銷要比函數調用大得多。

 

 

7 小結

標準IO函數庫分爲兩篇來介紹,本篇是第一篇,主要介紹了

  • 流的基本概念
  • 流的基本操做,包括打開、關閉、讀寫
  • 對比了使用標準IO庫的讀寫效率

 

 

參考資料:

《Advanced Programming in the UNIX Envinronment 3rd》 

相關文章
相關標籤/搜索