UNIX環境高級編程---標準I/O庫

 UNIX環境高級編程---標準I/O庫


前言:我想大家學習C語言接觸過的第一個函數應該是printf,但是我們真正理解它了嗎?最近看Linux以及網絡編程這塊,我覺得I/O這塊很難理解。以前從來沒認識到Unix I/O和C標準庫I/O函數壓根不是一碼事。Unix I/O也叫低級I/O,也叫Unbuffered I/O,是操作系統內核部分,也是系統調用;而C標準I/O函數相對也成Buffered I/O,高級I/O,一般是爲了效率考慮對這些系統調用的封裝。以前使用getchar()經常爲輸入完後的回車而出錯。那是不理解標準I/O實現時的緩衝區的概念。在網上找了這篇文章參考Unix環境高級編程,寫的很詳細。

在前面《UNIX環境高級編程----文件描述符淺析》一文中所講的I/O函數都是針對文件描述符。而對於標準I/O庫,它們的操作都是圍繞流來進行的。當用標準I/O庫打開或創建一個文件時,我們已經使一個流與文件相結合。

一、流和FILE對象

當打開一個流時,標準I/O函數fopen返回一個指向FILE對象的指針。該對象通常是一個結構,它包含了I/O庫爲管理該流所需要的所有信息:用於實際I / O的文件描述符,指向流緩存的指針,緩存的長度,當前在緩存中的字符數,出錯標誌等等

應用程序沒有必要檢驗FILE對象。爲了引用一個流,需將FILE指針作爲參數傳遞給每個標準I/O函數。在《UNIX環境高級編程》一書中,我們稱指向FILE對象的指針(類型爲FILE*)爲文件指針。

 

二、標準I/O庫的緩存(需要理解)

標準I/O提供緩存的目的是儘可能少的使用readwrite調用量,從而加速對文件的讀和寫操作。但是不幸的是標準I/O庫最讓人迷惑的恰好也是它的緩存。爲了詳細說明緩存的機制,必須先了解下爲什麼有了這個緩存就能提供文件的操作效率。

用戶程序調用標準I/O庫函數讀寫文件,而這些庫函數要通過系統調用把讀寫請求傳給內核,最終由內核驅動磁盤或設備完成I/O操作。標準I/O庫爲每個打開的文件分配一個I/O緩衝區以加速讀寫操作,通過文件的FILE結構體可以找到這個緩衝區,用戶調用讀寫函數大多數時候都在I/O緩衝區中讀寫,只有少數時候需要把讀寫請求傳給內核。以fgetc/fputc爲例,當用戶程序第一次調用fgetc讀一個字節時,fgetc函數可能通過系統調用進入內核讀1K字節到I/O緩衝區中,然後返回I/O緩衝區中的第一個字節給用戶,把讀寫位置指向I/O緩衝區中的第二個字符,以後用戶再調fgetc,就直接從I/O緩衝區中讀取,而不需要進內核了,當用戶把這1K字節都讀完之後,再次調用fgetc時,fgetc函數會再次進入內核讀1K字節到I/O緩衝區中。標準I/O庫之所以會從內核預讀一些數據放在I/O緩衝區中,是希望用戶程序隨後要用到這些數據,標準I/O庫的I/O緩衝區也在用戶空間,直接從用戶空間讀取數據比進內核讀數據要快得多。另一方面,用戶程序調用fputc通常只是寫到I/O緩衝區中,這樣fputc函數可以很快地返回,如果I/O緩衝區寫滿了,fputc就通過系統調用把I/O緩衝區中的數據傳給內核,內核最終把數據寫回磁盤。有時候用戶程序希望把I/O緩衝區中的數據立刻傳給內核,讓內核寫回設備,這稱爲Flush操作,對應的庫函數是fflushfclose函數在關閉文件之前也會做Flush操作。

下圖一以fgets/fputs示意了I/O緩衝區的作用,使用fgets/fputs函數時在用戶程序中也需要分配緩衝區(圖中的buf1buf2),注意區分用戶程序的緩衝區和C標準庫的I/O緩衝區。

圖一 I/O緩存區

標準I/O庫提供了三種類型的緩存:

1) 全緩存:如果緩衝區寫滿了就寫回內核。常規文件通常是全緩衝的。

2) 行緩存:如果用戶程序寫的數據中有換行符就把這一行寫回內核,或者如果緩衝區寫滿了就寫回內核。標準輸入標準輸出對應終端設備時通常是行緩衝的

行緩存有兩個限制:

第一個是行緩存區的緩存長度是固定,系統一般默認爲1K,所以只要行緩存區滿了,即使沒有寫一個新換行符,系統也會執行I/O操作;關於這一點,可以從下面的例子看出來。

第二個是任何時候只要通過標準輸入輸出庫要求從( a )一個不帶緩存的流,或者( b )一個行緩存的流(它預先要求從內核得到數據)得到輸入數據,那麼就會造成刷新所有行緩存輸出流。

Example 01.c

#include<stdio.h>

int main()

{

printf("Hello World");

Whlie(1);

return 0;

}

編譯執行時會發現終端什麼都沒有輸出。如果把whlie(1)去掉,就會在終端打印出Hello World

Example 02.c

#include<stdio.h>

int main()

{

printf("Hello World\n");

Whlie(1);

return 0;

}

編譯執行時會發現終端印出Hello World

Example 03.c

#include<stdio.h>

int main()

{

printf("Hello World ...Hello World");//...代表1024-11*2個字節

Whlie(1);

return 0;

}

編譯執行時會發現終端打印出Hello World ...Hello World。以上三個例子足以說明行緩存類型的緩存區長度是固定的。寫入緩存區的數據爲換行符或長度超過緩存區長度時系統會執行I/O操作。

3) 不帶緩存:用戶程序每次調庫函數做寫操作都要通過系統調用寫回內核。標準錯誤輸出通常是無緩衝的,這樣用戶程序產生的錯誤信息可以儘快輸出到設備。

對於任何一個流,如果我們不喜歡這些系統默認,可以通過調用下面兩個函數中一個來更改緩存類型

-----------------------------------------------------------------------------------------------------------------

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

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

返回:若成功則爲0,若出錯則爲非0

-----------------------------------------------------------------------------------------------------------------

下圖二是setbufsetvbuf函數各選項說明,可以明顯看出函數setvbuf功能更強大一些。

圖二 setbufsetvbuf函數各選項說明

 

三、標準I/O庫函數

1. 打開、關閉I/O流函數

下面三個函數可用於打開一個標準流:

----------------------------------------------------------------------------------------------------------------

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

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

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

三個函數的返回:若成功則爲文件指針,若出錯則爲NULL

-------------------------------------------------------------------------------------------------------------

這三個函數的區別是:

(1) fopen打開路徑名由pathname指示的一個文件。

(2) freopen在一個特定的流上(fp指示)打開一個指定的文件(其路徑名由pathname指示),如若該流已經打開,則先關閉該流。此函數一般用於將一個指定的文件打開爲一個預定義的流:標準輸入、標準輸出或標準出錯。

(3) fdopen取一個現存的文件描述符(我們可能從open,dup,dup2,fcntlpipe函數得到此文件描述符),並使一個標準的I/O流與該描述符相結合。

下面函數用於關閉一個標準流:

--------------------------------------------------------------------------------------------------------------

int fclose(FILE *fp)

---------------------------------------------------------------------------------------------------------------

2. 讀、寫I/O流函數

1)以字節爲單位的I/O函數

----------------------------------------------------------------------------------------------------------------

int getc(FILE *stream);

int fgetc(FILE *stream);

int getchar(void);

返回值:成功返回讀到的字節,出錯或者讀到文件末尾時返回EOF

----------------------------------------------------------------------------------------------------------------------

第一個跟第三個本身不是函數,是通過宏定義藉助fgetc來實現的。比如:

# define getc(_stream) fgetc(_stream)

# define getchar fgetc(stdin)

所以fgetc允許作爲一個參數傳遞給另一個函數。

fgetc成功時返回讀到一個字節,本來應該是unsigned char型的,但由於函數原型中返回值是int型,所以這個字節要轉換成int型再返回,那爲什麼要規定返回值是int型呢?因爲出錯或讀到文件末尾時fgetc將返回EOF,即-1,保存在int型的返回值中是0xffffffff,如果讀到字節0xff,由unsigned char型轉換爲int型是0x000000ff,只有規定返回值是int型才能把這兩種情況區分開,如果規定返回值是unsigned char型,那麼當返回值是0xff時無法區分到底是EOF還是字節0xff。如果需要保存fgetc的返回值,一定要保存在int型變量中,如果寫成unsigned char c = fgetc(fp);,那麼根據c的值又無法區分EOF0xff字節了。注意,fgetc讀到文件末尾時返回EOF,只是用這個返回值表示已讀到文件末尾,並不是說每個文件末尾都有一個字節是EOF(根據上面的分析,EOF並不是一個字節)。

---------------------------------------------------------------------------------------------------------------

int putc(int c, FILE *stream);

int fputc(int c, FILE *stream);

int putchar(int c);

返回值:若成功返回c,出錯則爲EOF

---------------------------------------------------------------------------------------------------------------

同樣第一個跟第三個本身不是函數,是通過宏定義藉助fgetc來實現的。

2)以字符串爲單位的I/O函數

----------------------------------------------------------------------------------------------------------------

char *fgets(char *s, int size, FILE *stream);

char *gets(char *s);

返回值:成功時s指向哪返回的指針就指向哪,出錯或者讀到文件末尾時返回NULL

---------------------------------------------------------------------------------------------------------------

這兩個函數都指定了緩存地址,讀入的字符串放入其中。gets是從標準輸入讀,fgets是從指定流讀。

gets不推薦程序員使用,它的存在只是爲了兼容以前的程序,我們寫的代碼不應該有調用這個函數。

現在說說fgets函數,參數s是緩衝區的首地址,size是緩衝區的長度,該函數從stream所指的文件中讀取以'\n'結尾的一行(包括'\n'在內)存到緩衝區s中,並且在該行末尾添加一個'\0'組成完整的字符串。如果文件中的一行太長,fgets從文件中讀了size-1個字符還沒有讀到'\n',就把已經讀到的size-1個字符和一個'\0'字符存入緩衝區,文件中剩下的半行可以在下次調用fgets時繼續讀。如果一次fgets調用在讀入若干個字符後到達文件末尾,則將已讀到的字符串加上'\0'存入緩衝區並返回,如果再次調用fgets則返回NULL,可以據此判斷是否讀到文件末尾。注意,對於fgets來說,'\n'是一個特別的字符,而'\0'並無任何特別之處,如果讀到'\0'就當作普通字符讀入。如果文件中存在'\0'字符(或者說0x00字節),調用fgets之後就無法判斷緩衝區中的'\0'究竟是從文件讀上來的字符還是由fgets自動添加的結束符,所以fgets只適合讀文本文件而不適合讀二進制文件,並且文本文件中的所有字符都應該是可見字符,不能有'\0'對於二進制文件可以通過fread來實現

---------------------------------------------------------------------------------------------

int fputs(const char *s, FILE *stream);

int puts(const char *s);

返回值:成功返回一個非負整數,出錯返回EOF

------------------------------------------------------------------------------------------------

緩衝區s中保存的是以'\0'結尾的字符串,fputs將該字符串寫入文件stream,但並不寫入結尾的'\0'。與fgets不同的是,fputs並不關心的字符串中的'\n'字符,字符串中可以有'\n'也可以沒有'\n'puts將字符串s寫到標準輸出(不包括結尾的'\0'),然後自動寫一個'\n'到標準輸出。

3)二進制I/O函數

上面也提到過用字符串爲單位的IO函數不適合二進制文本。當然對於二進制文件,我們可以通過使用fgetcfputc來實現,但是必須循環整個二進制文件,明顯比較低效。因此標準IO庫提供瞭如下兩個函數對二進制文件操作:

----------------------------------------------------------------------------------------------

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

返回值:讀或寫的記錄數,成功時返回的記錄數等於nmemb,出錯或讀到文件末尾時返回的記錄數小於nmemb,也可能返回0

--------------------------------------------------------------------------------------------------

使用二進制I/O的基本問題是,它只能用於讀已寫在同一系統上的數據。其原因是:

(1) 在一個結構中,同一成員的位移量可能隨編譯程序和系統的不同而異(由於不同的對準要求)。確實,某些編譯程序有一選擇項,它允許緊密包裝結構(節省存儲空間,而運行性能則可能有所下降)或準確對齊,以便在運行時易於存取結構中的各成員。這意味着即使在單一系統上,一個結構的二進制存放方式也可能因編譯程序的選擇項而不同。

(2) 用來存儲多字節整數和浮點值的二進制格式在不同的系統結構間也可能不同。

3)二進制I/O函數

上面也提到過用字符串爲單位的IO函數不適合二進制文本。當然對於二進制文件,我們可以通過使用fgetcfputc來實現,但是必須循環整個二進制文件,明顯比較低效。因此標準IO庫提供瞭如下兩個函數對二進制文件操作:

----------------------------------------------------------------------------------------------

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

返回值:讀或寫的記錄數,成功時返回的記錄數等於nmemb,出錯或讀到文件末尾時返回的記錄數小於nmemb,也可能返回0

-------------------------------------------------------------------------------------------------

使用二進制I/O的基本問題是,它只能用於讀已寫在同一系統上的數據。其原因是:

(1) 在一個結構中,同一成員的位移量可能隨編譯程序和系統的不同而異(由於不同的對準要求)。確實,某些編譯程序有一選擇項,它允許緊密包裝結構(節省存儲空間,而運行性能則可能有所下降)或準確對齊,以便在運行時易於存取結構中的各成員。這意味着即使在單一系統上,一個結構的二進制存放方式也可能因編譯程序的選擇項而不同。

(2) 用來存儲多字節整數和浮點值的二進制格式在不同的系統結構間也可能不同。

3. 定位I/O流函數

兩種方法定位標準I/O流:

(1) ftellfseek。這兩個函數自V7以來就存在了,但是它們都假定文件的位置可以存放在一個長整型中。

(2) fgetposfsetpos。這兩個函數是新由ANSI C引入的。它們引進了一個新的抽象數據類型fpost,它記錄文件的位置。在非UNIX系統中,這種數據類型可以定義爲記錄一個文件的位置所需的長度。所以移植到非UNIX系統的應用程序應當使用fgetposfsetpos

----------------------------------------------------------------------------------------------------

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

返回值:成功返回0,出錯返回-1並設置errno

long ftell(FILE *stream);

返回值:成功返回當前讀寫位置,出錯返回-1並設置errno

void rewind(FILE *stream);

把讀寫位置移到文件開頭

-------------------------------------------------------------------------------------------------------

fseekwhenceoffset參數共同決定了讀寫位置移動到何處,whence參數的含義如下:

SEEK_SET 

從文件開頭移動offset個字節

SEEK_CUR 

從當前位置移動offset個字節

SEEK_END 

從文件末尾移動offset個字節

offset可正可負,負值表示向前(向文件開頭的方向)移動,正值表示向後(向文件末尾的方向)移動,如果向前移動的字節數超過了文件開頭則出錯返回,如果向後移動的字節數超過了文件末尾,再次寫入時將增大文件尺寸,從原來的文件末尾到fseek移動之後的讀寫位置之間的字節都是0

-------------------------------------------------------------------------------------------------------

int fgetpos(FILEf *p, fpos_t *pos) ;

int fsetpos(FILEf *p, const fpos_t *pos) ;

兩個函數返回:若成功則爲0,若出錯則爲非0

-----------------------------------------------------------------------------------------------------------

fgetpos將文件位置指示器的當前值存入由pos指向的對象中。在以後調用fsetpos時,可以使用此值將流重新定位至該位置。

 

4. 格式化I/O流函數

格式化輸入函數:

----------------------------------------------------------------------------------------------------

int printf(const char *format, ...);

int fprintf(FILE *stream, const char *format, ...);

int sprintf(char *str, const char *format, ...);

int snprintf(char *str, size_t size, const char *format, ...);

int vprintf(const char *format, va_list ap);

int vfprintf(FILE *stream, const char *format, va_list ap);

int vsprintf(char *str, const char *format, va_list ap);

int vsnprintf(char *str, size_t size, const char *format, va_list ap);

返回值:成功返回格式化輸出的字節數(不包括字符串的結尾'\0'),出錯返回一個負值

--------------------------------------------------------------------------------------------------------

格式化輸出函數:

---------------------------------------------------------------------------------------------------------

int scanf(const char *format, ...);

int fscanf(FILE *stream, const char *format, ...);

int sscanf(const char *str, const char *format, ...);

#include <stdarg.h>

int vscanf(const char *format, va_list ap);

int vsscanf(const char *str, const char *format, va_list ap);

int vfscanf(FILE *stream, const char *format, va_list ap);

返回值:返回成功匹配和賦值的參數個數,成功匹配的參數可能少於所提供的賦值參數,返回0表示一個都不匹配,出錯或者讀到文件或字符串末尾時返回EOF並設置errno

---------------------------------------------------------------------------------------------------------

這裏僅僅說一下printf的一個小技巧,我們在%後面加上#,打印到終端的值,會在前面自動加上00x。比如pintf("%#x",1)語句在終端會打印0x1

 

5. 創建臨時文件I/O流函數

很多情況下,程序會創建一些文件形式的臨時文件,這些臨時文件可能保存這一個計算的中間結果,也可能是關鍵操作前的備份等等。這都是臨時文件的好處。

標準I/O提供了兩個函數創建臨時文件

---------------------------------------------------------------------------------------------------------

char *tmpnam(char *ptr) ;

返回:指向一唯一路徑名的指針

FILE *tmpfile(void);

返回:若成功則爲文件指針,若出錯則爲NULL

-----------------------------------------------------------------------------------------------------------

tmpnam函數返回一個不與任何已存在文件同名的有效文件名。每次調用它都會產生一個不同的文件名,但是一個進程中調用最多次數爲TMP_MAX【在stdio.h中有定義】。如果ptr不爲NULL,則認爲字符串ptr的長度至少是L_tmpnam【在stdio.h中有定義】,所產生的文件名會放入該字符串ptr中,因此返回值爲ptr的值;如果ptrNULL,則所產生的文件名存放在一個靜態區中,下一次調用時,會重寫改靜態區。

tmpfile 創建一個臨時二進制文件(類型爲wb+),關閉文件或程序結束時將自動刪除這種文件。

需要注意的是,tmpnam僅僅是創建一個臨時文件,並沒有打開它,所以我們如果要用它必須儘可能快的打開它,這樣減小另一個程序用同樣的名字打開文件的風險。而tmpfile除了創建外,會同時以讀寫方式打開。

 

四、參考資料

1. UNIX環境高級編程》

2. Linux程序設計(第三版)》

3. 標準I/O庫函數


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章