Linux文件IO(二)標準IO

塊做爲文件系統的抽象,它是I/O 中最基本的概念——所有的磁盤操作都是基於塊進行的。因此,當請求以塊大小整數倍對齊地址時, I/O 效率是最理想的。操作效率隨着系統調用次數的增多而急劇下降,例如,每次讀一字節讀1024次與一次讀1024字節相比,顯然後者效率更優。如果長度不是block的整數倍,即使每次以大於塊的長度進行一系列的操作,其效率也不是最理想的。例如塊的大小是1K ,每次以1130字節的長度操作數據要比每次1024字節的速度慢。

用戶態緩衝I/O
需要對普通文件執行許多輕量級I/O請求的程序通常使用用戶緩衝I/O。用戶緩衝I/O是在用戶空間而不是在內核中完成的,它可以在程序中設定,也可以調用標準庫透明地執行。實際應用中,塊大小一般是512字節,1024字節,2048字節,或4096字節。效率的大規模提升只是通過將每次操作的數據設置爲塊大小整數倍或約數獲得的。通過用系統調用stat()可以輕鬆指定設備的塊大小。
但問題是程序很少以塊爲單位進行操作。程序往往以區域,行,和單個字符爲單位進行操作,而不是抽象的塊。如前所述,爲了改善這種情況,程序使用用戶緩衝I/O。當數據被寫入時,它會被存儲在程序地址空間的緩衝區中。當緩衝區規模達到一個給定的值(緩衝區大小時),整個緩衝區會在一次操作中被寫出。
C標準庫中提供了標準I/O庫(通常簡單稱作stdio),其中實現了一個跨平臺用戶緩衝的解決方案。標準I/O例程並不直接操作文件描述符。取而代之的是它們用自己唯一的標誌符,即大家熟知的文件指針(filepointer)。在C標準庫裏,文件指針映射到一個文件描述符。文件指針由FILE類型的指針表示,FILE類型定義在<stdio.h>中。在標準I/O中,一個打開的文件叫做”流”(stream)。流可以被打開用來讀(輸入流),寫(輸出流),或者二者兼有(輸入輸出流)。

打開文件
FILE fopen(const char path, constchar mode);
FILE
fdopen(int fd, const char* mode);

關閉流
int fclose(FILE* stream);
int fcloseall(void);

從流中讀取數據
int fgetc(FILE stream);
int ungetc(int c, FILE
stream);
char fgets(char str, int size, FILE stream);
size_t fread(void
buf, size_t size, size_t nr, FILE* stream);

向流中寫數據
數據對齊所有的機器設計都有數據對齊的要求。程序員傾向於把內存想成一個簡單的字節數組。但是處理器並不以字符大小對內存進行讀寫。相反,處理器以特定的粒度(例如2,4,8或16字節)來訪問內存。因爲每個處理的地址空間都從0地址開始,進程必須從一個特定粒度的整數倍開始讀取。因此,C變量的存儲和訪問都要是地址對齊的。用另一種說法就是一個int需要被存儲在能被4整除的內存地址中。訪問不對齊的數據在不同的體系結構上有不同程度的性能損失。一些處理器能夠訪問不對齊的數據,但是會有一個很大性能損失。有些的處理器根本不能夠訪問非對齊的數據,而且企圖這麼做會導致硬件異常。更糟的是,一些處理器爲了強制地址對齊會丟棄了低位的數據,從而導致不可預料的行爲。
int fputc(int c,FILE stream);
int fputs(const char
str,FILE stream);
size_t fwrite(void
buf, size_t size, size_t nr, FILE* stream);

定位流
int fseek(FILE stream, long offset, int whence);
int fsetpos(FILE
stream, fpos_t pos);
int fgetpos(FILE
stream, fpos_t pos);
long ftell(FILE
stream);

清洗一個流
效率提高的原因——程序保留在用戶空間中,並且運行用戶的代碼,不執行系統調用。只有當磁盤或其它介質必須被訪問時系統調用纔會被執行。fflush()只是把用戶緩衝的數據寫入到內核緩衝區。效果和沒有用戶緩衝區一樣,而且write()是被直接調用的。但這並不保證數據能夠寫入物理介質——如果需要的話,使用fsync()這一類函數。
int fflush(FILE* stream); // 將用戶緩衝區寫入內核

錯誤和文件結束
一些標準I/O接口,例如fread(),向調用者傳遞失敗信息的能力很差,因爲它們沒有提供區分錯誤和EOF的機制。在一些場合中調用這些函數時,需要區分給定的流出現了錯誤還是到達了文件結尾。標準I/O爲此提供了兩個函數。函數ferror()測試是否在流上設置了錯誤標誌:
int ferror(FILE stream);
int feof(FILE
stream);
void clearerr(FILE* stream);

獲得關聯的文件描述符
int fileno(FILE* stream);

控制緩衝
標準I/O實現了三種用戶緩衝,而且爲開發者提供了一個用來控制緩衝區大小和類型的接口。不同的用戶緩衝提供不同功能,並適用於不同的場合。下面是一些選項:
不緩衝 沒有執行用戶緩衝。數據直接提交到內核。因爲這和用戶緩衝對立,這個選項通常不用。標準錯誤默認是不緩衝的。
行緩衝 緩衝以行爲單位執行。每當遇到換行符,緩衝區被提交到內核。行緩衝對輸出到屏幕的流有用。因此,它是終端的默認緩衝方式(標準輸出默認爲行緩衝)。
塊緩衝 緩衝以塊爲單位執行。這是本章一開始討論的緩衝類型,而且它適用於文件。默認的所有和文件相關的流都是塊緩衝的。標準I/O稱塊緩衝爲全緩衝。
setbuf()函數設置流的緩衝類型模式:
int setvbuf(FILE stream, char buf,int mode,size_t size);

線程安全
線程就是在同一個進程中執行的多個實例。線程的定義是共享同一地址空間的多個進程。如果不採取數據同步措施或將數據線程私有化,線程可以任何時間修改共享數據。支持線程的操作系統提供加鎖機制(保證相互排斥的程序結構)來保證線程不會互相干擾。標準I/O使用這些機制。而且,這些機制通常還不能滿足需求。例如,有時候你想給一組調用加鎖,將臨界區(一段獨立運行的代碼)的範圍從一個I/O操作擴大到幾個。而有些情況下,你可能想取消鎖機制來提高效率。標準I/O的函數本質上是線程安全的。
POSIX費線程安全函數:
http://kimi.it/506.html

標準I/O爲獨立操縱和流關聯的鎖提供了一系列的函數。通常,取消鎖會導致各種各樣的問題。但一些程序可能顯式地將所有的I/O操作都由一個線程來完成。在這種情況下,沒有必要增加鎖的開銷。
void flockfile(FILE stream);
void funlockfile(FILE
stream);
int ftrylockfile(FILE stream);
Linux提供了一系列的函數,類似於通常的標準I/O接口,但是不執行任何鎖操作。他們實際上是不加鎖的標準I/O:除了它們不檢查或獲得指定流上的鎖,這些函數與相對的加鎖函數執行相同操作。
fgetc_unlocked、
fgets_unlocked、fread_unlocked、fputc_unlocked、fputs_unlocked、fwrite_unlocked、fflush_unlocked、feof_unlocked、ferror_unlocked、fileno_unlocked、clearerr_unlocked

對標準I/O的批評
對標準I/O最大的抱怨是用戶空間雙副本的性能影響。當讀取數據時,標準I/O對內核執行read()系統調用,從內核中複製數據到標準I/O緩衝區。然後當一個程序通過標準I/O執行一個讀請求時一例如,用fgetc()—數據又被複制,這一次從標準I/O的緩衝區到指定的緩衝區。寫入請求時用相反的方式運行。
一個解決的辦法是,通過讓每個讀請求返回一個指向標準I/O緩衝區的指針來避免雙複本。數據可以被直接從標準I/O緩衝區中讀取,不需要多餘的複製。當程序確實需要自己本地緩衝區的數據時一可能向其中寫數據一總可以手動地執行復制。這個實現需要提供了一個”釋放”的接口,允許程序當它們完成緩衝區的一塊讀取時發出信號。寫操作會更復雜些,但是仍然能夠被避免雙複本。當執行一個寫入請求時,這個實現會記錄指針位置,最終當準備好將數據沖洗到內核時再寫出數據。這些可以通過分散-聚集I/O(scatter-gatherI/O)中的writev()函數來實現,完成這個功能只需要一個系統調用。
現在有一些高度優化的用戶緩衝庫,它們用了類似於我們剛剛討論的方法來解決雙複本問題。一些開發者會選擇實現他們自己的用戶緩衝方案。但是儘管有不同的選擇,標準I/O仍然很流行。

3.13 結論
標準I/O是C標準庫提供的一個用戶緩衝庫。除掉一些不完善的地方,它是一個很強大而且非常流行的解決方案。標準I/O(僅從這點來看,也包括用戶緩衝)適用於下列情況:
1,你可能需要執行許多系統調用,而你希望通過合併這些調用從而儘量減少開銷。
2,在性能至關重要的場合,你希望保證所有的I/O以塊大小進行,並且能保持塊對齊。
3,你的訪問模式是基於字符或行的,你希望通過一個接口來實現這種訪問,但又不想進行太多的系統調用。
4,相比底層的Linux系統調用,你更喜歡高層次的接口。

標準I/O並不完善。標準I/O庫的一個不足之處是效率不高,這與它需要複製的數據量有關。當使用每次一行函數fgets和fputs時,通常需要複製兩次數據:一次是在內核和標準I/O緩衝之間(當調用read和write時),第二次是在標準I/O緩衝區和用戶程序中的行緩衝區之間。
替代版本fio:快速I/O庫則避免了這一點,其方法是使讀一行的函數返回指向該行的指針,而不是將該行復制到另一個緩衝區中。由於執行了這種更改,grep(1)實用程序的速度增加了兩倍。
替代版本sfio:這一軟件包在速度上與fio相近,通常快於標準I/O庫。sfio也提供了一些其他標準I/O庫所沒有的新特徵:推廣了I/O流,使其不僅可以代表文件,也可代表存儲區;可以編寫處理模塊,並以棧方式將其壓入I/O流,這樣就可以改變一個流的操作;較好的異常處理等。
替代軟件包ASI:它使用了映射文件——mmap函數。ASI(Alloc Stream Interface),其編程接口類似與UNIX存儲分配函數(malloc、realloc和free)。與sfio軟件包相同,ASI使用指針力圖減少數據複製量。許多標準I/O庫實現可用於C函數庫中,這種C函數庫是爲內存較小的系統(例如嵌入式系統)設計的。這些實現對於合理內存要求的關注超過對可移植性、速度以及功能性等方面的關注。
這類函數庫的兩種實現是:uClibcC庫(見http://www.uclibc.org)和newlibcC庫(http://www.source.redhat.com/newlib)。

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