不带缓存的I/O和标准(带缓存的)I/O
首先,先稍微了解系統調用的概念:
系統調用,英文名system call,每個操作系統都在內核里有一些內建的函數庫,這些函數可以用來完成一些系統系統調用把應用程序的請求傳給內核,調用相應的的內核函數完成所需的處理,將處理結果返回給應用程序,如果沒有系統調用和內核函數,用戶將不能編寫大型應用程序,及別的功能,這些函數集合起來就叫做程序接口或應用編程接口(Application Programming Interface,API),我們要在這個系統上編寫各種應用程序,就是通過這個API接口來調用系統內核里面的函數。如果沒有系統調用,那么應用程序就失去內核的支持。
————————————————————————————————————————————————
現在,再聊不帶緩存的I/O操作:
linux對IO文件的操作分為不帶緩存的IO操作和標準IO操作(即帶緩存),剛開始,要明確以下幾點:
1.不帶緩存,不是直接對磁盤文件進行讀取操作,像read()和write()函數,它們都屬于系統調用,只不過在用戶層沒有緩存,所以叫做無緩存IO,但對于內核來說,還是進行了緩存,只是用戶層看不到罷了。如果這一點看不懂,請看第二點;
2:帶不帶緩存是相對來說的,如果你要寫入數據到文件上時(就是寫入磁盤上),內核先將數據寫入到內核中所設的緩沖儲存器,假如這個緩沖儲存器的長度是100個字節,你調用系統函數:
寫操作時,設每次寫入長度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的工作原理,借用了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; }buffered I/O中的”buffer”到底是指什么呢?
這個buffer在什么地方呢?
FILE是什么呢?它的空間是怎么分配的呢?
要弄清楚這些問題,就要看看FILE是如何定義和運作的了.(特別說明,在平時寫程序時,不用也不要關心FILE是如何定義和運作的,最好不要直接操作它,這里使用它,只是為了說明buffered IO)下面的這個glibc給出的FILE的定義,它是實現相關的,別的平臺定義方式不同.
struct _IO_FILE { int _flags; #define _IO_file_flags _flagschar* _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; };上面的定義中有三組重要的字段:
————————————————————————————————————————————————
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;
其中,
_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; }可以看到,在讀操作之前,myfile的緩沖區是沒有被分配的,在一次讀之后,myfile的緩沖區才被分配.這個緩沖區既不是內核中的緩沖區,也不是用戶分配的緩沖區,而是有用戶進程空間中的由buffered I/O系統負責維護的緩沖區.(當然,用戶可以可以維護該緩沖區,這里不做討論了)上面的例子只是說明了buffered I/O緩沖區的存在,下面從全緩沖,行緩沖和無緩沖3個方面看一下buffered I/O是如何工作的.
1.1. 全緩沖
下面是APUE上的原話:
全緩沖”在填滿標準I/O緩沖區后才進行實際的I/O操作.對于駐留在磁盤上的文件通常是由標準I/O庫實施全緩沖的”書中這里”實際的I/O操作”實際上容易引起誤導,這里并不是讀寫磁盤,而應該是進行read或write的系統調用.下面兩個例子會說明這個問題:
上面提到的bbb.txt文件的內容是由很多行的”123456789”組成
上例中, 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 就是用來記錄緩沖區中哪些數據是已讀的,哪些數據是未讀的.
上面這個是關于全緩沖寫的例子.
全緩沖時,只有當標準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操作.”書中這里”執行I/O操作”也容易引起誤導,這里不是讀寫磁盤,而應該是進行read或write的系統調用.
下面兩個例子會說明這個問題
第一個例子可以用來說明下面這篇帖子的問題
http://bbs.chinaunix.net/viewthread.php?tid=954547
上例中, fgets(buf, 5, stdin); 僅僅需要4個字符,但是,輸入行中的其他數據也被寫入緩沖區,但是_ IO_read_ptr 向前移動了5位,下次再次調用fgets操作時,就不需要再次調用系統調用 read,只要將數據從stdin的緩沖區拷貝到 buf2 即可(從 stdin->_IO_read_ptr 開始拷貝).stdin->_IO_read_ptr = stdin->_IO_read_end;會導致標準I/O會認為緩沖區已空,再次 fgets 則需要再次調用 read.比較一下將該句注釋掉前后的效果
行緩沖讀的時候,
_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是緩沖區的長度
這個例子將將FILE結構中指針的變化寫入的文件ccc.txt 運行后可以有興趣的話,可以看看.
上面這個是關于行緩沖寫的例子.
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
對無緩沖的流的每次讀寫操作都會引起系統調用
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; }運行上面的程序,輸入多于4個,少于13個字符,并且以連按兩次ctrl+d為結束(不要按回車)從上面的例子,可以看出,每當滿足 (_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr == _IO_read_end)時,標準I/O則認為已經到達文件末尾,feof(stdin) 才會被設置其中 _IO_buf_base-_IO_buf_end 是緩沖區的長度.也就是說,標準I/O是通過它的緩沖區來判斷流是否要結束了的.這就解釋了為什么即使是一個空文件,標準I/O也需要讀一次,才能使用feof判斷釋放為空
1.5. 其他說明
很多新手有一個誤解,就是 fgets, fputs 代表行緩沖, fread, fwrite 代表全緩沖,fgetc, fputc代表無緩沖等等.其實不是這樣的,是什么樣的緩沖跟使用那個函數沒有關系,而跟你讀寫什么類型的文件有關系.上面的例子中多次在全緩沖中使用fgets, fputs,而在行緩沖中使用fread, fwrite
下面的是引至APUE的
實際上
ISO C要求:
1.當且僅當標準輸入和標準輸出并不涉及交互式設備時,他們才是全緩沖的
2.標準輸出決不是全緩沖的.
很多系統默認使用下列類型的標準:
1.標準輸出是不帶緩沖的.
2.如若是涉及終端設備的其他流,則他們是行緩沖的;否則是全緩沖的.
總結
以上是生活随笔為你收集整理的不带缓存的I/O和标准(带缓存的)I/O的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux连接交换机软件,如何用超级终端
- 下一篇: 基于《知网》的词汇语义相似度计算以及复现