天天看點

帶緩沖I/O與不帶緩沖I/O:檔案I/O和标準I/O庫帶緩沖和不帶緩沖I/O的差別:檔案I/O标準I/O庫

介紹标準I/O前,先介紹檔案I/O,以便與差別比較。

帶緩沖和不帶緩沖I/O的差別:

所謂不帶緩沖,并不是指核心不提供緩沖,而是隻單純的系統調用,不是函數庫的調用。系統核心對磁盤的讀寫都會提供一個塊緩沖(核心緩沖區)(在有些地方也被稱為核心高速緩存),當用write函數對其寫資料時,直接調用系統調用,将資料寫入到塊緩沖進行排隊,當塊緩沖達到一定的量時,才會把資料寫入磁盤。是以所謂的不帶緩沖的I/O是指程序不提供緩沖功能(但核心還是提供緩沖的)。每調用一次write或read函數,直接系統調用。

帶緩沖的I/O是指程序對輸入輸出流進行了改進,提供了一個流緩沖,當用fwrite函數往磁盤寫資料時,先把資料寫入流緩沖區中,當達到一定條件,比如流緩沖區滿了,或重新整理流緩沖,這時候才會把資料一次送往核心提供的塊緩沖,再經塊緩沖寫入磁盤。(雙重緩沖)

是以,帶緩沖的I/O在往磁盤寫入相同的資料量時,會比不帶緩沖的I/O調用系統調用的次數要少。

帶緩存IO其實就是在使用者層再建立一個緩存區,這個緩存區的配置設定和優化長度等細節都是标準IO庫代你處理好了

總結:

無緩存IO操作資料流向路徑:資料——核心緩存區——磁盤

标準IO操作資料流向路徑:資料——流緩存區——核心緩存區——磁盤

不帶緩存的I/O對檔案描述符操作,下面帶緩存的I/O是針對流的

關于帶緩沖與不帶緩沖詳解部落格:

帶緩沖I/O與不帶緩沖I/O的差別與聯系

淺談标準I/O緩沖區 

檔案I/O

UNIX系統中的大多數檔案I/O隻需用到5個函數:open,read,write,lseek以及close。

這些I/O函數經常被稱為不帶緩沖的I/O,不帶緩沖是指每個read和write函數都調用核心中的一個系統調用。

檔案描述符:

所有打開的檔案都通過檔案描述符引用,檔案描述符是一個非負整數。打開或建立一個檔案時,核心向程序傳回一個檔案描述符。讀寫檔案時通過檔案描述符辨別檔案,将其作為參數傳遞給read或write。

檔案描述符的變化範圍是0~OPEN_MAX-1

标準輸入:STDIN_FILENO

标準輸出:STDOUT_FILENO

标準錯誤:STDERR_FILENO

函數open和openat

調用open或openat函數可以打開或建立一個檔案

int open(const char *path, int oflag,..)
int openat(ind fd, const char *path, int oflag,...)     //自行搜尋掌握
           

path參數是要打開或建立檔案的名字,oflag參數是此函數的多個選項,通常是一個或多個常量。

列出一些常用的oflag參數:

O_RDONLY:隻讀打開

O_WRONLY:隻寫打開

O_RDWR:讀寫打開

O_EXEC:隻執行打開

O_CREAT:若此檔案不存在則建立它

函數creat

調用creat函數建立一個新檔案

int creat(const char *path,mode_t mode);    //傳回隻寫打開的檔案描述符
           

此函數等效于:

open(path,O_WRONLY | O_CREAT | O_TRUNC,mode);
           

函數close

調用close函數關閉一個打開檔案

int close(int fd);
           

當一個程序終止時,核心自動關閉它所有的打開檔案。

函數lseek

每個打開檔案都有一個與其關聯的"目前檔案偏移量"--offset。通常是個非負整數,用以度量從檔案開始處計算的位元組數。讀寫操作都從目前檔案偏移量處開始,并使偏移量增加所讀寫的位元組數。

off_t lseek(int fd, off_t offset, int whence);      //傳回新的檔案偏移量
           

參數offset和參數whence:

-當whence是SEEK_SET,則将該檔案的偏移量設定為距檔案開始處offset個位元組。

-當whence是SEEK_CUR,則将該檔案的偏移量設定為其目前值加offset,offset可為正為負。

-當whence是SEEK_END,則将該檔案的偏移量設定為檔案長度加offset,offset可為正為負。

函數read

調用read函數從打開檔案中讀資料

ssize_t read(int fd, void *buf, size_t nbytes);     //傳回讀到的位元組數。若到檔案尾,傳回0
           

從fd指向的檔案中讀取nbytes到buf中。

-讀普通檔案時,若讀到要求位元組數之前到了檔案尾端,則read傳回讀到的位元組數,下次再調用時,傳回0。

-當從終端裝置讀時,通常一次最多讀一行。

-當從網絡中讀時,網絡中的緩沖機制可能造成傳回值小于所要求讀的位元組數。

函數write

調用write函數向打開檔案寫資料

ssize_t write(int fd, const void *buf, size_t nbytes);  //傳回已寫的位元組數
           

将buf中的資料寫入fd指向的檔案。

I/O的效率

/* 隻使用read和write函數複制一個檔案 */
#include<stdio.h>
#include<apue.h>
#include<myerr.h>
#include<fcntl.h>
#define BUFSIZE 4096

int main(void)
{
	int n;
	char buf[BUFSIZE];
	
	while((n = read(STDOUT_FILENO,buf,BUFSIZE)) > 0)
		if(write(STDOUT_FILENO,buf,n) != n)
			err_sys("write error");

	if(n<0)
		err_sys("read error");

	exit(0);
}
           

如何選取BUFSIZE的值呢?通常擁有較大的緩沖區長度耗費時間越少。

标準I/O庫

流和FILE對象

上面檔案I/O函數都是圍繞檔案描述符的,當打開一個檔案時,即傳回一個檔案描述符。

而對于标準I/O庫,操作是圍繞流(stream)進行的,用标準I/O庫打開或建立一個檔案時,一個流就與一個檔案相關聯

标準I/O庫就是帶緩存的I/O,它由ANSI C标準說明。當然,标準I/O最終都會調用上面的I/O例程。标準I/O庫代替使用者處理很多細節,比如緩存配置設定、以優化長度執行I/O等。

标準I/O提供緩存的目的就是減少調用read和write的次數,它對每個I/O流自動進行緩存管理(标準I/O函數通常調用malloc來配置設定緩存)。

流的定向決定了所讀寫的字元是單位元組還是多位元組的。一個流被建立時,并沒有定向。若在未定向的流上使用一個多位元組I/O函數,則流的定向設定為寬定向的;若在未定向的流上使用單位元組I/O函數,則流的定向設為位元組定向的。

兩個函數改變流的定向:freopen函數清除一個流的定向;fwide函數用于設定流的定向

#include<wchar.h>
int fwide(FILE* fp, int mode);  //若流是寬定向,傳回正值;流是位元組定向,傳回負值;流是未定向的,傳回0
           

fwide函數不改變已定向流的定向,若指定流已定向,則傳回相應值。

根據mode參數設定:

-若mode參數值為負,fwide将使指定的流是位元組定向的

-若mode參數值為正,fwide将使指定的流是寬定向的

-若mode參數值為0,fwide不設定流定向

标準I/O流預定義檔案指針:

标準輸入:stdin

标準輸出:stdout

标準錯誤:stderr

緩沖

标準I/O也被稱為帶緩沖的I/O,提供緩沖的目的是盡可能減少使用read和write調用的次數。

一般而言,由系統選擇緩存的長度,并自動配置設定。标準I/O庫在關閉流的時候自動釋放緩存。

标準I/O提供了以下3種類型的緩沖:

--全緩沖:在填滿标準I/O緩沖區後才進行實際I/O操作。在一個流上執行第一次I/O操作時,标準I/O函數通常調用malloc獲得需使用的緩沖區。

--行緩沖:當在輸入和輸出中遇到換行符時,标準I/O庫執行I/O操作。當流涉及一個終端時(标準輸入和标準輸出),通常使用行緩沖。因為I/O庫用來收集每一行緩沖區的長度是固定的,是以隻要填滿了緩沖區,即時還沒有換行符,也進行I/O操作。

--不帶緩沖。标準I/O庫不對字元進行緩沖存儲,例如标準I/O函數fputs寫15個字元到不帶緩沖的流的,期望15個字元能立即輸出,就很可能使用write函數将這些字元寫到相關聯的打開檔案中。标準錯誤流stderr通常是不帶緩沖的,這使得出錯消息可以盡快顯示出來。

系統預設使用下列類型的緩沖:

--标準錯誤不帶緩沖

--若是指向終端裝置的流,則是行緩沖的;否則是全緩沖的。

打開流

下列3個函數打開一個标準I/O流:

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);
//若成功,傳回檔案指針
           

3個函數差別如下:

--fopen函數打開路徑名為pathname的一個指定的檔案

--freopen函數在一個指定的流上打開一個指定的檔案,若該流已經打開,則先關閉該流。若該流已經定向,則使用freopen清除該定向

--fdopen函數擷取一個已有的檔案描述符,并使一個标準的I/O流與該描述符相結合。此函數常用于由建立管道和網絡通信通道函數傳回的描述符。

type參數指定對該I/O流的讀寫方式:

帶緩沖I/O與不帶緩沖I/O:檔案I/O和标準I/O庫帶緩沖和不帶緩沖I/O的差別:檔案I/O标準I/O庫

關閉流

調用fclose函數關閉一個打開的流

int fclose(FILE *fp);
           

在該檔案被關閉前,沖洗緩沖中的輸出資料。

讀和寫流

一旦打開了流,則可在3種不同類型的非格式化I/O種進行選擇,對其進行讀寫操作:

--每次一個字元的I/O,一次讀或寫一個字元,若流是帶緩沖的,則标準I/O函數處理所有緩沖

--每次一行的I/O,fgets和fputs。

--直接I/O,fread和fwrite函數支援這種類型的I/O,每次I/O操作讀或寫某種數量的對象,每個對象具有指定的長度。這兩個函數通常用于從二進制檔案中每次讀或寫一個結構。

每次一個字元I/O

輸入函數

一次讀一個字元:

int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
           

getchat等同于getc(stdin);

getc和fgetc的差別是getc可被實作為宏,而fgetc不能。

輸出函數

對應于上述輸入函數:

int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
           

每次一行I/O

輸入函數

每次輸入一行:

char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);          
//若成功,傳回buf
           

讀入的行将送入緩沖區,fgets從指定的流讀,gets從标準輸入讀。

gets不推薦用,因為不能指定緩沖區的長度,可能造成緩沖區溢出(即讀入的行大于緩沖區長度)

輸出函數

每次輸出一行:

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

fputs将一個以null字元終止的字元串寫到指定的流。注意:這并不一定是每次輸出一行。

puts.................................................寫到标準輸出。

标準I/O的效率

帶緩沖I/O與不帶緩沖I/O:檔案I/O和标準I/O庫帶緩沖和不帶緩沖I/O的差別:檔案I/O标準I/O庫

使用标準I/O的一個優點是無需考慮緩沖及最佳I/O長度的選擇,在使用fgets時需要考慮最大行長,但是與選擇最佳I/O長度來說,這要友善得多。

使用每次一行I/O版本的速度大約是每次一個字元版本速度的兩倍。

二進制I/O

前面介紹了以一次一個字元或一次一行的方式進行操作。如果進行二進制I/O操作,那麼我們更願意一次讀或寫一個完整的結構。

size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
    //傳回讀或寫的對象數
           

兩種常見的用法:

1.讀或寫一個二進制數組。例如:為了将一個浮點數組的第2~5個元素寫到一個檔案上

float data[10];

if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
    err_sys("fwrite error");
           

其中,指定size為每個數組元素的長度,nobj為欲寫的元素個數

2.讀或寫一個結構。例如:

struct {
    short count;
    long total;
    char name[NAMESIZE];
}item;

if (fwrite(&item,sizeof(item),1,fp) != 1)
    err_sys("fwrite error");
           

其中,指定size為結構的長度,nobj為1 (要寫的對象個數)

格式化I/O

格式化輸出

格式化輸出是由5個printf函數來處理的:

int printf(const char *restrict format...);
int fprintf(FILE *restrict fp, const *restrict format,...);
int dprintf(int fd, const char *restrict format,...);       //3個傳回值:若成功,傳回輸出字元數
int sprintf(char *restrict buf, const char *restrict format,...);   //若成功,傳回存入數組的字元數
int snprintf(char *restrict buf, size_t n, const char *restrict format,...);
           

printf将格式化資料寫到标準輸出,fprintf寫至指定的流,dprintf寫至指定的檔案描述符,sprintf将格式化的字元送入數組buf中。

格式化輸入

格式化輸入處理的是3個scanf函數 

int scanf(const char *restrict format...);
int fscanf(FILE *restrict fp, const char *restrict format...);
int sscanf(const char *restrict buf, const char *restrict format...);
           

每個标準I/O流都有一個與其相關聯的檔案描述符,可以對一個流調用fileno函數以獲得其描述符。

int fileno(FILE *fp);       //傳回與該流相關聯的檔案描述符