1. 程式人生 > >帶緩衝的I/O操作和不帶緩衝的I/O操作

帶緩衝的I/O操作和不帶緩衝的I/O操作

首先要明白不帶緩衝的概念:所謂不帶緩衝,並不是指核心不提供緩衝,而是隻單純的系統呼叫,不是函式庫的呼叫。系統核心對磁碟的讀寫都會提供一個塊緩衝(在有些地方也被稱為核心快取記憶體),當用write函式對其寫資料時,直接呼叫系統呼叫,將資料寫入到塊緩衝進行排隊,當塊緩衝達到一定的量時,才會把資料寫入磁碟。因此所謂的不帶緩衝的I/O是指程序不提供緩衝功能(但核心還是提供緩衝的)。每呼叫一次write或read函式,直接系統呼叫。
帶緩衝的I/O是指程序對輸入輸出流進行了改進,提供了一個流緩衝,當用fwrite函式網磁碟寫資料時,先把資料寫入流緩衝區中,當達到一定條件,比如流緩衝區滿了,或重新整理流緩衝,這時候才會把資料一次送往核心提供的塊緩衝,再經塊緩衝寫入磁碟。(雙重緩衝)

因此,帶緩衝的I/O在往磁碟寫入相同的資料量時,會比不帶緩衝的I/O呼叫系統呼叫的次數要少。

        看正常情況下,和磁碟互動的讀寫檔案是怎麼個流程!

        當應用程式嘗試讀取某塊資料的時候,如果這塊資料已經存放在頁快取中,那麼這塊資料就可以立即返回給應用程式,而不需要經過實際的物理讀盤操作。當然,如果資料在應用程式讀取之前並未被存放在頁快取中(也就是上面提到的核心快取記憶體),那麼就需要先將資料從磁碟讀到頁快取中去。對於寫操作來說,應用程式也會將資料先寫到頁快取中去(這裡所說的寫到頁快取中,如果是呼叫標準庫I/O進行寫,那麼首先是寫到標準庫的緩衝區內,如果標準庫的緩衝區寫滿以後,在寫到頁緩衝內;如果是系統呼叫,那麼直接寫到頁緩衝內

),資料是否被立即寫到磁碟上去取決於應用程式所採用的寫操作機制:如果使用者採用的是同步寫機制,那麼資料會立即被寫回到磁碟上,應用程式會一直等到資料被寫完為止;如果使用者採用的是延遲寫機制,那麼應用程式就完全不需要等到資料全部被 寫回到磁碟,資料只要被寫到頁快取中去就可以了。在延遲寫機制的情況下,作業系統會定期地將放在頁快取中的資料刷到磁碟上。與非同步寫機制不同的是,延遲寫機制在資料完全寫到磁碟上得時候不會通知應用程式,而非同步寫機制在資料完全寫到磁碟上得時候是會返回給應用程式的。所以延遲寫機制本省是存在資料丟失的風險的,而非同步寫機制則不會有這方面的擔心。

        下面聊聊不帶緩衝的I/O

不帶快取,不是直接對磁碟檔案進行讀取操作,像read()和write()函式,它們都屬於系統呼叫,只不過在使用者層沒有快取,所以叫做無快取IO,但對於核心來說,還是進行了快取,只是使用者層看不到罷了。

帶不帶快取是相對來說的,如果你要寫入資料到檔案上時(就是寫入磁碟上),核心先將資料寫入到核心中所設的緩衝儲存器,假如這個緩衝儲存器的長度是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()將資料寫入到核心緩衝內,如果核心緩衝也被填滿,或者核心進行fflush操作,那麼核心緩衝區內資料就別寫入到檔案(實質是磁碟)上,看到這裡,你用該明白一點,標準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來分配快取)。

下面的東西是我從網上查到的對這兩者的理解,我覺得還是很到位的:

以下主要討論關於open,write等基本系統IO的帶緩衝與不帶緩衝的差別

帶快取的檔案操作是標準C庫的實現,第一次呼叫帶快取的檔案操作函式時標準庫會自動分配記憶體並且讀出一段固定大小的內容儲存在快取中。所以以後每次的讀寫操作並不是針對硬碟上的檔案直接進行的,而是針對記憶體中的快取的。何時從硬碟中讀取檔案或者向硬碟中寫入檔案有標準庫的機制控制。不帶快取的檔案操作通常都是系統提供的系統呼叫, 更加低階,直接從硬碟中讀取和寫入檔案,由於IO瓶頸的原因,速度並不如意,而且原子操作需要程式設計師自己保證,但使用得當的話效率並不差。另外標準庫中的帶快取檔案IO 是呼叫系統提供的不帶快取IO實現的。

“術語不帶緩衝指的是每個read和write都呼叫核心中的一個系統呼叫。所有的磁碟I/O都要經過核心的塊緩衝(也稱核心的緩衝區快取記憶體),唯一例外的是對原始磁碟裝置的I/O。既然read或write的資料都要被核心緩衝,那麼術語“不帶緩衝的I/O“指的是在使用者的程序中對這兩個函式不會自動緩衝,每次read或write就要進行一次系統呼叫。“--------摘自<unix環境程式設計>

程式中用open和write開啟建立並把“hello world“寫入檔案test.txt,相應用fopen和fwrite操作檔案test2.txt。程式執行到open和fopen之後,sleep 15秒,這時用ls檢視生成了檔案沒,這時用open開啟的test.txt出現了,用fopen開啟的的test2.txt也出現了;當程式執行完write和 fwrite之後,在15秒睡眠期間,用cat檢視test.txt,其內容是“hello,world”;但是此時用cat檢視test2.txt,其內容為空。睡眠結束後,執行了close(fd),此時再用cat檢視test2.txt,發現其內容也有了:“hello,world”。該例子證明了open和write是不帶緩衝的,即程式一執行其io操作也立即執行,不會停留在系統提供的緩衝裡,不需等到close操作完才執行。與之相比的fopen和fwrite則是帶緩衝的,(一般)要等到fclose操作完後才會執行。

相關的原始碼示例如下:

複製程式碼
#include <unistd.h>
#include <iostream>
#include <fcntl.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>

using namespace std;

int main(){
int fd;
FILE *file;
char *s="hello,world\n";
if((fd=open("test.txt",O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR))==-1){
cout<<"Error open file"<<endl;
return -1;
}
if((file=fopen("test2.txt","w"))==NULL){
cout<<"Error Open File."<<endl;
return -1;
}
cout<<"File has been Opened."<<endl;
sleep(15);
if(write(fd,s,strlen(s))<strlen(s)){
cout<<"Write Error"<<endl;

return -1;
}
if(fwrite(s,sizeof(char),strlen(s),file)<strlen(s)){
cout<<"Write Error in 2."<<endl;

return -1;
}
cout<<"After write"<<endl;

sleep(15);
cout<<"After sleep."<<endl;

close(fd);
return 0;
}
複製程式碼

 
以 ssize_t write(int filedes, const void *buff, size_t nbytes)和size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *fp)來講講自己對unix系統下帶快取的I/O和不帶快取的I/O的區別。

首先要清楚一個概念,所謂的帶快取並不是指上面兩個函式的buff引數

當將資料寫到檔案上時,核心先將該資料寫到快取,如果該快取未滿,則並不將其排入輸出佇列,直到快取寫滿或者核心再次需要重新使用此快取時才將其排入輸入佇列,待其到達隊首,再進行實際的I/O操作,也就是此時才把資料真正寫到磁碟,這種技術叫延遲寫。

現在假設核心所設的快取是100個位元組,如果你使用write,且buff的size為10,當你要把9個同樣的buff寫到檔案時,你需要呼叫9次write,也就是9次系統呼叫,此時也並沒有寫到硬碟,如果想立即寫到硬碟,呼叫fsync,可以進行實際的I/O操作。

標準I/O,也就是帶快取的I/O採用 FILE*,FILE實際上包含了為管理流所需要的所有資訊:實際I/O的檔案描述符,指向流快取的指標(標準I/O快取,由malloc分配,又稱為使用者態程序空間的快取,區別於核心所設的快取),快取長度,當前在快取中的位元組數,出錯標誌等,假設流快取的長度為50位元組,把以上的資料寫到檔案,則只需要2次系統呼叫(fwrite呼叫write系統呼叫),因為先把資料寫到流快取,當其滿以後或者呼叫fflush時才填入核心快取,所以進行了2次的系統呼叫write。

fflush將流所有未寫的資料送入(重新整理)到核心(核心緩衝區),fsync將所有核心緩衝區的資料寫到檔案(磁碟)。至於究竟寫到了檔案中還是核心緩衝區中對於程序來說是沒有差別 的,如果程序A和程序B開啟同一檔案,程序A寫到核心I/O緩衝區中的資料從程序B也能讀到,因為核心空間是程序共享的,
而c標準庫的I/O緩衝區則不具有這一特性,因為程序的使用者空間是完全獨立的.(個人覺得這句話非常重要)

不帶快取的read和write是相對於 fread/fwrite等流函式來說明的,因為fread和fwrite是使用者函式(3),所以他們會在使用者層進行一次資料的快取,而read/write是系統呼叫(2)所以他們在使用者層是沒有快取的,所以稱read和write是無快取的IO,其實對於核心來說還是進行了快取,不過使用者層看不到罷了。

上面的內容介紹了庫緩衝機制,其中也提到了核心緩衝區這個概念,到底核心緩衝存在的價值是很麼呢:

為什麼總是需要將資料由核心緩衝區換到使用者緩衝區或者相反呢?

答:使用者程序是執行在使用者空間的,不能直接操作核心緩衝區的資料。 使用者程序進行系統呼叫的時候,會由使用者態切換到核心態,待核心處理完之後再返回使用者態

用緩衝技術能很明顯的提高系統效率。核心與外圍裝置的資料交換,核心與使用者空間的資料交換都是比較費時的,使用緩衝區就是為了優化這些費時的操作。其實核心到使用者空間的操作本身是不buffer的,是由I/O庫用buffer來優化了這個操作。比如read本來從核心讀取資料時是比較費時的,所以一次取出一塊,以避免多次陷入核心。
      應用核心緩衝區的 主要思想就是一次讀入大量的資料放在緩衝區,需要的時候從緩衝區取得資料
管理員模式和使用者模式之間的切換需要消耗時間,但相比之下,磁碟的I/O操作消耗的時間更多,為了提高效率,核心也使用緩衝區技術來提高對磁碟的訪問速度
磁碟是資料塊 的集合,核心會對磁碟上的資料塊做緩衝。核心將磁碟上的資料塊複製到核心緩衝區中,當一個使用者空間中的程序要從磁碟上讀資料時,核心一般不直接讀磁碟,而 是將核心緩衝區中的資料複製到程序的緩衝區中。當程序所要求的資料塊不在核心緩衝區時,核心會把相應的資料塊加入到請求佇列,然後把該程序掛起,接著為其 他程序服務。一段時間之後(其實很短的時間),核心把相應的資料塊從磁碟讀到核心緩衝區,然後再把資料複製到程序的緩衝區中,最後喚醒被掛起的程序
 注:理解核心緩衝區技術的原理有助於更好的掌握系統呼叫read&write,read把資料從核心緩衝區複製到程序緩衝區,write把資料從程序緩衝區複製到核心緩衝區,它們不等價於資料在核心緩衝區和磁碟之間的交換
      從理論上講,核心可以在任何時候寫磁碟,但並不是所有的write操作都會導致核心的寫動作。核心會把要寫的資料暫時存在緩衝區中,積累到一定數量後再一 次寫入。有時會導致意外情況,比如斷電,核心還來不及把核心緩衝區中的資料寫道磁碟上,這些更新的資料就會丟失。
      應用核心緩衝技術導致的結果是:提高了磁碟的I/O效率;優化了磁碟的寫操作;需要及時的將緩衝資料寫到磁碟。