本文隻讨論Linux下檔案的讀寫機制,不涉及不同讀取方式如read,fread,cin等的對比,這些讀取方式本質上都是調用系統api read,隻是做了不同封裝。以下所有測試均使用open, read, write這一套系統api
緩存
緩存是用來減少高速裝置通路低速裝置所需平均時間的元件,檔案讀寫涉及到計算機記憶體和磁盤,記憶體操作速度遠遠大于磁盤,如果每次調用read,write都去直接操作磁盤,一方面速度會被限制,一方面也會降低磁盤使用壽命,是以不管是對磁盤的讀操作還是寫操作,作業系統都會将資料緩存起來
Page Cache
頁緩存(Page Cache)是位于記憶體和檔案之間的緩沖區,它實際上也是一塊記憶體區域,所有的檔案IO(包括網絡檔案)都是直接和頁緩存互動,作業系統通過一系列的資料結構,比如inode, address_space, struct page,實作将一個檔案映射到頁的級别,這些具體資料結構及之間的關系我們暫且不讨論,隻需知道頁緩存的存在以及它在檔案IO中扮演着重要角色,很大一部分程度上,檔案讀寫的優化就是對頁緩存使用的優化
Dirty Page
頁緩存對應檔案中的一塊區域,如果頁緩存和對應的檔案區域内容不一緻,則該頁緩存叫做髒頁(Dirty Page)。對頁緩存進行修改或者建立頁緩存,隻要沒有刷磁盤,都會産生髒頁
檢視頁緩存大小
linux上有兩種方式檢視頁緩存大小,一種是free指令
$ free
total used free shared buffers cached
Mem: 20470840 1973416 18497424 164 270208 1202864
-/+ buffers/cache: 500344 19970496
Swap: 0 0 0
cached那一列就是頁緩存大小,機關Byte
另一種是直接檢視/proc/meminfo,這裡我們隻關注兩個字段
Cached: 1202872 kB
Dirty: 52 kB
Cached是頁緩存大小,Dirty是髒頁大小
髒頁回寫參數
Linux有一些參數可以改變作業系統對髒頁的回寫行為
$ sysctl -a 2>/dev/null | grep dirty
vm.dirty_background_ratio=10
vm.dirty_background_bytes=0
vm.dirty_ratio=20
vm.dirty_bytes=0
vm.dirty_writeback_centisecs=500
vm.dirty_expire_centisecs=3000
vm.dirty_background_ratio是記憶體可以填充髒頁的百分比,當髒頁總大小達到這個比例後,系統背景程序就會開始将髒頁刷磁盤(vm.dirty_background_bytes類似,隻不過是通過位元組數來設定)
vm.dirty_ratio是絕對的髒資料限制,記憶體裡的髒資料百分比不能超過這個值。如果髒資料超過這個數量,新的IO請求将會被阻擋,直到髒資料被寫進磁盤
vm.dirty_writeback_centisecs指定多長時間做一次髒資料寫回操作,機關為百分之一秒
vm.dirty_expire_centisecs指定髒資料能存活的時間,機關為百分之一秒,比如這裡設定為30秒,在作業系統進行寫回操作時,如果髒資料在記憶體中超過30秒時,就會被寫回磁盤
這些參數可以通過 sudo sysctl -w vm.dirty_background_ratio=5 這樣的指令來修改,需要root權限,也可以在root使用者下執行 echo 5 > /proc/sys/vm/dirty_background_ratio 來修改
檔案讀寫流程
在有了頁緩存和髒頁的概念後,我們再來看檔案的讀寫流程
讀檔案
使用者發起read操作
作業系統查找頁緩存
若未***,則産生缺頁異常,然後建立頁緩存,并從磁盤讀取相應頁填充頁緩存
若***,則直接從頁緩存傳回要讀取的内容
使用者read調用完成
寫檔案
使用者發起write操作
作業系統查找頁緩存
若未***,則産生缺頁異常,然後建立頁緩存,将使用者傳入的内容寫入頁緩存
若***,則直接将使用者傳入的内容寫入頁緩存
使用者write調用完成
頁被修改後成為髒頁,作業系統有兩種機制将髒頁寫回磁盤
使用者手動調用fsync()
由pdflush程序定時将髒頁寫回磁盤
頁緩存和磁盤檔案是有對應關系的,這種關系由作業系統維護,對頁緩存的讀寫操作是在核心态完成,對使用者來說是透明的
檔案讀寫的優化思路
不同的優化方案适應于不同的使用場景,比如檔案大小,讀寫頻次等,這裡我們不考慮修改系統參數的方案,修改系統參數總是有得有失,需要選擇一個平衡點,這和業務相關度太高,比如是否要求資料的強一緻性,是否容忍資料丢失等等。優化的思路有以下兩個考慮點
1.***化利用頁緩存
2.減少系統api調用次數
***點很容易了解,盡量讓每次IO操作都***頁緩存,這比操作磁盤會快很多,第二點提到的系統api主要是read和write,由于系統調用會從使用者态進入核心态,并且有些還伴随這記憶體資料的拷貝,是以在有些場景下減少系統調用也會提高性能
readahead
readahead是一種非阻塞的系統調用,它會觸發作業系統将檔案内容預讀到頁緩存中,并且立馬傳回,函數原型如下
ssize_t readahead(int fd, off64_t offset, size_t count);
在通常情況下,調用readahead後立馬調用read并不會提高讀取速度,我們通常在批量讀取或在讀取之前一段時間調用readahead,假設如下場景,我們需要連續讀取1000個1M的檔案,有如下兩個方案,僞代碼如下
直接調用read函數
char*buf= (char*)malloc(10*1024*1024);
for (int i=0; i<1000; ++i)
{
int fd=open_file();
int size=stat_file_size();
read(fd, buf, size);
// do something with buf
close(fd);
}
先批量調用readahead再調用read
int*fds= (int*)malloc(sizeof(int)*1000);
int* fd_size= (int*)malloc(sizeof(int)*1000);
for (int i=0; i<1000; ++i)
{
int fd=open_file();
int size=stat_file_size();
readahead(fd, 0, size);
fds[i] = fd;
fd_size[i] = size;
}
char* buf= (char*)malloc(10*1024*1024);
for (int i=0; i<1000; ++i)
{
read(fds[i], buf, fd_size[i]);
// do something with buf
close(fds[i]);
}
感興趣的可以寫代碼實際測試一下,需要注意的是在測試前必須先回寫髒頁和清空頁緩存,執行如下指令
sync && sudo sysctl -wvm.drop_caches=3
可通過檢視/proc/meminfo中的Cached及Dirty項确認是否生效
通過測試發現,第二種方法比***種讀取速度大約提高10%-20%,這種場景下是批量執行readahead後立馬執行read,優化空間有限,如果有一種場景可以在read之前一段時間調用readahead,那将大大提高read本身的讀取速度
這種方案實際上是利用了作業系統的頁緩存,即提前觸發作業系統将檔案讀取到頁緩存,并且作業系統對缺頁處理、緩存***、緩存淘汰都由一套完善的機制,雖然使用者也可以針對自己的資料做緩存管理,但和直接使用頁緩存比并沒有多大差别,而且會增加維護代價
mmap
mmap是一種記憶體映射檔案的方法,即将一個檔案或者其它對象映射到程序的位址空間,實作檔案磁盤位址和程序虛拟位址空間中一段虛拟位址的一一對映關系,函數原型如下
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
實作這樣的映射關系後,程序就可以采用指針的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁盤上,即完成了對檔案的操作而不必再調用read,write等系統調用函數。如下圖所示
mmap除了可以減少read,write等系統調用以外,還可以減少記憶體的拷貝次數,比如在read調用時,一個完整的流程是作業系統讀磁盤檔案到頁緩存,再從頁緩存将資料拷貝到read傳遞的buffer裡,而如果使用mmap之後,作業系統隻需要将磁盤讀到頁緩存,然後使用者就可以直接通過指針的方式操作mmap映射的記憶體,減少了從核心态到使用者态的資料拷貝
mmap适合于對同一塊區域頻繁讀寫的情況,比如一個64M的檔案存儲了一些索引資訊,我們需要頻繁修改并持久化到磁盤,這樣可以将檔案通過mmap映射到使用者虛拟記憶體,然後通過指針的方式修改記憶體區域,由作業系統自動将修改的部分刷回磁盤,也可以自己調用msync手動刷磁盤。
【編輯推薦】
【責任編輯:IT瘋 TEL:(010)68476606】
點贊 0