轉自:
http://blog.csdn.net/cowbane/article/details/6630298
首先,先稍微瞭解系統調用的概念:
系統調用,英文名system call,每個操作系統都在內核裏有一些內建的函數庫,這些函數可以用來完成一些系統系統調用把應用程序的請求傳給內核,調用相應的的內核函數完成所需的處理,將處理結果返回給應用程序,如果沒有系統調用和內核函數,用戶將不能編寫大型應用程序,及別的功能,這些函數集合起來就叫做程序接口或應用編程接口(Application Programming Interface,API),我們要在這個系統上編寫各種應用程序,就是通過這個API接口來調用系統內核裏面的函數。如果沒有系統調用,那麼應用程序就失去內核的支持。
現在,再聊不帶緩存的I/O操作:
linix對IO文件的操作分爲不帶緩存的IO操作和標準IO操作(即帶緩存),剛開始,要明確以下幾點:
1:不帶緩存,不是直接對磁盤文件進行讀取操作,像read()和write()函數,它們都屬於系統調用,只不過在用戶層沒有緩存,所以叫做無緩存IO,但對於內核來說,還是進行了緩存,只是用戶層看不到罷了。如果這一點看不懂,請看第二點;
2:帶不帶緩存是相對來說的,如果你要寫入數據到文件上時(就是寫入磁盤上),內核先將數據寫入到內核中所設的緩衝儲存器,假如這個緩衝儲存器的長度是100個字節,你調用系統函:
ssize_t write (int fd,const void * buf,size_t count);
寫操作時,設每次寫入長度count=10個字節,那麼你幾要調用10次這個函數才能把這個緩衝區寫滿,此時數據還是在緩衝區,並沒有寫入到磁盤,緩衝區滿時才進行實際上的IO操作,把數據寫入到磁盤上,所以上面說的“不帶緩存""不是沒有緩存而是沒有直寫進磁盤就是這個意思
那麼,既然不帶緩存的操作實際在內核是有緩存器的,那帶緩存的IO操作又是怎麼回事呢?
帶緩存IO也叫標準IO,符合ANSI C 的標準IO處理,不依賴系統內核,所以移植性強,我們使用標準IO操作很多時候是爲了減少對read()和write()的系統調用次數,帶緩存IO其實就是在用戶層再建立一個緩存區,這個緩存區的分配和優化長度等細節都是標準IO庫代你處理好了,不用去操心,還是用上面那個例子說明這個操作過程:
上面說要寫數據到文件上,內核緩存(注意這個不是用戶層緩存區)區長度是100字節,我們調用不帶緩存的IO函數write()就要調用10次,這樣系統效率低,現在我們在用戶層建立另一個緩存區(用戶層緩存區或者叫流緩存),假設流緩存的長度是50字節,我們用標準C庫函數的fwrite()將數據寫入到這個流緩存區裏面,流緩存區滿50字節後在進入內核緩存區,此時再調用系統函數write()將數據寫入到文件(實質是磁盤)上,看到這裏,你用該明白一點,標準IO操作fwrite()最後還是要掉用無緩存IO操作write,這裏進行了兩次調用fwrite()寫100字節也就是進行兩次系統調用write()。
如果看到這裏還沒有一點眉目的話,那就比較麻煩了,希望下面兩條總結能夠幫上忙:
無緩存IO操作數據流向路徑:數據——內核緩存區——磁盤
標準IO操作數據流向路徑:數據——流緩存區——內核緩存區——磁盤
下面是一個網友的見解,以供參考:
不帶緩存的I/O對文件描述符操作,下面帶緩存的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來分配緩存)。它提供了三種類型的緩存:
1) 全緩存。當填滿標準I/O緩存後才執行I/O操作。磁盤上的文件通常是全緩存的。
2) 行緩存。當輸入輸出遇到新行符或緩存滿時,才由標準I/O庫執行實際I/O操作。stdin、stdout通常是行緩存的。
3) 無緩存。相當於read、write了。stderr通常是無緩存的,因爲它必須儘快輸出。
一般而言,由系統選擇緩存的長度,並自動分配。標準I/O庫在關閉流的時候自動釋放緩存。
在標準I / O庫中,一個效率不高的不足之處是需要複製的數據量。當使用每次一行函數fgets和fputs時,通常需要複製兩次數據:一次是在內核和標準I / O緩存之間(當調用read和write時),第二次是在標準I / O緩存(通常系統分配和管理)和用戶程序中的行緩存(fgets的參數就需要一個用戶行緩存指針)之間。
不管上面講的到底懂沒懂,記住一點:
使用標準I / O例程的一個優點是無需考慮緩存及最佳I / O長度的選擇,並且它並不比直接調用read、write慢多少。
帶緩存的文件操作是標準C 庫的實現,第一次調用帶緩存的文件操作函數時標準庫會自動分配內存並且讀出一段固定大小的內容存儲在緩存中。所以以後每次的讀寫操作並不是針對硬盤上的文件直接進行的,而是針對內存中的緩存的。何時從硬盤中讀取文件或者向硬盤中寫入文件有標準庫的機制控制。不帶緩存的文件操作通常都是系統提供的系統調用,更加低級,直接從硬盤中讀取和寫入文件,由於 IO瓶頸的原因,速度並不如意,而且原子操作需要程序員自己保證,但使用得當的話效率並不差。另外標準庫中的帶緩存文件IO 是調用系統提供的不帶緩存IO實現的。
這裏爲了說明標準I/O的工作原理,借用了glibc中標準I/O實現的細節,所以代碼多是不可移植的.
1. buffered I/O, 即標準I/O
首先,要明確,unbuffered I/O只是相對於buffered I/O,即標準I/O來說的。而不是說unbuffered I/O讀寫磁盤時不用緩衝。實際上,內核是存在高速緩衝區來進行。真正的磁盤讀寫的,不過這裏要討論的buffer跟內核中的緩衝區無關.
buffered I/O的目的是什麼呢?很簡單,buffered I/O的目的就是爲了提高效率.請明確一個關係,那就是:
buffered I/O庫函數(fread, fwrite等,用戶空間) <----call---> unbuffered I/O系統調用(read,write等,內核空間)<-------> 讀寫磁盤
buffered I/O庫函數都是調用相關的unbuffered I/O系統調用來實現的,他們並不直接讀寫磁盤.那麼,效率的提高從何而來呢?
注意到,buffered I/O中都是庫函數,而unbuffered I/O中爲系統調用,使用庫函數的效率是高於使用系統調用的.buffered I/O就是通過儘可能的少使用系統調用來提高效率的.它的基本方法是,在用戶進程空間維護一塊緩衝區,第一次讀(庫函數)的時候用read(系統調用)多從內核讀出一些數據,下次在要讀(庫函數)數據的時候,先從該緩衝區讀,而不用進行再次read(系統調用)了.同樣,寫的時候,先將數據寫入(庫函數)一個緩衝區,多次以後,在集中進行一次write(系統調用),寫入內核空間.
buffered I/O中的fgets, puts, fread, fwrite等和unbufferedI/O中的read,write等就是調用和被調用的關係
下面是一個利用buffered I/O讀取數據的例子:
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- int main(void)
- {
- char buf[5];
- FILE *myfile = stdin;
- fgets(buf, 5, myfile);
- fputs(buf, myfile);
- return 0;
- }
要弄清楚這些問題,就要看看FILE是如何定義和運作的了.(特別說明,在平時寫程序時,不用也不要關心FILE是如何定義和運作的,最好不要直接操作它,這裏使用它,只是爲了說明buffered IO)
下面的這個是glibc給出的FILE的定義,它是實現相關的,別的平臺定義方式不同.
- struct _IO_FILE {
- int _flags;
- #define _IO_file_flags _flags
- char* _IO_read_ptr;
- char* _IO_read_end;
- char* _IO_read_base;
- char* _IO_write_base;
- char* _IO_write_ptr;
- char* _IO_write_end;
- char* _IO_buf_base;
- char* _IO_buf_end;
- char *_IO_save_base;
- char *_IO_backup_base;
- char *_IO_save_end;
- struct _IO_marker *_markers;
- struct _IO_FILE *_chain;
- int _fileno;
- };
1.char* _IO_read_ptr; char* _IO_read_end; char* _IO_read_base;
2.char* _IO_write_base; char* _IO_write_ptr; char* _IO_write_end;
3.char* _IO_buf_base; char* _IO_buf_end;
其中
_IO_read_base 指向"讀緩衝區"
_IO_read_end 指向"讀緩衝區"的末尾
_IO_read_end - _IO_read_base "讀緩衝區"的長度
_IO_write_base 指向"寫緩衝區"
_IO_write_end 指向"寫緩衝區"的末尾
_IO_write_end - _IO_write_base "寫緩衝區"的長度
_IO_buf_base 指向"緩衝區"
_IO_buf_end 指向"緩衝區"的末尾
_IO_buf_end - _IO_buf_base "緩衝區"的長度
上面的定義貌似給出了3個緩衝區,實際上上面的_IO_read_base,_IO_write_base, _IO_buf_base都指向了同一個緩衝區.這個緩衝區跟上面程序中的char buf[5];沒有任何關係.
他們在第一次buffered I/O操作時由庫函數自動申請空間,最後由相應庫函數負責釋放.(再次聲明,這裏只是glibc的實現,別的實現可能會不同,後面就不再強調了)
請看下面的程序(這裏給的是stdin,行緩衝的例子):
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- int main(void)
- {
- char buf[5];
- FILE *myfile =stdin;
- printf("before reading\n");
- printf("read buffer base %p\n", myfile->_IO_read_base);
- printf("read buffer length %d\n", myfile->_IO_read_end - myfile->_IO_read_base);
- printf("write buffer base %p\n", myfile->_IO_write_base);
- printf("write buffer length %d\n", myfile->_IO_write_end - myfile->_IO_write_base);
- printf("buf buffer base %p\n", myfile->_IO_buf_base);
- printf("buf buffer length %d\n", myfile->_IO_buf_end - myfile->_IO_buf_base);
- printf("\n");
- fgets(buf, 5, myfile);
- fputs(buf, myfile);
- printf("\n");
- printf("after reading\n");
- printf("read buffer base %p\n", myfile->_IO_read_base);
- printf("read buffer length %d\n", myfile->_IO_read_end - myfile->_IO_read_base);
- printf("write buffer base %p\n", myfile->_IO_write_base);
- printf("write buffer length %d\n", myfile->_IO_write_end - myfile->_IO_write_base);
- printf("buf buffer base %p\n", myfile->_IO_buf_base);
- printf("buf buffer length %d\n", myfile->_IO_buf_end - myfile->_IO_buf_base);
- return 0;
- }
上面的例子只是說明了buffered I/O緩衝區的存在,下面從全緩衝,行緩衝和無緩衝3個方面看一下buffered I/O是如何工作的.
1.1. 全緩衝
下面是APUE上的原話:
全緩衝"在填滿標準I/O緩衝區後才進行實際的I/O操作.對於駐留在磁盤上的文件通常是由標準I/O庫實施全緩衝的"書中這裏"實際的I/O操作"實際上容易引起誤導,這裏並不是讀寫磁盤,而應該是進行read或write的系統調用
下面兩個例子會說明這個問題
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- int main(void)
- {
- char buf[5];
- char *cur;
- FILE *myfile;
- myfile = fopen("bbb.txt", "r");
- printf("before reading, myfile->_IO_read_ptr: %d\n", myfile->_IO_read_ptr - myfile->_IO_read_base);
- fgets(buf, 5, myfile); //僅僅讀4個字符
- cur = myfile->_IO_read_base;
- while (cur < myfile->_IO_read_end) //實際上讀滿了這個緩衝區
- {
- printf("%c",*cur);
- cur++;
- }
- printf("\nafter reading, myfile->_IO_read_ptr: %d\n", myfile->_IO_read_ptr - myfile->_IO_read_base);
- return 0;
- }
上例中,fgets(buf, 5, myfile); 僅僅讀4個字符,但是,緩衝區已被寫滿,但是_IO_read_ptr卻向前移動了5位,下次再次調用讀操作時,只要要讀的位數不超過myfile->_IO_read_end - myfile->_IO_read_ptr那麼就不需要再次調用系統調用read,只要將數據從myfile的緩衝區拷貝到buf即可(從myfile->_IO_read_ptr開始拷貝)
全緩衝讀的時候:
_IO_read_base始終指向緩衝區的開始
_IO_read_end始終指向已從內核讀入緩衝區的字符的下一個(對全緩衝來說,buffered I/O讀每次都試圖都將緩衝區讀滿)
_IO_read_ptr始終指向緩衝區中已被用戶讀走的字符的下一個
(_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr == _IO_read_end)時則已經到達文件末尾,其中_IO_buf_base-_IO_buf_end是緩衝區的長度
一般大體的工作情景爲:
第一次fgets(或其他的)時,標準I/O會調用read將緩衝區充滿,下一次fgets不調用read而是直接從該緩衝區中拷貝數據,直到緩衝區的中剩餘的數據不夠時,再次調用read.在這個過程中,_IO_read_ptr就是用來記錄緩衝區中哪些數據是已讀的,哪些數據是未讀的
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- int main(void)
- {
- char buf[2048]={0};
- int i;
- FILE *myfile;
- myfile = fopen("aaa.txt", "r+");
- i= 0;
- while (i<2048)
- {
- fwrite(buf+i, 1, 512, myfile);
- i +=512;
- //註釋掉這句則可以寫入aaa.txt
- myfile->_IO_write_ptr = myfile->_IO_write_base;
- printf("%p write buffer base\n", myfile->_IO_write_base);
- printf("%p buf buffer base \n", myfile->_IO_buf_base);
- printf("%p read buffer base \n", myfile->_IO_read_base);
- printf("%p write buffer ptr \n", myfile->_IO_write_ptr);
- printf("\n");
- }
- return 0;
- }
全緩衝時,只有當標準I/O自動flush(比如當緩衝區已滿時)或者手工調用fflush時,標準I/O纔會調用一次write系統調用.例子中,fwrite(buf+i, 1, 512, myfile);這一句只是將buf+i接下來的512個字節寫入緩衝區,由於緩衝區未滿,標準I/O並未調用write.此時,myfile->_IO_write_ptr = myfile->_IO_write_base;會導致標準I/O認爲沒有數據寫入緩衝區,所以永遠不會調用write,這樣aaa.txt文件得不到寫入.註釋掉myfile->_IO_write_ptr = myfile->_IO_write_base;前後,看看效果.
全緩衝寫的時候:
_IO_write_base始終指向緩衝區的開始
_IO_write_end全緩衝的時候,始終指向緩衝區的最後一個字符的下一個
(對全緩衝來說,buffered I/O寫總是試圖在緩衝區寫滿之後,再系統調用write)
_IO_write_ptr始終指向緩衝區中已被用戶寫入的字符的下一個
flush的時候,將_IO_write_base和_IO_write_ptr之間的字符通過系統調用write寫入內核
1.2. 行緩衝
下面是APUE上的原話:
行緩衝"當輸入輸出中遇到換行符時,標準I/O庫執行I/O操作. "書中這裏"執行O操作"也容易引起誤導,這裏不是讀寫磁盤,而應該是進行read或write的系統調用
下面兩個例子會說明這個問題
第一個例子可以用來說明下面這篇帖子的問題
http://bbs.chinaunix.net/viewthread.php?tid=954547
- #include <stdlib.h>
- #include <stdio.h>
- int main(void)
- {
- char buf[5];
- char buf2[10];
- fgets(buf, 5, stdin); //第一次輸入時,超過5個字符
- puts(stdin->_IO_read_ptr);//本句說明整行會被一次全部讀入緩衝區,而非僅僅上面需要的個字符
- stdin->_IO_read_ptr = stdin->_IO_read_end; //標準I/O會認爲緩衝區已空,再次調用read,註釋掉,再看看效果
- printf("\n");
- puts(buf);
- fgets(buf2, 10, stdin);
- puts(buf2);
- return 0;
- }
行緩衝讀的時候,
_IO_read_base始終指向緩衝區的開始
_IO_read_end始終指向已從內核讀入緩衝區的字符的下一個
_IO_read_ptr始終指向緩衝區中已被用戶讀走的字符的下一個
(_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr == _IO_read_end)時則已經到達文件末尾
其中_IO_buf_base-_IO_buf_end是緩衝區的長度
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- char buf[5]={'1','2', '3', '4', '5'}; //最後一個不要是\n,是\n的話,標準I/O會自動flush的,這是行緩衝跟全緩衝的重要區別
- void writeLog(FILE *ftmp)
- {
- fprintf(ftmp, "%p write buffer base\n", stdout->_IO_write_base);
- fprintf(ftmp, "%p buf buffer base \n", stdout->_IO_buf_base);
- fprintf(ftmp, "%p read buffer base \n", stdout->_IO_read_base);
- fprintf(ftmp, "%p write buffer ptr \n", stdout->_IO_write_ptr);
- fprintf(ftmp, "\n");
- }
- int main(void)
- {
- int i;
- FILE *ftmp;
- ftmp = fopen("ccc.txt", "w");
- i= 0;
- while (i<4)
- {
- fwrite(buf, 1, 5, stdout);
- i++;
- *stdout->_IO_write_ptr++ = '\n';//可以單獨把這句打開,看看效果
- //getchar();//getchar()會將標準I/O將緩衝區輸出
- //打開下面的註釋,你就會發現屏幕上什麼輸出也沒有
- //stdout->_IO_write_ptr = stdout->_IO_write_base;
- writeLog(ftmp); //這個只是爲了查看緩衝區指針的變化
- }
- return 0;
- }
上面這個是關於行緩衝寫的例子.
stdout->_IO_write_ptr = stdout->_IO_write_base;會使得標準I/O認爲緩衝區是空的,從而沒有任何輸出.可以將上面程序中的註釋分別去掉,看看運行結果
行緩衝時,下面3個條件之一會導致緩衝區立即被flush
1. 緩衝區已滿
2. 遇到一個換行符;比如將上面例子中buf[4]改爲'\n'時
3. 再次要求從內核中得到數據時;比如上面的程序加上getchar()會導致馬上輸出
行緩衝寫的時候:
_IO_write_base始終指向緩衝區的開始
_IO_write_end始終指向緩衝區的開始
_IO_write_ptr始終指向緩衝區中已被用戶寫入的字符的下一個
flush的時候,將_IO_write_base和_IO_write_ptr之間的字符通過系統調用write寫入內核
1.3. 無緩衝
無緩衝時,標準I/O不對字符進行緩衝存儲.典型代表是stderr,這裏的無緩衝,並不是指緩衝區大小爲0,其實,還是有緩衝的,大小爲1
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- int main(void)
- {
- fputs("stderr", stderr);
- printf("%d\n", stderr->_IO_buf_end - stderr->_IO_buf_base);
- return 0;
- }
1.4 feof的問題
CU上已經有無數的帖子在探討feof了,這裏從緩衝區的角度去考察一下.對於一個空文件,爲什麼要先讀一下,才能用feof判斷出該文件到了結尾了呢?
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- int main(void)
- {
- char buf[5];
- char buf2[10];
- fgets(buf, sizeof(buf), stdin);//輸入要於4個,少於13個字符才能看出效果
- puts(buf);
- //交替註釋下面兩行
- //stdin->_IO_read_end = stdin->_IO_read_ptr+1;
- stdin->_IO_read_end = stdin->_IO_read_ptr + sizeof(buf2)-1;
- fgets(buf2, sizeof(buf2), stdin);
- puts(buf2);
- if (feof(stdin))
- printf("input end\n");
- return 0;
- }
1.5. 其他說明
很多新手有一個誤解,就是fgets, fputs代表行緩衝,fread, fwrite代表全緩衝 fgetc, fputc代表無緩衝等等.其實不是這樣的,是什麼樣的緩衝跟使用那個函數沒有關係,而跟你讀寫什麼類型的文件有關係.上面的例子中多次在全緩衝中使用fgets, fputs,而在行緩衝中使用fread, fwrite下面的是引至APUE的,實際上ISO C要求:
1.當且僅當標準輸入和標準輸出並不涉及交互式設備時,他們纔是全緩衝的
2.標準錯誤輸出決不是全緩衝的.
很多系統默認使用下列類型的標準:
1.標準錯誤輸出是不帶緩衝的.
2.如若是涉及終端設備的其他流,則他們是行緩衝的;否則是全緩衝的.