本文轉載於:http://www.360doc.com/content/11/0521/11/5455634_118306098.shtmlphp
首先,先稍微瞭解系統調用的概念:
系統調用,英文名systemcall,每一個操做系統都在內核裏有一些內建的函數庫,這些函數能夠用來完成一些系統系統調用把應用程序的請求傳給內核,調用相應的的內核函數完成所需的處理,將處理結果返回給應用程序,若是沒有系統調用和內核函數,用戶將不能編寫大型應用程序,及別的功能,這些函數集合起來就叫作程序接口或應用編程接口(Application Programming Interface,API),咱們要在這個系統上編寫各類應用程序,就是經過這個API接口來調用系統內核裏面的函數。若是沒有系統調用,那麼應用程序就失去內核的支持。
如今,再聊不帶緩存的I/O操做:
linix對IO文件的操做分爲不帶緩存的IO操做和標準IO操做(即帶緩存),剛開始,要明確如下幾點:
1:不帶緩存,不是直接對磁盤文件進行讀取操做,像read()和write()函數,它們都屬於系統調用,只不過在用戶層沒有緩存,因此叫作無緩存IO,但對於內核來講,仍是進行了緩存,只是用戶層看不到罷了。若是這一點看不懂,請看第二點;
2:帶不帶緩存是相對來講的,若是你要寫入數據到文件上時(就是寫入磁盤上),內核先將數據寫入到內核中所設的緩衝儲存器,假如這個緩衝儲存器的長度是100個字節,你調用系統函:html
ssize_twrite (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()。優化
若是看到這裏尚未一點眉目的話,那就比較麻煩了,但願下面兩條總結可以幫上忙:url
無緩存IO操做數據流向路徑:數據——內核緩存區——磁盤spa
標準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
首先,要明確,unbufferedI/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> |
buffered I/O中的"buffer"究竟是指什麼呢?
這個buffer在什麼地方呢?
FILE是什麼呢?它的空間是怎麼分配的呢?
要弄清楚這些問題,就要看看FILE是如何定義和運做的了.
(特別說明,在平時寫程序時,不用也不要關心FILE是如何定義和運做的,最好不要直接操做
它,這裏使用它,只是爲了說明buffered IO)
下面的這個是glibc給出的FILE的定義,它是實現相關的,別的平臺定義方式不一樣.
struct _IO_FILE { |
上面的定義中有三組重要的字段:
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> |
運行的結果以下:
before reading
read buffer base (nil)
read buffer length 0
write buffer base (nil)
write buffer length 0
buf buffer base (nil)
buf buffer length 0
123
after reading
read buffer base 0xb77c5000
read buffer length 4
write buffer base 0xb77c5000
write buffer length 0
buf buffer base 0xb77c5000
buf buffer length 1024
能夠看到,在讀操做以前,myfile的緩衝區是沒有被分配的,在一次讀以後,myfile的緩衝區才被分配.
這個緩衝區既不是內核中的緩衝區,也不是用戶分配的緩衝區,而是有用戶進程空間中的由buffered I/O系統負責維護的緩衝區.
用setbuf設置stdin緩衝區大小後(printf("bufbuffer length %d\n", myfile->_IO_buf_end - myfile->_IO_buf_base);)默認是8192,可是隻能讀setbuf設置的大小,也就是:printf("readbuffer length %d\n", myfile->_IO_read_end - myfile->_IO_read_base);若是越界程序將執行錯誤
(固然,用戶能夠能夠維護該緩衝區,這裏不作討論了)
上面的例子只是說明了buffered I/O緩衝區的存在,下面從全緩衝,行緩衝和無緩衝3個方面看一下buffered I/O
是如何工做的.
1.1. 全緩衝
下面是APUE上的原話:
全緩衝"在填滿標準I/O緩衝區後才進行實際的I/O操做.對於駐留在磁盤上的文件一般是由標準I/O庫實施全緩衝的"
書中這裏"實際的I/O操做"實際上容易引發誤導,這裏並非讀寫磁盤,而應該是進行read或write的系統調用
下面兩個例子會說明這個問題
#include <stdlib.h> |
上面提到的bbb.txt文件的內容是由不少行的"123456789"組成
運行結果:
before reading, myfile->_IO_read_ptr:0
123456789
123456789
123456789
123456789
123456789
123456789
123456789
123456789
123456789
123456789
after reading, myfile->_IO_read_ptr:4
上例中,fgets(buf, 5, myfile);僅僅讀4個字符,可是,緩衝區已被寫滿,(這個緩衝區默認是4096)
可是_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> |
上面這個是關於全緩衝寫的例子.
全緩衝時,只有當標準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文件得不到寫入.
沒有註釋掉時的運行結果:
0xb77f9000 write buffer base
0xb77f9000 buf buffer base
0xb77f9000 read buffer base
0xb77f9000 write buffer ptr
0xb77f9000 write buffer base
0xb77f9000 buf buffer base
0xb77f9000 read buffer base
0xb77f9000 write buffer ptr
0xb77f9000 write buffer base
0xb77f9000 buf buffer base
0xb77f9000 read buffer base
0xb77f9000 write buffer ptr
0xb77f9000 write buffer base
0xb77f9000 buf buffer base
0xb77f9000 read buffer base
0xb77f9000 write buffer ptr
註釋掉myfile->_IO_write_ptr = myfile->_IO_write_base;先後,看看效果
註釋掉之後的運行結果:
0xb774e000 write buffer base
0xb774e000 buf buffer base
0xb774e000 read buffer base
0xb774e200 write buffer ptr
0xb774e000 write buffer base
0xb774e000 buf buffer base
0xb774e000 read buffer base
0xb774e400 write buffer ptr
0xb774e000 write buffer base
0xb774e000 buf buffer base
0xb774e000 read buffer base
0xb774e600 write buffer ptr
0xb774e000 write buffer base
0xb774e000 buf buffer base
0xb774e000 read buffer base
0xb774e800 write buffer ptr
上面write buffer ptr在每次往緩衝中寫512個字節後地址就增加512.
全緩衝寫的時候:
_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操做. "
書中這裏"執行IO操做"也容易引發誤導,這裏不是讀寫磁盤,而應該是進行read或write的系統調用
下面兩個例子會說明這個問題
第一個例子能夠用來講明下面這篇帖子的問題
http://bbs.chinaunix.net/viewthread.php?tid=954547
#include <stdlib.h> //而非僅僅上面須要的個字符 |
上例中, 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是緩衝區的長度
#include <stdlib.h> |
這個例子將將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
#include <stdlib.h> |
對無緩衝的流的每次讀寫操做都會引發系統調用
1.4 feof的問題
CU上已經有無數的帖子在探討feof了,這裏從緩衝區的角度去考察一下.
對於一個空文件,爲何要先讀一下,才能用feof判斷出該文件到告終尾了呢?
#include <stdlib.h> |
運行上面的程序,輸入多於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.如如果涉及終端設備的其餘流,則他們是行緩衝的;不然是全緩衝的.