IO之核心buffer----"buffer cache"

TuxedoLinux發表於2018-06-03
 

分類: LINUX


"buffer cache"

要理解"buffer cache"這個東西,需要澄清一下概念:

一般情況下,程式在io的時候,要依賴於核心中的一個buffer模組來和外存發生資料交換行為。另一個角度來說,資料從應用程式自己的buffer流動到外存,中間要先拷貝到核心的buffer中,然後再由核心決定什麼時候把這些載有資料的核心buffer寫出到外存。

"buffer cache"僅僅被核心用於常規檔案(磁碟檔案)的I/O操作。

 

核心中的buffer模組,就是今天的主題----"buffer cache"(buffer,cache的功能兼備)

一般情況下,Read,write系統呼叫並不直接訪問磁碟。這兩個系統呼叫僅僅是在使用者空間和核心空間的buffer之間傳遞目標資料。舉個例子,下面的write系統呼叫僅僅是把3個位元組從使用者空間拷貝到核心空間的buffer之後就直接返回了

write(fd,”abc”,3);

在以後的某個時間點上,核心把裝著“abc”三個位元組的buffer寫入(flush)磁碟……

如果另外的程式在這個過程中想要讀剛才被開啟寫的那個檔案怎麼辦?答案是:核心會從剛才的buffer提供要讀取的資料,而不是從磁碟讀。

 

介紹完“寫出”,該介紹“讀入”了。

當前系統上第一次讀一個檔案時,Read系統呼叫觸發核心以block為單位從磁碟讀取檔案資料,並把資料blocks存入核心buffer,然後read不斷地從這個buffer取需要的資料,直到buffer中的資料全部被讀完,接下來,核心從磁碟按順序把當前檔案後面的blocks再讀入核心buffer,然後read重複之前的動作…

一般的檔案訪問,都是這種不斷的順序讀取的行為,為了加速應用程式讀磁碟,unix的設計者們為這種普遍的順序讀取行為,設計了這樣的機制----預讀,來保證程式在想讀後續資料的時候,這些後續資料已經的由核心預先從磁碟讀好並且放在buffer裡了。這麼做的原因是磁碟的io訪問比記憶體的io訪問要慢很多,指數級的差別。

 

read,write從語義和概念上來說,本來是必須要直接和磁碟互動的,呼叫時間非常長,應用每次在使用這兩個系統的時候,從表象上來說都是被卡住。而有了這些buffer,這些系統呼叫就直接和buffer互動就可以了,大幅的加速了應用執行。

Linux核心並沒有規定"buffer cache"的尺寸上線,原則上來說,除了系統正常執行所必需和使用者程式自身所必需的之外的記憶體都可以被"buffer cache"使用。而系統和使用者程式需要申請更多的記憶體的時候,"buffer cache"的記憶體釋放行為會被觸發,一些長久未被讀取,以及被寫過的髒頁就會被釋放和寫入磁碟,騰出記憶體,以便被需要的行為方使用。

現在大體上你們已經知道了吧,"buffer cache"有五個flush的觸發點:

1.pdflush(核心執行緒)定期flush;

2.系統和其他程式需要記憶體的時候觸發它flush;

3.使用者手工sync,外部命令觸發它flush;

4.proc核心介面觸發flush,"echo 3 >/proc/sys/vm/drop_caches;

5.應用程式內部控制flush。

 

這個"buffer cache"從概念上的理解就是這些了,實際上,更準確的說,linux從2.4開始就不再維護獨立的"buffer cache"模組了,而是把它的功能併入了"page cache"這個記憶體管理的子系統了,"buffer cache"現在已經是一個unix系統族的普遍的歷史概念了。

 

高效能寫檔案

寫100MB的資料

場景1,1次寫1個位元組,總共write 100M次;

場景2,1次寫1K個位元組,總共write 100K次;

場景3,1次寫4K個位元組,總共write 25K次;

場景4,1次寫16k個位元組,總共write大約不到7K次。

以上4種寫入方式,核心寫磁碟的次數基本相同,因為寫磁碟的單位是block,而不是位元組。現在的系統預設的block都是4k。

第1種效能非常差,user time和system time執行時間都很長,既然寫盤次數都差不多,那他慢在哪兒呢?答案是系統呼叫的次數太多

第2種,user time和system time都顯著降低,不過system time降低幅度更大

第2種以後,效能差別就不是很高了,第3種和第4種效能幾乎一樣

有興趣的朋友可以試一試,如果你的伺服器很好,可以適當放大測試樣本。

總而言之,得出的結論是以block的尺寸為write(fd, sizeof(buf),buf)的呼叫單位就可以了,再大對效能也沒什麼太大的提高。

 

題外話:一個衡量涉及IO程式的好壞的粗略標準是“程式執行應該儘量集中在user time,避免大量的system time”以及“IO的時候肯定是需要一些應用層buf的,比如上述4個場景,匹配就可以了(比如場景3,場景1和場景2會導致系統呼叫次數太多,場景4使用的buf尺寸過於浪費)”

每個系統呼叫在返回的時候,會有一個從核心態向使用者態切換的間隙,每次在這個間隙裡面,系統要幹兩個事情----遞送訊號和程式排程,其中程式排程會重新計算全部RUN狀態程式的優先順序。

系統呼叫太多的話,遞送訊號和程式排程引起的計算量是不容忽視的。

 

精確地flush "buffer cache"

在很多業務場景下,我們僅僅呼叫write()把需要寫盤的資料推送至核心的"buffer cache"中,這是很不負責任的。或許我們應該不斷地頻繁地把"buffer cache"中的資料強制flush到磁碟,盡最大可能保證我們的業務資料儘量不因斷電而丟失。

天下沒有免費的午餐,既想要效率(寫入核心buffer),又想要安全性(資料必須flush到外存介質中才安全),這似乎是很矛盾的。SUSv3(Single UNIX Specification Version 3)給了這種需求一個折中的解決方案,讓OS儘量滿足我們的苛刻的要求。介紹這個折中方案之前,有兩個SUSv3提案的規範很重要,說明如下:

1.資料完整性同步(synchronized I/O data integrity)

一個常規檔案所包含的資訊有兩種:檔案後設資料和檔案內容資料。

檔案後設資料包括:檔案所屬使用者、組、訪問許可權,檔案尺寸,檔案硬連線數目,最後訪問時間戳,最後修改時間戳,最後檔案後設資料修改時間戳,檔案資料塊指標。

對於檔案內容資料,大家應該都很清楚是什麼東西。

對於寫操作,這個規範規定了,寫檔案時保證檔案內容資料和必要的檔案後設資料保持完整性即可。粗糙地舉個例子來解釋這個規範,某次flush核心中的資料到磁碟的時候,僅僅把檔案內容資料寫入磁碟即可,但是如果這次寫檔案導致了檔案尺寸的變化,那麼這個檔案尺寸作為檔案的後設資料也需要被寫入磁碟,必要資訊保持同步。而其他的檔案後設資料,例如修改時間,訪問時間一概略去,不需要同步。

2.檔案完整性同步(synchronized I/O file integrity)

相對於資料完整性同步而言,這個規範規定了,所有內容資料以及後設資料都要同步。

 

下面來介紹linux提供的幾種flush核心緩衝資料的幾種方案,相信看完之後,大家應該知道上述提及的折中方案是怎樣的:)

1.

int fsync(int fd);

檔案完整性同步;

2.

int fdatasync(int fd);

資料完整性同步。

fdatasync相對於fsync的意義在於,fdatasync大致僅需要一次磁碟操作,而fsync需要兩次磁碟操作。舉例說明一下,假如檔案內容改變了,但是檔案尺寸並沒有發生變化,那呼叫fdatasync僅僅是把檔案內容資料flush到磁碟,而fsync不僅僅把檔案內容flush刷入磁碟,還要把檔案的last modified time也同步到磁碟檔案系統。last modified time屬於檔案的後設資料,一般情況下檔案的後設資料和檔案內容資料在磁碟上不是連續存放的,寫完內容資料再寫後設資料,必然涉及到磁碟的seek,而seek又是機械硬碟速度慢的根源。。。

在某些業務場景下,fdatasync和fsync的這點微小差別會導致應用程式效能的大幅差異。

3.

sync_file_range()

這個介面是linux從2.6.17之後實現的,是linux獨有的非標準介面。這個介面提供了比fdatasync更為精準的flush資料的能力。詳細請參照man。

4.

void sync(void);

強制"buffer cache"中的資料全部flush到磁碟,並且要遵循檔案完整性同步。

上面4種方式介紹完畢,open()系統呼叫的開啟檔案的標誌位,比如O_DSYNC諸如此類的標誌,對flush資料的影響和上面幾個介面作用類似。 


預讀

上面介紹了寫buffer以及如何控制buffer的flush,下面來講一講如何控制讀cache的行為。

讀cache這一塊,基本上,我們可以控制的就是檔案的預讀。

我們從POSIX規定的一個介面來論述一下如何控制檔案的預讀以及控制它的意義。介面原型如下:

#include

int posix_fadvise(int fd, off_t offset, off_t len, int advice);

fd:開啟檔案的描述符其實;

offset和len:指明檔案區域;

advice:預讀的方式。預讀方式及其意義如下:

1. POSIX_FADV_NORMAL:核心預設的預讀方式;

2. POSIX_FADV_RANDOM:核心禁用預讀。適合隨機讀檔案的業務,每次按業務要求的量讀取資料,不多讀;

3. POSIX_FADV_SEQUENTIALP:核心把預設的預讀量(POSIX_FADV_NORMAL)擴大一倍;

4. POSIX_FADV_WILLNEED:讀取出來的內容會被應用程式多次訪問(就是應用程式會不斷的呼叫read()對這些內容不斷的讀);

5. POSIX_FADV_NOREUSE:讀取出來的內容只會被應用程式訪問一次,訪問一次之後就清理掉並且釋放記憶體。cache伺服器,比如memcache或者redis啟動時,把檔案內容載入到應用層cache,就是這個引數存在的典型場景;

6. POSIX_FADV_DONTNEED:應用程式後續不打算訪問指定範圍中的檔案內容,核心從"page cache(buffer cache)"中刪除指定範圍的檔案內容,釋放記憶體。

 

對於POSIX_FADV_WILLNEED這種方式,linux自己有一個特定介面,原型如下:

ssize_t readahead(int fd, off64_t offset, size_t count);

linux的"buffer cache"預設預讀128k。

 

實際上,OS全域性控制"buffer cache"的操作介面不僅僅是上面提及的幾種,/proc/sys/vm/目錄下還有幾個引數可以從其他一些方面來控制"buffer cache"的行為,這部分內容在之後我整理筆記之後會介紹。


相關文章